diff --git a/composer.lock b/composer.lock index 844cdb51..597efa59 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": "d00fd63e825db5ce16878c1639f362f3", + "content-hash": "1193e4106b6c84c545e6091560214ad5", "packages": [ { "name": "docopt/docopt", @@ -760,16 +760,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.2.7", + "version": "v2.2.8", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "b6202ccad4c00778887e7e8282d52f854802b59a" + "reference": "aca23e791784eade7b377d578d6dfc6fcf1398d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/b6202ccad4c00778887e7e8282d52f854802b59a", - "reference": "b6202ccad4c00778887e7e8282d52f854802b59a", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/aca23e791784eade7b377d578d6dfc6fcf1398d2", + "reference": "aca23e791784eade7b377d578d6dfc6fcf1398d2", "shasum": "" }, "require": { @@ -778,7 +778,7 @@ "ext-json": "*", "ext-tokenizer": "*", "gecko-packages/gecko-php-unit": "^2.0", - "php": "^5.3.6 || >=7.0 <7.2", + "php": "^5.3.6 || >=7.0 <7.3", "sebastian/diff": "^1.4", "symfony/console": "^2.4 || ^3.0", "symfony/event-dispatcher": "^2.1 || ^3.0", @@ -841,7 +841,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-09-11T14:27:07+00:00" + "time": "2017-09-29T15:07:49+00:00" }, { "name": "gecko-packages/gecko-php-unit", @@ -1136,16 +1136,16 @@ }, { "name": "jms/serializer", - "version": "1.8.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "ce65798f722c836f16d5d7d2e3ca9d21e0fb4331" + "reference": "f4683f41ebf21e60667447bb49939bee35807c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ce65798f722c836f16d5d7d2e3ca9d21e0fb4331", - "reference": "ce65798f722c836f16d5d7d2e3ca9d21e0fb4331", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/f4683f41ebf21e60667447bb49939bee35807c3c", + "reference": "f4683f41ebf21e60667447bb49939bee35807c3c", "shasum": "" }, "require": { @@ -1184,7 +1184,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -1215,7 +1215,7 @@ "serialization", "xml" ], - "time": "2017-07-13T11:23:56+00:00" + "time": "2017-09-28T15:17:28+00:00" }, { "name": "justinrainbow/json-schema", @@ -1539,16 +1539,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.10", + "version": "v2.0.11", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", "shasum": "" }, "require": { @@ -1583,7 +1583,7 @@ "pseudorandom", "random" ], - "time": "2017-03-13T16:27:32+00:00" + "time": "2017-09-27T21:40:39+00:00" }, { "name": "pear/archive_tar", @@ -3632,16 +3632,16 @@ }, { "name": "symfony/config", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "0b8541d18507d10204a08384640ff6df3c739ebe" + "reference": "1dbeaa8e2db4b29159265867efff075ad961558c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/0b8541d18507d10204a08384640ff6df3c739ebe", - "reference": "0b8541d18507d10204a08384640ff6df3c739ebe", + "url": "https://api.github.com/repos/symfony/config/zipball/1dbeaa8e2db4b29159265867efff075ad961558c", + "reference": "1dbeaa8e2db4b29159265867efff075ad961558c", "shasum": "" }, "require": { @@ -3684,20 +3684,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-10-04T18:56:36+00:00" }, { "name": "symfony/console", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253" + "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c0807a2ca978e64d8945d373a9221a5c35d1a253", - "reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253", + "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853", + "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853", "shasum": "" }, "require": { @@ -3745,7 +3745,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/debug", @@ -3806,16 +3806,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d" + "reference": "7fe089232554357efb8d4af65ce209fc6e5a2186" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1377400fd641d7d1935981546aaef780ecd5bf6d", - "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7fe089232554357efb8d4af65ce209fc6e5a2186", + "reference": "7fe089232554357efb8d4af65ce209fc6e5a2186", "shasum": "" }, "require": { @@ -3862,7 +3862,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-02T07:47:27+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/filesystem", @@ -3915,16 +3915,16 @@ }, { "name": "symfony/finder", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4f4e84811004e065a3bb5ceeb1d9aa592630f9ad" + "reference": "a945724b201f74d543e356f6059c930bb8d10c92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4f4e84811004e065a3bb5ceeb1d9aa592630f9ad", - "reference": "4f4e84811004e065a3bb5ceeb1d9aa592630f9ad", + "url": "https://api.github.com/repos/symfony/finder/zipball/a945724b201f74d543e356f6059c930bb8d10c92", + "reference": "a945724b201f74d543e356f6059c930bb8d10c92", "shasum": "" }, "require": { @@ -3960,11 +3960,11 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01T20:52:29+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/options-resolver", - "version": "v3.3.9", + "version": "v3.3.10", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -4018,16 +4018,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", - "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", "shasum": "" }, "require": { @@ -4039,7 +4039,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -4073,20 +4073,20 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php54", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "b7763422a5334c914ef0298ed21b253d25913a6e" + "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/b7763422a5334c914ef0298ed21b253d25913a6e", - "reference": "b7763422a5334c914ef0298ed21b253d25913a6e", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/d7810a14b2c6c1aff415e1bb755f611b3d5327bc", + "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc", "shasum": "" }, "require": { @@ -4095,7 +4095,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -4131,20 +4131,20 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php55", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68" + "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/29b1381d66f16e0581aab0b9f678ccf073288f68", - "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/b64e7f0c37ecf144ecc16668936eef94e628fbfd", + "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd", "shasum": "" }, "require": { @@ -4154,7 +4154,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -4187,20 +4187,20 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "b6482e68974486984f59449ecea1fbbb22ff840f" + "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/b6482e68974486984f59449ecea1fbbb22ff840f", - "reference": "b6482e68974486984f59449ecea1fbbb22ff840f", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", + "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", "shasum": "" }, "require": { @@ -4210,7 +4210,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -4246,20 +4246,20 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "8abc9097f5001d310f0edba727469c988acc6ea7" + "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8abc9097f5001d310f0edba727469c988acc6ea7", - "reference": "8abc9097f5001d310f0edba727469c988acc6ea7", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/6de4f4884b97abbbed9f0a84a95ff2ff77254254", + "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254", "shasum": "" }, "require": { @@ -4268,7 +4268,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -4301,20 +4301,20 @@ "portable", "shim" ], - "time": "2017-07-11T13:25:55+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/process", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8" + "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8", - "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8", + "url": "https://api.github.com/repos/symfony/process/zipball/26c9fb02bf06bd6b90f661a5bd17e510810d0176", + "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176", "shasum": "" }, "require": { @@ -4350,20 +4350,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-03T08:04:30+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/stopwatch", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "e02577b841394a78306d7b547701bb7bb705bad5" + "reference": "28ee62ea4736431ca817cdaebcb005663e9cd1cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e02577b841394a78306d7b547701bb7bb705bad5", - "reference": "e02577b841394a78306d7b547701bb7bb705bad5", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/28ee62ea4736431ca817cdaebcb005663e9cd1cb", + "reference": "28ee62ea4736431ca817cdaebcb005663e9cd1cb", "shasum": "" }, "require": { @@ -4399,7 +4399,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/translation", @@ -4467,16 +4467,16 @@ }, { "name": "symfony/validator", - "version": "v2.8.27", + "version": "v2.8.28", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "864ba6865e253a7ffc3db5629af676cfdc3bd104" + "reference": "1531ddfd96efd1b2c231cbf45f22e652a8f67925" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/864ba6865e253a7ffc3db5629af676cfdc3bd104", - "reference": "864ba6865e253a7ffc3db5629af676cfdc3bd104", + "url": "https://api.github.com/repos/symfony/validator/zipball/1531ddfd96efd1b2c231cbf45f22e652a8f67925", + "reference": "1531ddfd96efd1b2c231cbf45f22e652a8f67925", "shasum": "" }, "require": { @@ -4536,20 +4536,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/yaml", - "version": "v3.3.9", + "version": "v3.3.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0" + "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0", - "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46", + "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46", "shasum": "" }, "require": { @@ -4591,7 +4591,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" + "time": "2017-10-05T14:43:42+00:00" }, { "name": "theseer/tokenizer", @@ -4635,16 +4635,16 @@ }, { "name": "twig/twig", - "version": "v1.34.4", + "version": "v1.35.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "f878bab48edb66ad9c6ed626bf817f60c6c096ee" + "reference": "daa657073e55b0a78cce8fdd22682fddecc6385f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/f878bab48edb66ad9c6ed626bf817f60c6c096ee", - "reference": "f878bab48edb66ad9c6ed626bf817f60c6c096ee", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/daa657073e55b0a78cce8fdd22682fddecc6385f", + "reference": "daa657073e55b0a78cce8fdd22682fddecc6385f", "shasum": "" }, "require": { @@ -4658,7 +4658,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.34-dev" + "dev-master": "1.35-dev" } }, "autoload": { @@ -4696,7 +4696,7 @@ "keywords": [ "templating" ], - "time": "2017-07-04T13:19:31+00:00" + "time": "2017-09-27T18:06:46+00:00" }, { "name": "webmozart/assert", diff --git a/lib/AbstractException.php b/lib/AbstractException.php index c29f6fe1..2ae34d33 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -5,6 +5,8 @@ namespace JKingWeb\Arsse; abstract class AbstractException extends \Exception { const CODES = [ "Exception.uncoded" => -1, "Exception.unknown" => 10000, + "ExceptionType.strictFailure" => 10011, + "ExceptionType.typeUnknown" => 10012, "Lang/Exception.defaultFileMissing" => 10101, "Lang/Exception.fileMissing" => 10102, "Lang/Exception.fileUnreadable" => 10103, diff --git a/lib/ExceptionType.php b/lib/ExceptionType.php new file mode 100644 index 00000000..16094cef --- /dev/null +++ b/lib/ExceptionType.php @@ -0,0 +1,6 @@ + ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'iso8601m' => ["!Y-m-d\TH:i:s.u", "Y-m-d\TH:i:s.u\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'microtime' => ["U.u", "0.u00 U" ], // NOTE: the actual input format at the user level matches the output format; pre-processing is required for PHP not to fail + 'http' => ["!D, d M Y H:i:s \G\M\T", "D, d M Y H:i:s \G\M\T"], + 'sql' => ["!Y-m-d H:i:s", "Y-m-d H:i:s" ], + 'date' => ["!Y-m-d", "Y-m-d" ], + 'time' => ["!H:i:s", "H:i:s" ], + 'unix' => ["U", "U" ], + 'float' => ["U.u", "U.u" ], + ]; + public static function transform($date, string $outFormat = null, string $inFormat = null, bool $inLocal = false) { $date = self::normalize($date, $inFormat, $inLocal); if (is_null($date) || is_null($outFormat)) { @@ -14,7 +26,7 @@ class Date { } switch ($outFormat) { case 'http': $f = "D, d M Y H:i:s \G\M\T"; break; - case 'iso8601': $f = "Y-m-d\TH:i:s"; break; + case 'iso8601': $f = "Y-m-d\TH:i:s"; break; case 'sql': $f = "Y-m-d H:i:s"; break; case 'date': $f = "Y-m-d"; break; case 'time': $f = "H:i:s"; break; @@ -23,41 +35,8 @@ class Date { return $date->format($f); } - public static function normalize($date, string $inFormat = null, bool $inLocal = false) { - if ($date instanceof \DateTimeInterface) { - return $date; - } elseif (is_numeric($date)) { - $time = (int) $date; - } elseif ($date===null) { - return null; - } elseif (is_string($date)) { - try { - $tz = (!$inLocal) ? new \DateTimeZone("UTC") : null; - if (!is_null($inFormat)) { - switch ($inFormat) { - case 'http': $f = "D, d M Y H:i:s \G\M\T"; break; - case 'iso8601': $f = "Y-m-d\TH:i:sP"; break; - case 'sql': $f = "Y-m-d H:i:s"; break; - case 'date': $f = "Y-m-d"; break; - case 'time': $f = "H:i:s"; break; - default: $f = $inFormat; break; - } - return \DateTime::createFromFormat("!".$f, $date, $tz); - } else { - return new \DateTime($date, $tz); - } - } catch (\Throwable $e) { - return null; - } - } elseif (is_bool($date)) { - return null; - } else { - $time = (int) $date; - } - $tz = (!$inLocal) ? new \DateTimeZone("UTC") : null; - $d = new \DateTime("now", $tz); - $d->setTimestamp($time); - return $d; + public static function normalize($date, string $inFormat = null) { + return ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat); } public static function add(string $interval, $date = null): \DateTimeInterface { diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index c382836b..09dca6d2 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Misc; +use JKingWeb\Arsse\ExceptionType; + class ValueInfo { // universal const VALID = 1 << 0; @@ -9,9 +11,232 @@ class ValueInfo { // integers const ZERO = 1 << 2; const NEG = 1 << 3; + const FLOAT = 1 << 4; // strings const EMPTY = 1 << 2; const WHITE = 1 << 3; + //normalization types + const T_MIXED = 0; // pass through unchanged + const T_NULL = 1; // convert to null + const T_BOOL = 2; // convert to boolean + const T_INT = 3; // convert to integer + const T_FLOAT = 4; // convert to floating point + const T_DATE = 5; // convert to DateTimeInterface instance + const T_STRING = 6; // convert to string + const T_ARRAY = 7; // convert to array + //normalization modes + const M_NULL = 1 << 28; // pass nulls through regardless of target type + const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match + const M_STRICT = 1 << 30; // throw an exception if the type doesn't match + const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable + + public function normalize($value, int $type, string $dateFormat = null) { + $allowNull = ($type & self::M_NULL); + $strict = ($type & (self::M_STRICT | self::M_DROP)); + $drop = ($type & self::M_DROP); + $arrayVal = ($type & self::M_ARRAY); + $type = ($type & ~(self::M_NULL | self::M_DROP | self::M_STRICT | self::M_ARRAY)); + // if the value is null and this is allowed, simply return + if ($allowNull && is_null($value)) { + return null; + } + // if the value is supposed to be an array, handle it specially + if ($arrayVal) { + $value = self::normalize($value, self::T_ARRAY); + foreach ($value as $key => $v) { + $value[$key] = self::normalize($v, $type | ($allowNull ? self::M_NULL : 0) | ($strict ? self::M_STRICT : 0) | ($drop ? self::M_DROP : 0), $dateFormat); + } + return $value; + } + switch ($type) { + case self::T_MIXED: + return $value; + case self::T_NULL: + return null; + case self::T_BOOL: + if (is_bool($value)) { + return $value; + } + $out = self::bool($value); + if ($strict && is_null($out)) { + // if strict and input is not a boolean, this is an error + if ($drop) { + return null; + } + throw new ExceptionType("strictFailure", $type); + } elseif (is_float($value) && is_nan($value)) { + return false; + } elseif (is_null($out)) { + // if not strict and input is not a boolean, return a simple type-cast + return (bool) $value; + } + return $out; + case self::T_INT: + if (is_int($value)) { + return $value; + } elseif ($value instanceof \DateTimeInterface) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } + return (!$drop) ? (int) $value->getTimestamp(): null; + } + $info = self::int($value); + if ($strict && !($info & self::VALID)) { + // if strict and input is not an integer, this is an error + if ($drop) { + return null; + } + throw new ExceptionType("strictFailure", $type); + } elseif (is_bool($value)) { + return (int) $value; + } elseif ($info & (self::VALID | self::FLOAT)) { + $out = strtolower((string) $value); + if (strpos($out, "e")) { + return (int) (float) $out; + } else { + return (int) $out; + } + } else { + return 0; + } + case self::T_FLOAT: + if (is_float($value)) { + return $value; + } elseif ($value instanceof \DateTimeInterface) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } + return (!$drop) ? (float) $value->getTimestamp(): null; + } elseif (is_bool($value) && $strict) { + if ($drop) { + return null; + } + throw new ExceptionType("strictFailure", $type); + } + $out = filter_var($value, \FILTER_VALIDATE_FLOAT); + if ($strict && $out===false) { + // if strict and input is not a float, this is an error + if ($drop) { + return null; + } + throw new ExceptionType("strictFailure", $type); + } + return (float) $out; + case self::T_STRING: + if (is_string($value)) { + return $value; + } + if ($value instanceof \DateTimeImmutable) { + return $value->setTimezone(new \DateTimeZone("UTC"))->format(Date::FORMAT['iso8601'][1]); + } elseif ($value instanceof \DateTime) { + $out = clone $value; + $out->setTimezone(new \DateTimeZone("UTC")); + return $out->format(Date::FORMAT['iso8601'][1]); + } elseif (is_float($value) && is_finite($value)) { + $out = (string) $value; + if(!strpos($out, "E")) { + return $out; + } else { + $out = sprintf("%F", $value); + return substr($out, -2)==".0" ? (string) (int) $out : $out; + } + } + $info = self::str($value); + if (!($info & self::VALID)) { + if ($drop) { + return null; + } elseif ($strict) { + // if strict and input is not a string, this is an error + throw new ExceptionType("strictFailure", $type); + } elseif (!is_scalar($value)) { + return ""; + } else { + return (string) $value; + } + } else { + return (string) $value; + } + case self::T_DATE: + if ($value instanceof \DateTimeImmutable) { + return $value->setTimezone(new \DateTimeZone("UTC")); + } elseif ($value instanceof \DateTime) { + $out = clone $value; + $out->setTimezone(new \DateTimeZone("UTC")); + return $out; + } elseif (is_int($value)) { + return \DateTime::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); + } elseif (is_float($value)) { + return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); + } elseif (is_string($value)) { + try { + if (!is_null($dateFormat)) { + $out = false; + if ($dateFormat=="microtime") { + // PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float + if (preg_match("<^0\.\d{6}00 \d+$>", $value)) { + $value = substr($value,11).".".substr($value,2,6); + } else { + throw new \Exception; + } + } + $f = isset(Date::FORMAT[$dateFormat]) ? Date::FORMAT[$dateFormat][0] : $dateFormat; + if ($dateFormat=="iso8601" || $dateFormat=="iso8601m") { + // DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works + if ($dateFormat=="iso8601m") { + $f2 = Date::FORMAT["iso8601"][0]; + $zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"]; + } else { + $zones = [$f."", $f."\Z", $f."P", $f."O"]; + } + do { + $ftz = array_shift($zones); + $out = \DateTime::createFromFormat($ftz, $value, new \DateTimeZone("UTC")); + } while (!$out && $zones); + } else { + $out = \DateTime::createFromFormat($f, $value, new \DateTimeZone("UTC")); + } + if (!$out) { + throw new \Exception; + } + return $out; + } else { + return new \DateTime($value, new \DateTimeZone("UTC")); + } + } catch (\Exception $e) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } + return null; + } + } elseif ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } + return null; + case self::T_ARRAY: + if (is_array($value)) { + return $value; + } elseif ($value instanceof \Traversable) { + $out = []; + foreach ($value as $k => $v) { + $out[$k] = $v; + } + return $out; + } else { + if ($drop) { + return null; + } elseif ($strict) { + // if strict and input is not a string, this is an error + throw new ExceptionType("strictFailure", $type); + } elseif (is_null($value) || (is_float($value) && is_nan($value))) { + return []; + } else { + return [$value]; + } + } + default: + throw new ExceptionType("typeUnknown", $type); // @codeCoverageIgnore + } + } public static function int($value): int { $out = 0; @@ -19,28 +244,42 @@ class ValueInfo { // check if the input is null return self::NULL; } elseif (is_string($value) || (is_object($value) && method_exists($value, "__toString"))) { - $value = (string) $value; + $value = strtolower((string) $value); // normalize a string an integer or float if possible if (!strlen($value)) { // the empty string is equivalent to null when evaluating an integer return self::NULL; - } elseif (filter_var($value, \FILTER_VALIDATE_FLOAT) !== false && !fmod((float) $value, 1)) { - // an integral float is acceptable - $value = (int) $value; + } + // interpret the value as a float + $float = filter_var($value, \FILTER_VALIDATE_FLOAT); + if ($float !== false) { + if (!fmod($float, 1)) { + // an integral float is acceptable + $value = (int) (!strpos($value, "e") ? $value : $float); + } else { + $out += self::FLOAT; + $value = $float; + } } else { return $out; } - } elseif (is_float($value) && !fmod($value, 1)) { - // an integral float is acceptable - $value = (int) $value; + } elseif (is_float($value)) { + if (!fmod($value, 1)) { + // an integral float is acceptable + $value = (int) $value; + } else { + $out += self::FLOAT; + } } elseif (!is_int($value)) { // if the value is not an integer or integral float, stop return $out; } // mark validity - $out += self::VALID; + if (is_int($value)) { + $out += self::VALID; + } // mark zeroness - if ($value==0) { + if (!$value) { $out += self::ZERO; } // mark negativeness @@ -96,8 +335,8 @@ class ValueInfo { } $out = filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE); if (is_null($out) && (ValueInfo::int($value) & ValueInfo::VALID)) { - $out = abs((int) filter_var($value, \FILTER_VALIDATE_FLOAT)); - return ($out < 2) ? (bool) $out : $default; + $out = (int) filter_var($value, \FILTER_VALIDATE_FLOAT); + return ($out==1 || $out==0) ? (bool) $out : $default; } return !is_null($out) ? $out : $default; } diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 81c3a68a..7cc7871d 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -32,52 +32,13 @@ abstract class AbstractHandler implements Handler { return $data; } - protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array { + protected function normalizeInput(array $data, array $types, string $dateFormat = null, int $mode = 0): array { $out = []; - foreach ($data as $key => $value) { - if (!isset($types[$key])) { - $out[$key] = $value; - continue; - } - if (is_null($value)) { + foreach ($types as $key => $type) { + if (isset($data[$key])) { + $out[$key] = ValueInfo::normalize($data[$key], $type | $mode, $dateFormat); + } else { $out[$key] = null; - continue; - } - switch ($types[$key]) { - case "int": - if (valueInfo::int($value) & ValueInfo::VALID) { - $out[$key] = (int) $value; - } - break; - case "string": - if (is_bool($value)) { - $out[$key] = var_export($value, true); - } elseif (!is_scalar($value)) { - break; - } else { - $out[$key] = (string) $value; - } - break; - case "bool": - $test = ValueInfo::bool($value); - if (!is_null($test)) { - $out[$key] = $test; - } - break; - case "float": - $test = filter_var($value, \FILTER_VALIDATE_FLOAT); - if ($test !== false) { - $out[$key] = $test; - } - break; - case "datetime": - $t = Date::normalize($value, $dateFormat); - if ($t) { - $out[$key] = $t; - } - break; - default: - throw new Exception("typeUnknown", $types[$key]); } } return $out; diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 46a6a1c0..4fefdd6d 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -21,21 +21,21 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { protected $dateFormat = "unix"; protected $validInput = [ - 'name' => "string", - 'url' => "string", - 'folderId' => "int", - 'feedTitle' => "string", - 'userId' => "string", - 'feedId' => "int", - 'newestItemId' => "int", - 'batchSize' => "int", - 'offset' => "int", - 'type' => "int", - 'id' => "int", - 'getRead' => "bool", - 'oldestFirst' => "bool", - 'lastModified' => "datetime", - // 'items' => "array int", // just pass these through + 'name' => ValueInfo::T_STRING, + 'url' => ValueInfo::T_STRING, + 'folderId' => ValueInfo::T_INT, + 'feedTitle' => ValueInfo::T_STRING, + 'userId' => ValueInfo::T_STRING, + 'feedId' => ValueInfo::T_INT, + 'newestItemId' => ValueInfo::T_INT, + 'batchSize' => ValueInfo::T_INT, + 'offset' => ValueInfo::T_INT, + 'type' => ValueInfo::T_INT, + 'id' => ValueInfo::T_INT, + 'getRead' => ValueInfo::T_BOOL, + 'oldestFirst' => ValueInfo::T_BOOL, + 'lastModified' => ValueInfo::T_DATE, + 'items' => ValueInfo::T_MIXED | ValueInfo::M_ARRAY, ]; public function __construct() { @@ -61,10 +61,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $data = []; } // FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ? - $data = $this->normalizeInput($data, $this->validInput, "U"); - $query = $this->normalizeInput($req->query, $this->validInput, "U"); - $data = array_merge($data, $query); - unset($query); + $data = $this->normalizeInput(array_merge($data, $req->query), $this->validInput, "unix"); // check to make sure the requested function is implemented try { $func = $this->chooseCall($req->paths, $req->method); @@ -233,7 +230,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // create a folder protected function folderAdd(array $url, array $data): Response { try { - $folder = Arsse::$db->folderAdd(Arsse::$user->id, $data); + $folder = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => $data['name']]); } catch (ExceptionInput $e) { switch ($e->getCode()) { // folder already exists @@ -263,13 +260,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // rename a folder (also supports moving nesting folders, but this is not a feature of the API) protected function folderRename(array $url, array $data): Response { - // there must be some change to be made - if (!sizeof($data)) { - return new Response(422); - } - // perform the edit try { - Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], $data); + Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], ['name' => $data['name']]); } catch (ExceptionInput $e) { switch ($e->getCode()) { // folder does not exist @@ -288,15 +280,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // mark all articles associated with a folder as read protected function folderMarkRead(array $url, array $data): Response { - $c = new Context; - if (isset($data['newestItemId'])) { - // if the item ID is valid (i.e. an integer), add it to the context - $c->latestEdition($data['newestItemId']); - } else { - // otherwise return an error + if (!ValueInfo::id($data['newestItemId'])) { + // if the item ID is invalid (i.e. not a positive integer), this is an error return new Response(422); } - // add the folder ID to the context + // build the context + $c = new Context; + $c->latestEdition((int) $data['newestItemId']); $c->folder((int) $url[1]); // perform the operation try { @@ -330,10 +320,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) { return new Response(403); } - // perform an update of a single feed - if (!isset($data['feedId'])) { - return new Response(422); - } try { Arsse::$db->feedUpdate($data['feedId']); } catch (ExceptionInput $e) { @@ -351,16 +337,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // add a new feed protected function subscriptionAdd(array $url, array $data): Response { - // normalize the feed URL - if (!isset($data['url'])) { - return new Response(422); - } - // normalize the folder ID, if specified - $folder = isset($data['folderId']) ? $data['folderId'] : null; // try to add the feed $tr = Arsse::$db->begin(); try { - $id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['url']); + $id = Arsse::$db->subscriptionAdd(Arsse::$user->id, (string) $data['url']); } catch (ExceptionInput $e) { // feed already exists return new Response(409); @@ -369,9 +349,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(422); } // if a folder was specified, move the feed to the correct folder; silently ignore errors - if ($folder) { + if ($data['folderId']) { try { - Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $folder]); + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $data['folderId']]); } catch (ExceptionInput $e) { } } @@ -416,16 +396,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // rename a feed protected function subscriptionRename(array $url, array $data): Response { - // normalize input - $in = []; - if (array_key_exists('feedTitle', $data)) { // we use array_key_exists because null is a valid input - $in['title'] = $data['feedTitle']; - } else { - return new Response(422); - } - // perform the renaming try { - Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in); + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['title' => (string) $data['feedTitle']]); } catch (ExceptionInput $e) { switch ($e->getCode()) { // subscription does not exist @@ -442,16 +414,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // move a feed to a folder protected function subscriptionMove(array $url, array $data): Response { - // normalize input - $in = []; - if (isset($data['folderId'])) { - $in['folder'] = $data['folderId'] ? $data['folderId'] : null; - } else { + // if no folder is specified this is an error + if (!isset($data['folderId'])) { return new Response(422); } // perform the move try { - Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in); + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['folder' => $data['folderId']]); } catch (ExceptionInput $e) { switch ($e->getCode()) { case 10239: // subscription does not exist @@ -468,14 +437,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // mark all articles associated with a subscription as read protected function subscriptionMarkRead(array $url, array $data): Response { - $c = new Context; - if (isset($data['newestItemId'])) { - $c->latestEdition($data['newestItemId']); - } else { - // otherwise return an error + if (!ValueInfo::id($data['newestItemId'])) { + // if the item ID is invalid (i.e. not a positive integer), this is an error return new Response(422); } - // add the subscription ID to the context + // build the context + $c = new Context; + $c->latestEdition((int) $data['newestItemId']); $c->subscription((int) $url[1]); // perform the operation try { @@ -492,17 +460,17 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // set the context options supplied by the client $c = new Context; // set the batch size - if (isset($data['batchSize']) && $data['batchSize'] > 0) { + if ($data['batchSize'] > 0) { $c->limit($data['batchSize']); } // set the order of returned items - if (isset($data['oldestFirst']) && $data['oldestFirst']) { + if ($data['oldestFirst']) { $c->reverse(false); } else { $c->reverse(true); } // set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one - if (isset($data['offset']) && $data['offset'] > 0) { + if ($data['offset'] > 0) { if ($c->reverse) { $c->latestEdition($data['offset'] - 1); } else { @@ -510,13 +478,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } } // set whether to only return unread - if (isset($data['getRead']) && !$data['getRead']) { + if (!ValueInfo::bool($data['getRead'], true)) { $c->unread(true); } // if no type is specified assume 3 (All) - if (!isset($data['type'])) { - $data['type'] = 3; - } + $data['type'] = $data['type'] ?? 3; switch ($data['type']) { case 0: // feed if (isset($data['id'])) { @@ -535,7 +501,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // return all items } // whether to return only updated items - if (isset($data['lastModified'])) { + if ($data['lastModified']) { $c->modifiedSince($data['lastModified']); } // perform the fetch @@ -555,14 +521,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // mark all articles as read protected function articleMarkReadAll(array $url, array $data): Response { - $c = new Context; - if (isset($data['newestItemId'])) { - // set the newest item ID as specified - $c->latestEdition($data['newestItemId']); - } else { - // otherwise return an error + if (!ValueInfo::id($data['newestItemId'])) { + // if the item ID is invalid (i.e. not a positive integer), this is an error return new Response(422); } + // build the context + $c = new Context; + $c->latestEdition((int) $data['newestItemId']); // perform the operation Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); return new Response(204); @@ -604,13 +569,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function articleMarkReadMulti(array $url, array $data): Response { // determine whether to mark read or unread $set = ($url[1]=="read"); - // if the input data is not at all valid, return an error - if (!isset($data['items']) || !is_array($data['items'])) { - return new Response(422); - } // start a transaction and loop through the items $t = Arsse::$db->begin(); - $in = array_chunk($data['items'], 50); + $in = array_chunk($data['items'] ?? [], 50); for ($a = 0; $a < sizeof($in); $a++) { // initialize the matching context $c = new Context; @@ -628,13 +589,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function articleMarkStarredMulti(array $url, array $data): Response { // determine whether to mark starred or unstarred $set = ($url[1]=="star"); - // if the input data is not at all valid, return an error - if (!isset($data['items']) || !is_array($data['items'])) { - return new Response(422); - } // start a transaction and loop through the items $t = Arsse::$db->begin(); - $in = array_chunk(array_column($data['items'], "guidHash"), 50); + $in = array_chunk(array_column($data['items'] ?? [], "guidHash"), 50); for ($a = 0; $a < sizeof($in); $a++) { // initialize the matching context $c = new Context; diff --git a/locale/en.php b/locale/en.php index 0539a0f9..d151beea 100644 --- a/locale/en.php +++ b/locale/en.php @@ -74,6 +74,17 @@ return [ 'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php', // this should not usually be encountered 'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred', + 'Exception.JKingWeb/Arsse/ExceptionType.strictFailure' => 'Supplied value could not be normalized to {0, select, + 1 {null} + 2 {boolean} + 3 {integer} + 4 {float} + 5 {datetime} + 6 {string} + 7 {array} + other {requested type} + }', + 'Exception.JKingWeb/Arsse/ExceptionType.typeUnknown' => 'Normalization type {0} is not implemented', 'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', 'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', 'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', diff --git a/tests/Misc/TestValueInfo.php b/tests/Misc/TestValueInfo.php index 1291b47a..b886e48a 100644 --- a/tests/Misc/TestValueInfo.php +++ b/tests/Misc/TestValueInfo.php @@ -7,6 +7,10 @@ use JKingWeb\Arsse\Test\Misc\StrClass; /** @covers \JKingWeb\Arsse\Misc\ValueInfo */ class TestValueInfo extends Test\AbstractTest { + public function setUp() { + $this->clearData(); + } + public function testGetIntegerInfo() { $tests = [ [null, I::NULL], @@ -58,9 +62,9 @@ class TestValueInfo extends Test\AbstractTest { ["no", 0], ["true", 0], ["false", 0], - [INF, 0], - [-INF, 0], - [NAN, 0], + [INF, I::FLOAT], + [-INF, I::FLOAT | I::NEG], + [NAN, I::FLOAT], [[], 0], ["some string", 0], [" ", 0], @@ -71,6 +75,10 @@ class TestValueInfo extends Test\AbstractTest { [new StrClass("-1"), I::VALID | I::NEG], [new StrClass("Msg"), 0], [new StrClass(" "), 0], + [2.5, I::FLOAT], + [0.5, I::FLOAT], + ["2.5", I::FLOAT], + ["0.5", I::FLOAT], ]; foreach ($tests as $test) { list($value, $exp) = $test; @@ -249,13 +257,13 @@ class TestValueInfo extends Test\AbstractTest { ["+000", false], ["+0.0", false], ["+000.000", false], - [-1, true], - [-1.0, true], - ["-1.0", true], - ["-001.0", true], + [-1, null], + [-1.0, null], + ["-1.0", null], + ["-001.0", null], ["-1.0e2", null], - ["-1", true], - ["-001", true], + ["-1", null], + ["-001", null], ["-1e2", null], [-0, false], ["-0", false], @@ -281,7 +289,7 @@ class TestValueInfo extends Test\AbstractTest { [new StrClass(""), false], [new StrClass("1"), true], [new StrClass("0"), false], - [new StrClass("-1"), true], + [new StrClass("-1"), null], [new StrClass("Msg"), null], [new StrClass(" "), null], ]; @@ -294,4 +302,215 @@ class TestValueInfo extends Test\AbstractTest { } } } + + public function testNormalizeValues() { + $tests = [ + /* The test data are very dense for this set. Each value is normalized to each of the following types: + + - mixed (no normalization performed) + - null + - boolean + - integer + - float + - string + - array + + For each of these types, there is an expected output value, as well as a boolean indicating whether + the value should pass or fail a strict normalization. Conversion to DateTime is covered below by a different data set + */ + /* Input value null bool int float string array */ + [null, [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], false]], + ["", [null,true], [false,true], [0, false], [0.0, false], ["", true], [[""], false]], + [1, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1], false]], + [PHP_INT_MAX, [null,true], [true, false], [PHP_INT_MAX, true], [(float) PHP_INT_MAX,true], [(string) PHP_INT_MAX, true], [[PHP_INT_MAX], false]], + [1.0, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1.0], false]], + ["1.0", [null,true], [true, true], [1, true], [1.0, true], ["1.0", true], [["1.0"], false]], + ["001.0", [null,true], [true, true], [1, true], [1.0, true], ["001.0", true], [["001.0"], false]], + ["1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["1.0e2", true], [["1.0e2"], false]], + ["1", [null,true], [true, true], [1, true], [1.0, true], ["1", true], [["1"], false]], + ["001", [null,true], [true, true], [1, true], [1.0, true], ["001", true], [["001"], false]], + ["1e2", [null,true], [true, false], [100, true], [100.0, true], ["1e2", true], [["1e2"], false]], + ["+1.0", [null,true], [true, true], [1, true], [1.0, true], ["+1.0", true], [["+1.0"], false]], + ["+001.0", [null,true], [true, true], [1, true], [1.0, true], ["+001.0", true], [["+001.0"], false]], + ["+1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["+1.0e2", true], [["+1.0e2"], false]], + ["+1", [null,true], [true, true], [1, true], [1.0, true], ["+1", true], [["+1"], false]], + ["+001", [null,true], [true, true], [1, true], [1.0, true], ["+001", true], [["+001"], false]], + ["+1e2", [null,true], [true, false], [100, true], [100.0, true], ["+1e2", true], [["+1e2"], false]], + [0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0], false]], + ["0", [null,true], [false,true], [0, true], [0.0, true], ["0", true], [["0"], false]], + ["000", [null,true], [false,true], [0, true], [0.0, true], ["000", true], [["000"], false]], + [0.0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0.0], false]], + ["0.0", [null,true], [false,true], [0, true], [0.0, true], ["0.0", true], [["0.0"], false]], + ["000.000", [null,true], [false,true], [0, true], [0.0, true], ["000.000", true], [["000.000"], false]], + ["+0", [null,true], [false,true], [0, true], [0.0, true], ["+0", true], [["+0"], false]], + ["+000", [null,true], [false,true], [0, true], [0.0, true], ["+000", true], [["+000"], false]], + ["+0.0", [null,true], [false,true], [0, true], [0.0, true], ["+0.0", true], [["+0.0"], false]], + ["+000.000", [null,true], [false,true], [0, true], [0.0, true], ["+000.000", true], [["+000.000"], false]], + [-1, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1], false]], + [-1.0, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1.0], false]], + ["-1.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-1.0", true], [["-1.0"], false]], + ["-001.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-001.0", true], [["-001.0"], false]], + ["-1.0e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1.0e2", true], [["-1.0e2"], false]], + ["-1", [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [["-1"], false]], + ["-001", [null,true], [true, false], [-1, true], [-1.0, true], ["-001", true], [["-001"], false]], + ["-1e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1e2", true], [["-1e2"], false]], + [-0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[-0], false]], + ["-0", [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [["-0"], false]], + ["-000", [null,true], [false,true], [0, true], [-0.0, true], ["-000", true], [["-000"], false]], + [-0.0, [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [[-0.0], false]], + ["-0.0", [null,true], [false,true], [0, true], [-0.0, true], ["-0.0", true], [["-0.0"], false]], + ["-000.000", [null,true], [false,true], [0, true], [-0.0, true], ["-000.000", true], [["-000.000"], false]], + [false, [null,true], [false,true], [0, false], [0.0, false], ["", false], [[false], false]], + [true, [null,true], [true, true], [1, false], [1.0, false], ["1", false], [[true], false]], + ["on", [null,true], [true, true], [0, false], [0.0, false], ["on", true], [["on"], false]], + ["off", [null,true], [false,true], [0, false], [0.0, false], ["off", true], [["off"], false]], + ["yes", [null,true], [true, true], [0, false], [0.0, false], ["yes", true], [["yes"], false]], + ["no", [null,true], [false,true], [0, false], [0.0, false], ["no", true], [["no"], false]], + ["true", [null,true], [true, true], [0, false], [0.0, false], ["true", true], [["true"], false]], + ["false", [null,true], [false,true], [0, false], [0.0, false], ["false", true], [["false"], false]], + [INF, [null,true], [true, false], [0, false], [INF, true], ["INF", false], [[INF], false]], + [-INF, [null,true], [true, false], [0, false], [-INF, true], ["-INF", false], [[-INF], false]], + [NAN, [null,true], [false,false], [0, false], [NAN, true], ["NAN", false], [[], false]], + [[], [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], true] ], + ["some string", [null,true], [true, false], [0, false], [0.0, false], ["some string", true], [["some string"], false]], + [" ", [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[" "], false]], + [new \StdClass, [null,true], [true, false], [0, false], [0.0, false], ["", false], [[new \StdClass], false]], + [new StrClass(""), [null,true], [false,true], [0, false], [0.0, false], ["", true], [[new StrClass("")], false]], + [new StrClass("1"), [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[new StrClass("1")], false]], + [new StrClass("0"), [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[new StrClass("0")], false]], + [new StrClass("-1"), [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[new StrClass("-1")], false]], + [new StrClass("Msg"), [null,true], [true, false], [0, false], [0.0, false], ["Msg", true], [[new StrClass("Msg")], false]], + [new StrClass(" "), [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[new StrClass(" ")], false]], + [2.5, [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [[2.5], false]], + [0.5, [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [[0.5], false]], + ["2.5", [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [["2.5"], false]], + ["0.5", [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [["0.5"], false]], + [$this->d("2010-01-01T00:00:00",0,0), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00",0,0)],false]], + [$this->d("2010-01-01T00:00:00",0,1), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00",0,1)],false]], + [$this->d("2010-01-01T00:00:00",1,0), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00",1,0)],false]], + [$this->d("2010-01-01T00:00:00",1,1), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00",1,1)],false]], + [1e14, [null,true], [true, false], [100000000000000,true], [1e14, true], ["100000000000000", true], [[1e14], false]], + [1e-6, [null,true], [true, false], [0, false], [1e-6, true], ["0.000001", true], [[1e-6], false]], + [[1,2,3], [null,true], [true, false], [0, false], [0.0, false], ["", false], [[1,2,3], true] ], + [['a'=>1,'b'=>2], [null,true], [true, false], [0, false], [0.0, false], ["", false], [['a'=>1,'b'=>2], true] ], + [new Test\Result([['a'=>1,'b'=>2]]), [null,true], [true, false], [0, false], [0.0, false], ["", false], [[['a'=>1,'b'=>2]], true] ], + ]; + $params = [ + [I::T_MIXED, "Mixed" ], + [I::T_NULL, "Null", ], + [I::T_BOOL, "Boolean", ], + [I::T_INT, "Integer", ], + [I::T_FLOAT, "Floating point"], + [I::T_STRING, "String", ], + [I::T_ARRAY, "Array", ], + ]; + foreach ($params as $index => $param) { + list($type, $name) = $param; + $this->assertNull(I::normalize(null, $type | I::M_STRICT | I::M_NULL), $name." null-passthrough test failed"); + foreach ($tests as $test) { + list($exp, $pass) = $index ? $test[$index] : [$test[$index], true]; + $value = $test[0]; + $assert = (is_float($exp) && is_nan($exp) ? "assertNan" : (is_scalar($exp) ? "assertSame" : "assertEquals")); + $this->$assert($exp, I::normalize($value, $type), $name." test failed for value: ".var_export($value, true)); + if ($pass) { + $this->$assert($exp, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); + $this->$assert($exp, I::normalize($value, $type | I::M_STRICT), $name." error test failed for value: ".var_export($value, true)); + } else { + $this->assertNull(I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); + $exc = new ExceptionType("strictFailure", $type); + try { + $act = I::normalize($value, $type | I::M_STRICT); + } catch (ExceptionType $e) { + $act = $e; + } finally { + $this->assertEquals($exc, $act, $name." error test failed for value: ".var_export($value, true)); + } + } + } + } + // DateTimeInterface tests + $tests = [ + /* Input value microtime iso8601 iso8601m http sql date time unix float '!M j, Y (D)' *strtotime* (null) */ + [null, null, null, null, null, null, null, null, null, null, null, null, ], + [$this->d("2010-01-01T00:00:00",0,0), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), ], + [$this->d("2010-01-01T00:00:00",0,1), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), ], + [$this->d("2010-01-01T00:00:00",1,0), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), ], + [$this->d("2010-01-01T00:00:00",1,1), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), ], + [1262304000, $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), ], + [1262304000.123456, $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), ], + [1262304000.42, $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), $this->t(1262304000.42), ], + ["0.12345600 1262304000", $this->t(1262304000.123456), null, null, null, null, null, null, null, null, null, null, ], + ["0.42 1262304000", null, null, null, null, null, null, null, null, null, null, null, ], + ["2010-01-01T00:00:00", null, $this->t(1262304000), $this->t(1262304000), null, null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01T00:00:00Z", null, $this->t(1262304000), $this->t(1262304000), null, null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01T00:00:00+0000", null, $this->t(1262304000), $this->t(1262304000), null, null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01T00:00:00-0000", null, $this->t(1262304000), $this->t(1262304000), null, null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01T00:00:00+00:00", null, $this->t(1262304000), $this->t(1262304000), null, null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01T00:00:00-05:00", null, $this->t(1262322000), $this->t(1262322000), null, null, null, null, null, null, null, $this->t(1262322000), ], + ["2010-01-01T00:00:00.123456Z", null, null, $this->t(1262304000.123456), null, null, null, null, null, null, null, $this->t(1262304000.123456), ], + ["Fri, 01 Jan 2010 00:00:00 GMT", null, null, null, $this->t(1262304000), null, null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01 00:00:00", null, null, null, null, $this->t(1262304000), null, null, null, null, null, $this->t(1262304000), ], + ["2010-01-01", null, null, null, null, null, $this->t(1262304000), null, null, null, null, $this->t(1262304000), ], + ["12:34:56", null, null, null, null, null, null, $this->t(45296), null, null, null, $this->t(strtotime("today")+45296), ], + ["1262304000", null, null, null, null, null, null, null, $this->t(1262304000), null, null, null, ], + ["1262304000.123456", null, null, null, null, null, null, null, null, $this->t(1262304000.123456), null, null, ], + ["1262304000.42", null, null, null, null, null, null, null, null, $this->t(1262304000.42), null, null, ], + ["Jan 1, 2010 (Fri)", null, null, null, null, null, null, null, null, null, $this->t(1262304000), null, ], + ["First day of Jan 2010 12AM", null, null, null, null, null, null, null, null, null, null, $this->t(1262304000), ], + ]; + $formats = [ + "microtime", + "iso8601", + "iso8601m", + "http", + "sql", + "date", + "time", + "unix", + "float", + "!M j, Y (D)", + null, + ]; + $exc = new ExceptionType("strictFailure", I::T_DATE); + foreach ($formats as $index => $format) { + foreach ($tests as $test) { + $value = $test[0]; + $exp = $test[$index+1]; + $this->assertEquals($exp, I::normalize($value, I::T_DATE, $format), "Test failed for format ".var_export($format, true)." using value ".var_export($value, true)); + $this->assertEquals($exp, I::normalize($value, I::T_DATE | I::M_DROP, $format), "Drop test failed for format ".var_export($format, true)." using value ".var_export($value, true)); + // test for exception in case of errors + $exp = $exp ?? $exc; + try { + $act = I::normalize($value, I::T_DATE | I::M_STRICT, $format); + } catch (ExceptionType $e) { + $act = $e; + } finally { + $this->assertEquals($exp, $act, "Error test failed for format ".var_export($format, true)." using value ".var_export($value, true)); + } + } + } + // Array-mode tests + $tests = [ + [I::T_INT | I::M_DROP, new Test\Result([1, 2, 2.2, 3]), [1,2,null,3] ], + [I::T_INT, new Test\Result([1, 2, 2.2, 3]), [1,2,2,3] ], + [I::T_STRING | I::M_STRICT, "Bare string", ["Bare string"]], + ]; + foreach ($tests as $index => $test) { + list($type, $value, $exp) = $test; + $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index"); + } + } + + protected function d($spec, $local, $immutable): \DateTimeInterface { + $tz = $local ? new \DateTimeZone("America/Toronto") : new \DateTimeZone("UTC"); + if ($immutable) { + return \DateTimeImmutable::createFromFormat("!Y-m-d\TH:i:s", $spec, $tz); + } else { + return \DateTime::createFromFormat("!Y-m-d\TH:i:s", $spec, $tz); + } + } + + protected function t(float $spec): \DateTime { + return \DateTime::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); + } } diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index 813ef9dd..53aaba36 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -388,6 +388,7 @@ class TestNCNV1_2 extends Test\AbstractTest { ['id' => 2, 'name' => "Hardware", 'parent' => null], ]; // set of various mocks for testing + Phake::when(Arsse::$db)->folderAdd($this->anything(), $this->anything())->thenThrow(new \Exception); Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $in[0])->thenReturn(1)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $in[1])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 1)->thenReturn($out[0]); @@ -499,6 +500,7 @@ class TestNCNV1_2 extends Test\AbstractTest { // set up the necessary mocks Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "")->thenThrow(new \JKingWeb\Arsse\Feed\Exception("", new \PicoFeed\Reader\SubscriptionNotFoundException)); Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2112)->thenReturn($this->feeds['db'][0]); Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->feeds['db'][1]); Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 47)->thenReturn($this->feeds['db'][2]); @@ -584,7 +586,7 @@ class TestNCNV1_2 extends Test\AbstractTest { Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, $this->identicalTo(['title' => ""]))->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, $this->identicalTo(['title' => false]))->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); - $exp = new Response(204); + $exp = new Response(422); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[0]), 'application/json'))); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[1]), 'application/json'))); @@ -628,7 +630,7 @@ class TestNCNV1_2 extends Test\AbstractTest { ]; Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true); Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing")); - Phake::when(Arsse::$db)->feedUpdate(-1)->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->feedUpdate($this->lessThan(1))->thenThrow(new ExceptionInput("typeViolation")); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); $exp = new Response(404); @@ -788,7 +790,7 @@ class TestNCNV1_2 extends Test\AbstractTest { Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->editions($in[1]))->thenThrow(new ExceptionInput("tooLong")); // data model function limited to 50 items for multiples Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->articles([]))->thenThrow(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->articles($in[1]))->thenThrow(new ExceptionInput("tooLong")); // data model function limited to 50 items for multiples - $exp = new Response(422); + $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple"))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple"))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple"))); @@ -797,14 +799,12 @@ class TestNCNV1_2 extends Test\AbstractTest { $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => "ook"]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => "ook"]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => "ook"]), 'application/json'))); - $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => []]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => []]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[0]]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[0]]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[1]]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[1]]), 'application/json'))); - $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => []]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => []]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[0]]), 'application/json'))); @@ -812,13 +812,13 @@ class TestNCNV1_2 extends Test\AbstractTest { $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); // ensure the data model was queried appropriately for read/unread - Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->editions([])); - Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[0])); + Phake::verify(Arsse::$db, Phake::times(2))->articleMark(Arsse::$user->id, $read, (new Context)->editions([])); + Phake::verify(Arsse::$db, Phake::times(2))->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[0])); Phake::verify(Arsse::$db, Phake::times(0))->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[1])); Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[2])); Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[3])); - Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unread, (new Context)->editions([])); - Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unread, (new Context)->editions($in[0])); + Phake::verify(Arsse::$db, Phake::times(2))->articleMark(Arsse::$user->id, $unread, (new Context)->editions([])); + Phake::verify(Arsse::$db, Phake::times(2))->articleMark(Arsse::$user->id, $unread, (new Context)->editions($in[0])); Phake::verify(Arsse::$db, Phake::times(0))->articleMark(Arsse::$user->id, $unread, (new Context)->editions($in[1])); Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unread, (new Context)->editions($in[2])); Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unread, (new Context)->editions($in[3]));