1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Merge changes from master

This commit is contained in:
J. King 2017-10-19 22:58:42 -04:00
parent 4e3369cd03
commit 8c6c49d588
10 changed files with 670 additions and 296 deletions

190
composer.lock generated
View file

@ -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",

View file

@ -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,

6
lib/ExceptionType.php Normal file
View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
class ExceptionType extends AbstractException {
}

View file

@ -3,6 +3,18 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Misc;
class Date {
const FORMAT = [ // in out
'iso8601' => ["!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)) {
@ -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 {

View file

@ -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)) {
}
// 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) $value;
$value = (int) (!strpos($value, "e") ? $value : $float);
} else {
$out += self::FLOAT;
$value = $float;
}
} else {
return $out;
}
} elseif (is_float($value) && !fmod($value, 1)) {
} 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
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;
}

View file

@ -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)) {
$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;
foreach ($types as $key => $type) {
if (isset($data[$key])) {
$out[$key] = ValueInfo::normalize($data[$key], $type | $mode, $dateFormat);
} 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]);
$out[$key] = null;
}
}
return $out;

View file

@ -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;

View file

@ -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}"',

View file

@ -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"));
}
}

View file

@ -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]));