From a81b28e29c58c9e40a14f3d52a1a06d3a53186b1 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 10 Jul 2025 12:28:16 -0400 Subject: [PATCH 01/28] [test] Upgrade PHPUnit to the latest release --- composer.json | 2 +- composer.lock | 619 +++++++++++++++++++++++++++++--------------------- 2 files changed, 359 insertions(+), 262 deletions(-) diff --git a/composer.json b/composer.json index 841af8656b4..16a49d7b354 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ }, "require-dev" : { "squizlabs/php_codesniffer" : "^3.5", - "phpunit/phpunit" : "12.0", + "phpunit/phpunit" : "^12.2", "phan/phan": "^5.0", "phpstan/phpstan": "^1.4", "slevomat/coding-standard": "^6.4", diff --git a/composer.lock b/composer.lock index cb72c4768a5..307294b0aec 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "962186512cf6f4ef19d97b2738569257", + "content-hash": "b2d623d7391c8e620aff8c05988950f1", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.337.3", + "version": "3.359.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "06dfc8f76423b49aaa181debd25bbdc724c346d6" + "reference": "40543e3993fc5094094ac9f9bdc4434bf81cca2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/06dfc8f76423b49aaa181debd25bbdc724c346d6", - "reference": "06dfc8f76423b49aaa181debd25bbdc724c346d6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/40543e3993fc5094094ac9f9bdc4434bf81cca2d", + "reference": "40543e3993fc5094094ac9f9bdc4434bf81cca2d", "shasum": "" }, "require": { @@ -79,31 +79,30 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", - "composer/composer": "^1.10.22", + "composer/composer": "^2.7.8", "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-pcntl": "*", "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -152,24 +151,24 @@ "sdk" ], "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.337.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.359.1" }, - "time": "2025-01-21T19:10:05+00:00" + "time": "2025-10-29T20:13:06+00:00" }, { "name": "bjeavons/zxcvbn-php", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/bjeavons/zxcvbn-php.git", - "reference": "603e015f2c81118a8f42930140311d125eba6f8a" + "reference": "426f664501a0747beb8f3ee17ac30c7dd6327ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bjeavons/zxcvbn-php/zipball/603e015f2c81118a8f42930140311d125eba6f8a", - "reference": "603e015f2c81118a8f42930140311d125eba6f8a", + "url": "https://api.github.com/repos/bjeavons/zxcvbn-php/zipball/426f664501a0747beb8f3ee17ac30c7dd6327ffa", + "reference": "426f664501a0747beb8f3ee17ac30c7dd6327ffa", "shasum": "" }, "require": { @@ -210,22 +209,22 @@ ], "support": { "issues": "https://github.com/bjeavons/zxcvbn-php/issues", - "source": "https://github.com/bjeavons/zxcvbn-php/tree/1.4.1" + "source": "https://github.com/bjeavons/zxcvbn-php/tree/1.4.2" }, - "time": "2024-11-21T22:10:41+00:00" + "time": "2025-02-24T16:47:20+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.11.0", + "version": "v6.11.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "shasum": "" }, "require": { @@ -273,22 +272,22 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" }, - "time": "2025-01-23T05:11:06+00:00" + "time": "2025-04-09T20:32:01+00:00" }, { "name": "google/recaptcha", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/google/recaptcha.git", - "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df" + "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/google/recaptcha/zipball/d59a801e98a4e9174814a6d71bbc268dff1202df", - "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df", + "url": "https://api.github.com/repos/google/recaptcha/zipball/56522c261d2e8c58ba416c90f81a4cd9f2ed89b9", + "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9", "shasum": "" }, "require": { @@ -327,26 +326,26 @@ "issues": "https://github.com/google/recaptcha/issues", "source": "https://github.com/google/recaptcha" }, - "time": "2023-02-18T17:41:46+00:00" + "time": "2025-06-26T22:21:57+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -437,7 +436,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -453,20 +452,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -474,7 +473,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -520,7 +519,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -536,20 +535,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -565,7 +564,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -636,7 +635,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -652,24 +651,24 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "laminas/laminas-diactoros", - "version": "3.5.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" + "reference": "60c182916b2749480895601649563970f3f12ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2", - "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/http-factory": "^1.1", "psr/http-message": "^1.1 || ^2.0" }, @@ -686,11 +685,11 @@ "ext-gd": "*", "ext-libxml": "*", "http-interop/http-factory-tests": "^2.2.0", - "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-coding-standard": "~3.1.0", "php-http/psr7-integration-tests": "^1.4.0", "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" }, "type": "library", "extra": { @@ -740,7 +739,7 @@ "type": "community_bridge" } ], - "time": "2024-10-14T11:59:49+00:00" + "time": "2025-10-12T15:31:36+00:00" }, { "name": "mtdowling/jmespath.php", @@ -1345,16 +1344,16 @@ }, { "name": "smarty/smarty", - "version": "v4.5.5", + "version": "v4.5.6", "source": { "type": "git", "url": "https://github.com/smarty-php/smarty.git", - "reference": "c4851c12e34ff80073ddeb7d98b059d57dea9de2" + "reference": "a8d77c86660ca0562ec2fb781fbbda737fb7a62b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smarty-php/smarty/zipball/c4851c12e34ff80073ddeb7d98b059d57dea9de2", - "reference": "c4851c12e34ff80073ddeb7d98b059d57dea9de2", + "url": "https://api.github.com/repos/smarty-php/smarty/zipball/a8d77c86660ca0562ec2fb781fbbda737fb7a62b", + "reference": "a8d77c86660ca0562ec2fb781fbbda737fb7a62b", "shasum": "" }, "require": { @@ -1405,22 +1404,28 @@ "support": { "forum": "https://github.com/smarty-php/smarty/discussions", "issues": "https://github.com/smarty-php/smarty/issues", - "source": "https://github.com/smarty-php/smarty/tree/v4.5.5" + "source": "https://github.com/smarty-php/smarty/tree/v4.5.6" }, - "time": "2024-11-21T22:06:22+00:00" + "funding": [ + { + "url": "https://github.com/wisskid", + "type": "github" + } + ], + "time": "2025-08-26T08:37:46+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1433,7 +1438,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1458,7 +1463,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1474,23 +1479,24 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1538,7 +1544,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -1549,12 +1555,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" } ], "packages-dev": [ @@ -1639,16 +1649,16 @@ }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -1700,7 +1710,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -1710,13 +1720,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", @@ -2006,16 +2012,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -2054,7 +2060,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -2062,7 +2068,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "netresearch/jsonmapper", @@ -2117,16 +2123,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -2145,7 +2151,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -2169,22 +2175,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phan/phan", - "version": "5.4.5", + "version": "5.5.2", "source": { "type": "git", "url": "https://github.com/phan/phan.git", - "reference": "2b15302175931a0629a85c57d0c1f91d68b26a4d" + "reference": "25d7e8d185a4c78e7423c188fb28bba5dbde20c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phan/phan/zipball/2b15302175931a0629a85c57d0c1f91d68b26a4d", - "reference": "2b15302175931a0629a85c57d0c1f91d68b26a4d", + "url": "https://api.github.com/repos/phan/phan/zipball/25d7e8d185a4c78e7423c188fb28bba5dbde20c1", + "reference": "25d7e8d185a4c78e7423c188fb28bba5dbde20c1", "shasum": "" }, "require": { @@ -2195,7 +2201,7 @@ "ext-tokenizer": "*", "felixfbecker/advanced-json-rpc": "^3.0.4", "microsoft/tolerant-php-parser": "0.1.2", - "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0", + "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0|^5.0", "php": "^7.2.0|^8.0.0", "sabre/event": "^5.1.3", "symfony/console": "^3.2|^4.0|^5.0|^6.0|^7.0", @@ -2249,9 +2255,9 @@ ], "support": { "issues": "https://github.com/phan/phan/issues", - "source": "https://github.com/phan/phan/tree/5.4.5" + "source": "https://github.com/phan/phan/tree/5.5.2" }, - "time": "2024-08-13T21:41:35+00:00" + "time": "2025-10-04T18:04:38+00:00" }, { "name": "phar-io/manifest", @@ -2377,12 +2383,12 @@ "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" + "reference": "898f0be8267680fbf962e371ea39b7f7f6411bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/898f0be8267680fbf962e371ea39b7f7f6411bdb", + "reference": "898f0be8267680fbf962e371ea39b7f7f6411bdb", "shasum": "" }, "require": { @@ -2434,9 +2440,9 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + "source": "https://github.com/php-webdriver/php-webdriver/tree/main" }, - "time": "2024-11-21T15:12:59+00:00" + "time": "2025-02-24T19:21:58+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2658,16 +2664,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.16", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e0bb5cb78545aae631220735aa706eac633a6be9", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -2712,38 +2713,38 @@ "type": "github" } ], - "time": "2025-01-21T14:50:05+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.3.1", + "version": "12.4.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170" + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ddec29dfc128eba9c204389960f2063f3b7fa170", - "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.6.1", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0", + "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^12.1" + "phpunit/phpunit": "^12.3.7" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -2752,7 +2753,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.3.x-dev" + "dev-main": "12.4.x-dev" } }, "autoload": { @@ -2781,7 +2782,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" }, "funding": [ { @@ -2801,7 +2802,7 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:58:13+00:00" + "time": "2025-09-24T13:44:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3050,16 +3051,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.0.0", + "version": "12.4.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9912c83b5207ab3730fcadc42992e95bdb02dad8" + "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9912c83b5207ab3730fcadc42992e95bdb02dad8", - "reference": "9912c83b5207ab3730fcadc42992e95bdb02dad8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a94ea4d26d865875803b23aaf78c3c2c670ea2ea", + "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea", "shasum": "" }, "require": { @@ -3069,23 +3070,23 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.0.0", + "phpunit/php-code-coverage": "^12.4.0", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.0.0", - "sebastian/comparator": "^7.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.0", - "sebastian/exporter": "^7.0.0", - "sebastian/global-state": "^8.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", - "sebastian/type": "^6.0.0", + "sebastian/type": "^6.0.3", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, @@ -3095,7 +3096,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.0-dev" + "dev-main": "12.4-dev" } }, "autoload": { @@ -3127,7 +3128,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.0" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.2" }, "funding": [ { @@ -3138,12 +3139,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-02-07T05:03:33+00:00" + "time": "2025-10-30T08:41:39+00:00" }, { "name": "psr/container", @@ -3266,16 +3275,16 @@ }, { "name": "sebastian/cli-parser", - "version": "4.0.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", - "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { @@ -3287,7 +3296,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -3311,28 +3320,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2025-02-07T04:53:50+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.0", + "version": "7.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f" + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/03d905327dccc0851c9a08d6a979dfc683826b6f", - "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", "shasum": "" }, "require": { @@ -3391,7 +3412,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" }, "funding": [ { @@ -3411,7 +3432,7 @@ "type": "tidelift" } ], - "time": "2025-06-17T07:41:58+00:00" + "time": "2025-08-20T11:27:00+00:00" }, { "name": "sebastian/complexity", @@ -3540,16 +3561,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.2", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792" + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792", - "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", "shasum": "" }, "require": { @@ -3592,7 +3613,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.2" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "funding": [ { @@ -3612,20 +3633,20 @@ "type": "tidelift" } ], - "time": "2025-05-21T15:05:44+00:00" + "time": "2025-08-12T14:11:56+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.0", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { @@ -3682,28 +3703,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:42+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.0", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { @@ -3744,15 +3777,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:59+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", @@ -3928,16 +3973,16 @@ }, { "name": "sebastian/recursion-context", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { @@ -3980,28 +4025,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:01+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", - "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { @@ -4037,15 +4094,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2025-03-18T13:37:31+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", @@ -4164,16 +4233,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.3", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", - "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -4240,11 +4309,11 @@ "type": "open_collective" }, { - "url": "https://thanks.dev/phpcsstandards", + "url": "https://thanks.dev/u/gh/phpcsstandards", "type": "thanks_dev" } ], - "time": "2025-01-23T17:04:15+00:00" + "time": "2025-09-05T05:47:09+00:00" }, { "name": "staabm/side-effects-detector", @@ -4300,23 +4369,24 @@ }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4373,7 +4443,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.3.5" }, "funding": [ { @@ -4384,16 +4454,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2025-10-14T15:46:26+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4452,7 +4526,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -4463,6 +4537,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4472,16 +4550,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -4530,7 +4608,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -4541,16 +4619,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4611,7 +4693,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -4622,6 +4704,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4631,16 +4717,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -4691,7 +4777,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -4702,25 +4788,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -4752,7 +4842,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -4763,25 +4853,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -4799,7 +4893,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4835,7 +4929,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -4851,20 +4945,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -4879,7 +4973,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -4922,7 +5015,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -4933,12 +5026,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "theseer/tokenizer", @@ -5054,28 +5151,28 @@ }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -5106,9 +5203,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], @@ -5121,6 +5218,6 @@ "platform": { "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } From 8e437395072313822a667c88a8d8b1e0c154563c Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 10 Jul 2025 16:16:44 -0400 Subject: [PATCH 02/28] Add classes for generating TOTP and HOTP codes --- src/Security/OTP/HOTP.php | 50 ++++++++++ src/Security/OTP/TOTP.php | 18 ++++ test/unittests/security/HOTP_Test.php | 126 ++++++++++++++++++++++++++ test/unittests/security/TOTP_Test.php | 119 ++++++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 src/Security/OTP/HOTP.php create mode 100644 src/Security/OTP/TOTP.php create mode 100644 test/unittests/security/HOTP_Test.php create mode 100644 test/unittests/security/TOTP_Test.php diff --git a/src/Security/OTP/HOTP.php b/src/Security/OTP/HOTP.php new file mode 100644 index 00000000000..918afc6b32e --- /dev/null +++ b/src/Security/OTP/HOTP.php @@ -0,0 +1,50 @@ +algorithm, $counter, $this->secret); + } + + public function getTruncatedDecimal(int $counter) : int { + $hash = $this->getHash($counter); + // sha1 is 20 bytes (40 chars as hex) but other algorithms + // can be longer for TOTP. + $hashlen = strlen($hash); + assert($hashlen >= 40); + // RFC4226 is ambiguous about whether it should be the last + // nibble of the hash or the last nibble of byte 20 because + // it only deals with SHA1 (20 bytes long). RFC6238's tests + // that use SHA256 and SHA512 only work with the last nibble + // of the hash, hash-size dependent, so we use that interpretation. + $offset = hexdec($hash[$hashlen-1]); + + // Convert from a byte offset to an offset into the string by + // multiplying by 2, and then take next 4 bytes (or 8 characters + // when encoded as a hex string) + $truncated = substr($hash, $offset*2, 8); + + $decimal = hexdec($truncated); + + // Clear the top bit as per RFC4226 + $nosign = $decimal & ~(1<<31); + + return $nosign; + } + + public function getCode(int $counter, int $len) : string { + $dec = $this->getTruncatedDecimal($counter); + return str_pad(strval($dec % pow(10, $len)), $len, "0", STR_PAD_LEFT); + } +} diff --git a/src/Security/OTP/TOTP.php b/src/Security/OTP/TOTP.php new file mode 100644 index 00000000000..4cb3e68f651 --- /dev/null +++ b/src/Security/OTP/TOTP.php @@ -0,0 +1,18 @@ +getTimestamp(); + $count = (int )floor($ut / $this->timestep); + return $count; + } +} diff --git a/test/unittests/security/HOTP_Test.php b/test/unittests/security/HOTP_Test.php new file mode 100644 index 00000000000..1da37f40537 --- /dev/null +++ b/test/unittests/security/HOTP_Test.php @@ -0,0 +1,126 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace LORIS\Security; + +require_once __DIR__ . '/../../../vendor/autoload.php'; + +use \PHPUnit\Framework\TestCase; +use \LORIS\Security\OTP\HOTP; + +/** + * Unit test class for the CandID value object + * + * PHP Version 7 + * + * @category Tests + * @package StudyEntities + * @author Xavier Lecours + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class HOTP_Test extends TestCase +{ + + function testGetCounter() : void + { + $validValues = [ + // Values used by the RFC4226 tests + 0 => hex2bin('0000000000000000'), + 1 => hex2bin('0000000000000001'), + 2 => hex2bin('0000000000000002'), + 3 => hex2bin('0000000000000003'), + 4 => hex2bin('0000000000000004'), + 5 => hex2bin('0000000000000005'), + 6 => hex2bin('0000000000000006'), + 7 => hex2bin('0000000000000007'), + 8 => hex2bin('0000000000000008'), + 9 => hex2bin('0000000000000009'), + + // not used by RFC4226, but test non-decimal + 10 => hex2bin('000000000000000a'), + // test padding > 1 byte should be + // big endian + 256 => hex2bin('0000000000000100') + ]; + foreach($validValues as $i => $padded) { + $this->assertEquals(HOTP::getPaddedCounter($i), $padded); + } + } + + function testRFC4226GetHash() : void + { + // Secret and algorithm for RFC4226 test suite + $hotp = new HOTP("12345678901234567890", "sha1"); + $validValues = [ + 0 => "cc93cf18508d94934c64b65d8ba7667fb7cde4b0", + 1 => "75a48a19d4cbe100644e8ac1397eea747a2d33ab", + 2 => "0bacb7fa082fef30782211938bc1c5e70416ff44", + 3 => "66c28227d03a2d5529262ff016a1e6ef76557ece", + 4 => "a904c900a64b35909874b33e61c5938a8e15ed1c", + 5 => "a37e783d7b7233c083d4f62926c7a25f238d0316", + 6 => "bc9cd28561042c83f219324d3c607256c03272ae", + 7 => "a4fb960c0bc06e1eabb804e5b397cdc4b45596fa", + 8 => "1b3c89f65e6c9e883012052823443f048b4332db", + 9 => "1637409809a679dc698207310c8c7fc07290d9e5" + ]; + foreach($validValues as $i => $hashstr) { + $this->assertEquals($hotp->getHash($i), $hashstr); + } + + } + + function testRFC4226Truncation() : void + { + // Secret and algorithm for RFC4226 test suite + $hotp = new HOTP("12345678901234567890", "sha1"); + $validValues = [ + 0 => 1284755224, + 1 => 1094287082, + 2 => 137359152, + 3 => 1726969429, + 4 => 1640338314, + 5 => 868254676, + 6 => 1918287922, + 7 => 82162583, + 8 => 673399871, + 9 => 645520489 + ]; + foreach($validValues as $i => $decval) { + $this->assertEquals($hotp->getTruncatedDecimal($i), $decval); + } + + } + + function testRFC4226Code() : void + { + // Secret and algorithm for RFC4226 test suite + $hotp = new HOTP("12345678901234567890", "sha1"); + $validValues = [ + 0 => "755224", + 1 => "287082", + 2 => "359152", + 3 => "969429", + 4 => "338314", + 5 => "254676", + 6 => "287922", + 7 => "162583", + 8 => "399871", + 9 => "520489" + ]; + foreach($validValues as $i => $code) { + $this->assertEquals($hotp->getCode($i, 6), $code); + } + + } +} diff --git a/test/unittests/security/TOTP_Test.php b/test/unittests/security/TOTP_Test.php new file mode 100644 index 00000000000..f491daba32c --- /dev/null +++ b/test/unittests/security/TOTP_Test.php @@ -0,0 +1,119 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace LORIS\Security; + +require_once __DIR__ . '/../../../vendor/autoload.php'; + +use \PHPUnit\Framework\TestCase; +use \LORIS\Security\OTP\TOTP; + +/** + * Unit test class for the CandID value object + * + * PHP Version 7 + * + * @category Tests + * @package StudyEntities + * @author Xavier Lecours + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class TOTP_Test extends TestCase +{ + + function testRFC6238Counters() : void + { + $totp = new TOTP("abc", timestep: 30); + // Unix time => RFC6238 time based counter for HOTP + $validValues = [ + 59 => hexdec("0000000000000001"), + 1111111109 => hexdec("00000000023523EC"), + 1111111111 => hexdec("00000000023523ED"), + 1234567890 => hexdec("000000000273EF07"), + 2000000000 => hexdec("0000000003F940AA"), + 20000000000 => hexdec("0000000027BC86AA"), + ]; + foreach($validValues as $i => $counter) { + $this->assertEquals($totp->getTimeCounter(\DateTimeImmutable::createFromFormat("U", strval($i))), $counter); + } + } + + public function testRFC6238Sha1Codes() : void { + + $totp = new TOTP("12345678901234567890", timestep: 30, algorithm: 'sha1'); + $validValues = [ + 59 => "94287082", + 1111111109 => "07081804", + 1111111111 => "14050471", + 1234567890 => "89005924", + 2000000000 => "69279037", + 20000000000 => "65353130" + ]; + + foreach($validValues as $i => $code) { + $counter = $totp->getTimeCounter(\DateTimeImmutable::createFromFormat("U", strval($i))); + var_dump($counter); + $this->assertEquals($totp->getCode($counter, 8), $code); + } + } + + public function testRFC6238Sha256Codes() : void { + // RFC6238's test values in Appendix B say the shared secret is + // 12345678901234567890, but the reference implementation in + // Appendix A uses different hash-size dependent shared keys. The + // test vectors only work when we use the secrets from the reference + // implementation. + $totp = new TOTP("12345678901234567890123456789012", timestep: 30, algorithm: 'sha256'); + $validValues = [ + 59 => "46119246", + 1111111109 => "68084774", + 1111111111 => "67062674", + 1234567890 => "91819424", + 2000000000 => "90698825", + 20000000000 => "77737706" + ]; + + foreach($validValues as $i => $code) { + $counter = $totp->getTimeCounter(\DateTimeImmutable::createFromFormat("U", strval($i))); + var_dump($counter); + $this->assertEquals($totp->getCode($counter, 8), $code); + } + } + + public function testRFC6238Sha512Codes() : void { + // RFC6238's test values in Appendix B say the shared secret is + // 12345678901234567890, but the reference implementation in + // Appendix A uses different hash-size dependent shared keys. The + // test vectors only work when we use the secrets from the reference + // implementation. + $totp = new TOTP("12345678901234567890". + "12345678901234567890" + ."123456789012345678901234" + , timestep: 30, algorithm: 'sha512'); + $validValues = [ + 59 => "90693936", + 1111111109 => "25091201", + 1111111111 => "99943326", + 1234567890 => "93441116", + 2000000000 => "38618901", + 20000000000 => "47863826" + ]; + + foreach($validValues as $i => $code) { + $counter = $totp->getTimeCounter(\DateTimeImmutable::createFromFormat("U", strval($i))); + var_dump($counter); + $this->assertEquals($totp->getCode($counter, 8), $code); + } + } +} From afb47fa3e94842ad06a9898ad4630d0015949277 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 24 Jul 2025 08:42:06 -0400 Subject: [PATCH 03/28] WIP -- add middleware to require MFA token --- htdocs/index.php | 1 + .../php/my_preferences.class.inc | 3 + .../templates/form_my_preferences.tpl | 7 ++ php/libraries/NDB_Client.class.inc | 3 + php/libraries/SinglePointLogin.class.inc | 9 +++ php/libraries/UserPermissions.class.inc | 4 ++ src/Middleware/MFA.php | 57 +++++++++++++++ test/phpunit.xml | 71 +++++++++++++++++++ 8 files changed, 155 insertions(+) create mode 100644 src/Middleware/MFA.php diff --git a/htdocs/index.php b/htdocs/index.php index a11bd96d962..a0b1fa8d3ab 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -77,6 +77,7 @@ function array_find(array $array, callable $callback) ->withMiddleware(new \LORIS\Middleware\ContentLength()) ->withMiddleware(new \LORIS\Middleware\AWS()) ->withMiddleware(new \LORIS\Middleware\ContentSecurityPolicy()) + ->withMiddleware(new \LORIS\Middleware\MFA()) ->withMiddleware(new \LORIS\Middleware\ResponseGenerator()); $serverrequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals(); diff --git a/modules/my_preferences/php/my_preferences.class.inc b/modules/my_preferences/php/my_preferences.class.inc index f1ac6509250..21981788431 100644 --- a/modules/my_preferences/php/my_preferences.class.inc +++ b/modules/my_preferences/php/my_preferences.class.inc @@ -384,6 +384,9 @@ class My_Preferences extends \NDB_Form unset($nGroup); } } + $this->tpl_data['mfa_secret'] = $user->getMFASecret(); + $config = \NDB_Factory::singleton()->config(); + $this->tpl_data['study_title'] = $config->getSetting("title"); $this->tpl_data['notification_rows'] = $notification_rows; //------------------------------------------------------------ diff --git a/modules/my_preferences/templates/form_my_preferences.tpl b/modules/my_preferences/templates/form_my_preferences.tpl index 437bf525ba9..ff9c2de6b31 100644 --- a/modules/my_preferences/templates/form_my_preferences.tpl +++ b/modules/my_preferences/templates/form_my_preferences.tpl @@ -63,6 +63,13 @@ + {if $mfa_secret == ""} +
+ +
+ {else} +otpauth://totp/{urlencode($study_title)}:{urlencode($form.UserID.html)}?secret={$mfa_secret}&period=30&digits=6&issuer={urlencode($study_title)} + {/if}
- {if $mfa_secret == ""} +
- +
- {else} -otpauth://totp/{urlencode($study_title)}:{urlencode($form.UserID.html)}?secret={$mfa_secret}&period=30&digits=6&issuer={urlencode($study_title)} - {/if} +
+ ); } /** * diff --git a/modules/my_preferences/php/mfa.class.inc b/modules/my_preferences/php/mfa.class.inc index 0690274f59f..5707e441ba5 100644 --- a/modules/my_preferences/php/mfa.class.inc +++ b/modules/my_preferences/php/mfa.class.inc @@ -94,7 +94,7 @@ class MFA extends \NDB_Page $wantCode = $validator->getCode($counter, 6); if ($wantCode !== strval($values['code'])) { return new \LORIS\Http\Response\JSON\BadRequest( - 'Code does not match expected value' + 'Invalid code provided. MFA not registered.' ); } $db = $this->loris->getDatabaseConnection(); From d1c3b8a419d4f0078bb5164988ce02860e5d6d3c Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Tue, 26 Aug 2025 14:07:20 -0400 Subject: [PATCH 13/28] Fix space between words --- modules/my_preferences/jsx/mfa.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index c9c3424355b..1e2e66ed240 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -86,8 +86,8 @@ function MFAIndex(): React.ReactElement {

Scan the following QR code below in your MFA authenticator and enter the code to validate.

- Note that this will overwrite - any previously setup MFA in LORIS! + Note that this will overwrite any previously + setup MFA in LORIS!

Can't scan the QR code? From 7ba70f44942d0de3584932ae14de67783ab0b515 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Tue, 26 Aug 2025 14:15:21 -0400 Subject: [PATCH 14/28] Fix comments in tests --- test/unittests/security/HOTP_Test.php | 22 +++------------------- test/unittests/security/TOTP_Test.php | 25 +++++-------------------- 2 files changed, 8 insertions(+), 39 deletions(-) diff --git a/test/unittests/security/HOTP_Test.php b/test/unittests/security/HOTP_Test.php index dd2234c1d27..505668af1ba 100644 --- a/test/unittests/security/HOTP_Test.php +++ b/test/unittests/security/HOTP_Test.php @@ -1,16 +1,5 @@ - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ namespace LORIS\Security; require_once __DIR__ . '/../../../vendor/autoload.php'; @@ -19,15 +8,10 @@ use \LORIS\Security\OTP\HOTP; /** - * Unit test class for the CandID value object - * - * PHP Version 7 + * Group of tests for HMAC-based One Time Passwords (HOTP) + * primarily based on RFC4226 test vectors. * - * @category Tests - * @package StudyEntities - * @author Xavier Lecours - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 */ class HOTP_Test extends TestCase { diff --git a/test/unittests/security/TOTP_Test.php b/test/unittests/security/TOTP_Test.php index 77cd0ffe58d..569df9f3a28 100644 --- a/test/unittests/security/TOTP_Test.php +++ b/test/unittests/security/TOTP_Test.php @@ -1,16 +1,5 @@ - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ namespace LORIS\Security; require_once __DIR__ . '/../../../vendor/autoload.php'; @@ -19,15 +8,10 @@ use \LORIS\Security\OTP\TOTP; /** - * Unit test class for the CandID value object - * - * PHP Version 7 + * Unit test class for Time-based One Time Passwords (TOTP) + * primarily based on RFC6238 * - * @category Tests - * @package StudyEntities - * @author Xavier Lecours - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 */ class TOTP_Test extends TestCase { @@ -40,7 +24,8 @@ class TOTP_Test extends TestCase function testRFC6238Counters() : void { $totp = new TOTP("abc", timestep: 30); - // Unix time => RFC6238 time based counter for HOTP + // Unix time => RFC6238 time based counter to pass to HOTP + // algorithm. $validValues = [ 59 => hexdec("0000000000000001"), 1111111109 => hexdec("00000000023523EC"), From d258537f04d4590293f9cef8038ae26724ffc720 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Wed, 27 Aug 2025 12:20:35 -0400 Subject: [PATCH 15/28] Add jefferson's suggestions Co-authored-by: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> --- jsx/MFAPrompt.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jsx/MFAPrompt.tsx b/jsx/MFAPrompt.tsx index 3c62a2dd357..03d94696fa7 100644 --- a/jsx/MFAPrompt.tsx +++ b/jsx/MFAPrompt.tsx @@ -65,18 +65,20 @@ function MFAPrompt(props: {validate: const digitCallback = useCallback( (index: number, value: number): boolean => { if (value >= 0 && value <= 9) { - code[index] = value; - setCode([...code]); - return true; + setCode(prev => { + const newCode = [...prev]; + newCode[index] = value; + return newCode; + }); } return false; }, - [code, setCode] + [] ); const errorCallback = useCallback( (msg: string) => { swal.fire('Error', msg, 'error'); setCode([null, null, null, null, null, null]); - }, [setCode]); + }, []); useEffect( () => { if (code.indexOf(null) >= 0) { return; From bf65e9e4f34e871ece9532bf1080920b52dad2b8 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Wed, 27 Aug 2025 12:28:34 -0400 Subject: [PATCH 16/28] fix compile and explicitly name acronym in link --- jsx/MFAPrompt.tsx | 22 ++++++++++--------- modules/my_preferences/jsx/mfa.tsx | 4 ++-- .../templates/form_my_preferences.tpl | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/jsx/MFAPrompt.tsx b/jsx/MFAPrompt.tsx index 03d94696fa7..f46f3d24dc2 100644 --- a/jsx/MFAPrompt.tsx +++ b/jsx/MFAPrompt.tsx @@ -43,9 +43,17 @@ function Digit(props: { } type errorCallback = (msg: string) => void; +type MFACode = [ + number|null, + number|null, + number|null, + number|null, + number|null, + number|null]; + /** - * Prompt for a multi-factor authentication code and call onValidate - * after a valid code has been entered. + * Prompt for a multi-factor authentication code and call validate + * callback to validate the code after all 6 digits have been entered. * * @param props - React props * @param props.validate - Callback when a code is entered to validate it. @@ -55,18 +63,12 @@ type errorCallback = (msg: string) => void; function MFAPrompt(props: {validate: (code: string, onError: errorCallback) => void }) { - const [code, setCode] = useState<[ - number|null, - number|null, - number|null, - number|null, - number|null, - number|null]>([null, null, null, null, null, null]); + const [code, setCode] = useState([null, null, null, null, null, null]); const digitCallback = useCallback( (index: number, value: number): boolean => { if (value >= 0 && value <= 9) { setCode(prev => { - const newCode = [...prev]; + const newCode: MFACode = [...prev]; newCode[index] = value; return newCode; }); diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 1e2e66ed240..0586375930b 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -90,8 +90,8 @@ function MFAIndex(): React.ReactElement { setup MFA in LORIS!

-

Can't scan the QR code? - setShowModal(true)}>Setup manually. +

Can't scan the QR code? setShowModal(true)}> + Setup manually.

; diff --git a/modules/my_preferences/templates/form_my_preferences.tpl b/modules/my_preferences/templates/form_my_preferences.tpl index ff2479e9b59..b93e8c8568d 100644 --- a/modules/my_preferences/templates/form_my_preferences.tpl +++ b/modules/my_preferences/templates/form_my_preferences.tpl @@ -70,7 +70,7 @@ in a different form element with a smarty template and it's easier to create a new "fresh" page with modern react/etc than rewrite the whole page or do a hybrid here *} - Configure MFA + Configure multi-factor authentication (MFA) From a6829e9cb6c860aa8a31383fd569339ba36d3fdc Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Wed, 27 Aug 2025 13:05:44 -0400 Subject: [PATCH 17/28] Fix manual code input --- jsx/MFAPrompt.tsx | 15 +++++++++------ modules/my_preferences/jsx/mfa.tsx | 2 +- test/unittests/security/TOTP_Test.php | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/jsx/MFAPrompt.tsx b/jsx/MFAPrompt.tsx index f46f3d24dc2..1d72f216df9 100644 --- a/jsx/MFAPrompt.tsx +++ b/jsx/MFAPrompt.tsx @@ -63,15 +63,18 @@ type MFACode = [ function MFAPrompt(props: {validate: (code: string, onError: errorCallback) => void }) { - const [code, setCode] = useState([null, null, null, null, null, null]); + const [code, setCode] = useState( + [null, null, null, null, null, null] + ); const digitCallback = useCallback( (index: number, value: number): boolean => { if (value >= 0 && value <= 9) { - setCode(prev => { - const newCode: MFACode = [...prev]; - newCode[index] = value; - return newCode; - }); + setCode((prev) => { + const newCode: MFACode = [...prev]; + newCode[index] = value; + return newCode; + }); + return true; } return false; }, diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 0586375930b..3217b62290d 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -68,8 +68,8 @@ function CodeValidator(props: { */ function MFAIndex(): React.ReactElement { const [showModal, setShowModal] = useState(false); + const [key] = useState(genPotentialSecret()); const studyTitle = loris.config('studyTitle'); - const key = genPotentialSecret(); const mfaUrl = 'otpauth://totp/' + encodeURI(studyTitle) + ':' + encodeURI(loris.user.username) diff --git a/test/unittests/security/TOTP_Test.php b/test/unittests/security/TOTP_Test.php index 569df9f3a28..78a4fb348eb 100644 --- a/test/unittests/security/TOTP_Test.php +++ b/test/unittests/security/TOTP_Test.php @@ -24,8 +24,8 @@ class TOTP_Test extends TestCase function testRFC6238Counters() : void { $totp = new TOTP("abc", timestep: 30); - // Unix time => RFC6238 time based counter to pass to HOTP - // algorithm. + // Unix time => RFC6238 time based counter to pass to HOTP + // algorithm. $validValues = [ 59 => hexdec("0000000000000001"), 1111111109 => hexdec("00000000023523EC"), From b1fe0c38f7c636026030b907dfb6a585634a3daa Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Wed, 27 Aug 2025 13:16:03 -0400 Subject: [PATCH 18/28] Fix breadcrumb links --- modules/my_preferences/php/mfa.class.inc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/my_preferences/php/mfa.class.inc b/modules/my_preferences/php/mfa.class.inc index 5707e441ba5..bb308f7cab6 100644 --- a/modules/my_preferences/php/mfa.class.inc +++ b/modules/my_preferences/php/mfa.class.inc @@ -23,10 +23,12 @@ class MFA extends \NDB_Page { return new \LORIS\BreadcrumbTrail( new \LORIS\Breadcrumb( - dgettext("loris", "My Preferences") + dgettext("loris", "My Preferences"), + '/my_preferences', ), new \LORIS\Breadcrumb( - dgettext("my_preferences", "Configure 2FA") + dgettext("my_preferences", "Configure MFA"), + '/my_preferences/mfa', ), ); } From d3f1d1f747b738a195010bb0033f08e817ff1a39 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 08:23:13 -0400 Subject: [PATCH 19/28] Restore phpunit version after rebase --- composer.json | 2 +- composer.lock | 56 ++++++++++++++++---------------------- test/phpunit.xml | 71 ------------------------------------------------ 3 files changed, 25 insertions(+), 104 deletions(-) diff --git a/composer.json b/composer.json index c9b0c618120..5ea57eca61d 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev" : { "squizlabs/php_codesniffer" : "^3.5", - "phpunit/phpunit" : "9.4.4", + "phpunit/phpunit" : "12.0", "phan/phan": "^5.0", "phpstan/phpstan": "^1.4", "slevomat/coding-standard": "^6.4", diff --git a/composer.lock b/composer.lock index 6619c420804..7d9c3a679f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1ef012a82c44beec9e9fe4941f1690d5", + "content-hash": "a588887142a42bc949d04d77c3540042", "packages": [ { "name": "aws/aws-crt-php", @@ -215,16 +215,16 @@ }, { "name": "chillerlan/php-qrcode", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2" + "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/42e215640e9ebdd857570c9e4e52245d1ee51de2", - "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/390393e97a6e42ccae0e0d6205b8d4200f7ddc43", + "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43", "shasum": "" }, "require": { @@ -235,13 +235,13 @@ "require-dev": { "chillerlan/php-authenticator": "^4.3.1 || ^5.2.1", "ext-fileinfo": "*", - "phan/phan": "^5.4.5", + "phan/phan": "^5.5.1", "phpcompatibility/php-compatibility": "10.x-dev", "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^9.6", "setasign/fpdf": "^1.8.2", - "slevomat/coding-standard": "^8.15", - "squizlabs/php_codesniffer": "^3.11" + "slevomat/coding-standard": "^8.23.0", + "squizlabs/php_codesniffer": "^4.0.0" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", @@ -304,7 +304,7 @@ "type": "Ko-Fi" } ], - "time": "2024-11-21T16:12:34+00:00" + "time": "2025-09-19T17:30:27+00:00" }, { "name": "chillerlan/php-settings-container", @@ -3258,16 +3258,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.4.2", + "version": "12.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea" + "reference": "9912c83b5207ab3730fcadc42992e95bdb02dad8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a94ea4d26d865875803b23aaf78c3c2c670ea2ea", - "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9912c83b5207ab3730fcadc42992e95bdb02dad8", + "reference": "9912c83b5207ab3730fcadc42992e95bdb02dad8", "shasum": "" }, "require": { @@ -3277,23 +3277,23 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-code-coverage": "^12.0.0", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.3", + "sebastian/cli-parser": "^4.0.0", + "sebastian/comparator": "^7.0.0", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.3", - "sebastian/exporter": "^7.0.2", - "sebastian/global-state": "^8.0.2", + "sebastian/environment": "^8.0.0", + "sebastian/exporter": "^7.0.0", + "sebastian/global-state": "^8.0.0", "sebastian/object-enumerator": "^7.0.0", - "sebastian/type": "^6.0.3", + "sebastian/type": "^6.0.0", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, @@ -3303,7 +3303,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.4-dev" + "dev-main": "12.0-dev" } }, "autoload": { @@ -3335,7 +3335,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.0" }, "funding": [ { @@ -3346,20 +3346,12 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-10-30T08:41:39+00:00" + "time": "2025-02-07T05:03:33+00:00" }, { "name": "psr/container", diff --git a/test/phpunit.xml b/test/phpunit.xml index a020c21bfba..782d7bd6967 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -57,77 +57,6 @@ From 937d9f8fc9e55d6a7525e184b98591d0f076f71f Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 09:22:37 -0400 Subject: [PATCH 20/28] Add translatons after rebase --- Makefile | 7 +- jsx/DataTable.d.ts | 1 + jsx/I18nSetup.d.ts | 2 + locale/ja/LC_MESSAGES/loris.po | 67 +++++++++++++++++++ modules/my_preferences/jsx/mfa.tsx | 11 +-- .../locale/hi/LC_MESSAGES/my_preferences.po | 5 +- .../my_preferences/locale/my_preferences.pot | 9 ++- .../templates/form_my_preferences.tpl | 2 +- 8 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 jsx/I18nSetup.d.ts diff --git a/Makefile b/Makefile index 00ad0f538ca..a3bd480fd37 100755 --- a/Makefile +++ b/Makefile @@ -135,6 +135,9 @@ locales: msgfmt -o modules/module_manager/locale/ja/LC_MESSAGES/module_manager.mo modules/module_manager/locale/ja/LC_MESSAGES/module_manager.po msgfmt -o modules/mri_violations/locale/ja/LC_MESSAGES/mri_violations.mo modules/mri_violations/locale/ja/LC_MESSAGES/mri_violations.po msgfmt -o modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.mo modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po + npx i18next-conv -l ja -s modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po -t modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.json --compatibilityJSON v4 + npx i18next-conv -l hi -s modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po -t modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.json --compatibilityJSON v4 + msgfmt -o modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.mo modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po msgfmt -o modules/next_stage/locale/ja/LC_MESSAGES/next_stage.mo modules/next_stage/locale/ja/LC_MESSAGES/next_stage.po msgfmt -o modules/next_stage/locale/es/LC_MESSAGES/next_stage.mo modules/next_stage/locale/es/LC_MESSAGES/next_stage.po msgfmt -o modules/oidc/locale/ja/LC_MESSAGES/oidc.mo modules/oidc/locale/ja/LC_MESSAGES/oidc.po @@ -199,5 +202,7 @@ server_processes_manager: modules/server_processes_manager/locale/ja/LC_MESSAGES conflict_resolver: target=conflict_resolver npm run compile -my_preferences: +my_preferences: modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.mo modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.mo + npx i18next-conv -l ja -s modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po -t modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.json --compatibilityJSON v4 + npx i18next-conv -l hi -s modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po -t modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.json --compatibilityJSON v4 target=my_preferences npm run compile diff --git a/jsx/DataTable.d.ts b/jsx/DataTable.d.ts index 82c89b7b4e4..53caa9c563b 100644 --- a/jsx/DataTable.d.ts +++ b/jsx/DataTable.d.ts @@ -1,3 +1,4 @@ + import {ReactNode} from 'react'; type TableRow = (string|null)[] diff --git a/jsx/I18nSetup.d.ts b/jsx/I18nSetup.d.ts new file mode 100644 index 00000000000..0b845a98587 --- /dev/null +++ b/jsx/I18nSetup.d.ts @@ -0,0 +1,2 @@ +import i18n from 'i18next'; +export default i18n; diff --git a/locale/ja/LC_MESSAGES/loris.po b/locale/ja/LC_MESSAGES/loris.po index 7c299035cd5..6cc0c718427 100644 --- a/locale/ja/LC_MESSAGES/loris.po +++ b/locale/ja/LC_MESSAGES/loris.po @@ -241,6 +241,12 @@ msgstr "言語" msgid "Ethnicity" msgstr "民族" +msgid "Save" +msgstr "保存" + +msgid "Reset" +msgstr "リセット" + # Data table strings msgid "{{pageCount}} rows displayed of {{totalCount}}." msgstr "{{totalCount}}行中{{pageCount}}行を表示" @@ -255,6 +261,22 @@ msgstr "データをCSVとしてダウンロード" msgid "Views" msgstr "ビュー" +#: php/libraries/Password.class.inc +msgid "The password is too short" +msgstr "パスワードが短すぎます" + +msgid "The password is not complex enough." +msgstr "パスワードが十分に複雑ではありません。" + +msgid "This password is known to be exposed in online data breaches." +msgstr "このパスワードは、オンラインデータ侵害で漏洩されることが知られています。" + +msgid "Data Supervisors to Email" +msgstr "データ管理者に電子メールで連絡" + +msgid "Invalid email address" +msgstr "無効なメールアドレス" + # Common strings on widgets msgid "NEW" msgstr "新しい" @@ -355,3 +377,48 @@ msgstr "{{years}}歳" msgid "Loading..." msgstr "読み込み中..." +# User Account related +msgid "Password Rules" +msgstr "パスワードルール" + +msgid "Username" +msgstr "ユーザー名" + +msgid "First name" +msgstr "ファーストネーム" + +msgid "First name is required and should not exceed 120 characters" +msgstr "名は必須で、120文字以内で入力してください。" + +msgid "Last name" +msgstr "苗字" + +msgid "Last name is required and should not exceed 120 characters" +msgstr "姓は必須で、120文字以内で入力してください。" + +msgid "Email address" +msgstr "電子メールアドレス" + +msgid "New Password" +msgstr "新しいパスワード" + +msgid "Confirm Password" +msgstr "パスワードを認証する" + +msgid "Email address is required" +msgstr "メールアドレスは必須です" + +msgid "The password must be at least 8 characters long." +msgstr "パスワードは8文字以上である必要があります" + +msgid "The password cannot be your username or email address." +msgstr "パスワードにはユーザー名やメールアドレスは使用できません。" + +msgid "Please choose a unique password." +msgstr "固有のパスワードを選択してください。" + +msgid "No special characters are required but your password must be sufficiently complex to be accepted." +msgstr "特殊文字は必要ありませんが、パスワードは受け入れられるほど複雑である必要があります。" + +msgid "We suggest using a password manager to generate one for you." +msgstr "パスワード マネージャーを使用してパスワードを生成することをお勧めします。" diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 3217b62290d..d7ae63f5c9c 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -5,6 +5,8 @@ import QRCode from 'react-qr-code'; import * as base32 from 'hi-base32'; import Modal from 'Modal'; import MFAPrompt from 'jsx/MFAPrompt'; +import {withTranslation} from 'react-i18next'; +import i18n from 'I18nSetup'; declare const loris: any; @@ -98,16 +100,17 @@ function MFAIndex(): React.ReactElement { } window.addEventListener('load', () => { - /* - const MFAIndex = withTranslation( + i18n.addResourceBundle('ja', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); + i18n.addResourceBundle('hi', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); + const TranslatedMFAIndex = withTranslation( ['my_preferences', 'loris'] )(MFAIndex); - */ + const element = document.getElementById('lorisworkspace'); if (!element) { throw new Error('Missing lorisworkspace'); } createRoot(element).render( - + ); }); diff --git a/modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po b/modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po index 8b54649b398..d686640a1d7 100644 --- a/modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po +++ b/modules/my_preferences/locale/hi/LC_MESSAGES/my_preferences.po @@ -18,9 +18,6 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "My Preferences" -msgstr "मेरी प्राथमिकताएँ" - msgid "Edit My Information" msgstr "मेरी जानकारी संपादित करें" @@ -52,4 +49,4 @@ msgid "The passwords do not match" msgstr "पासवर्ड मेल नहीं खाते।" msgid "New and old passwords are identical" -msgstr "नया और पुराना पासवर्ड समान हैं।" \ No newline at end of file +msgstr "नया और पुराना पासवर्ड समान हैं।" diff --git a/modules/my_preferences/locale/my_preferences.pot b/modules/my_preferences/locale/my_preferences.pot index b83fd0bb954..df3675532a0 100644 --- a/modules/my_preferences/locale/my_preferences.pot +++ b/modules/my_preferences/locale/my_preferences.pot @@ -18,9 +18,6 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "My Preferences" -msgstr "" - msgid "Edit My Information" msgstr "" @@ -53,3 +50,9 @@ msgstr "" msgid "New and old passwords are identical" msgstr "" + +msgid "Configure multi-factor authentication (MFA)" +msgstr "" + +msgid "Configure MFA" +msgstr "" diff --git a/modules/my_preferences/templates/form_my_preferences.tpl b/modules/my_preferences/templates/form_my_preferences.tpl index b93e8c8568d..a8146d1b35d 100644 --- a/modules/my_preferences/templates/form_my_preferences.tpl +++ b/modules/my_preferences/templates/form_my_preferences.tpl @@ -70,7 +70,7 @@ in a different form element with a smarty template and it's easier to create a new "fresh" page with modern react/etc than rewrite the whole page or do a hybrid here *} - Configure multi-factor authentication (MFA) + {dgettext("my_preferences", "Configure multi-factor authentication (MFA)")} From a9921c0d9b4667bec0778aa77aa28a7212715e85 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 10:14:34 -0400 Subject: [PATCH 21/28] Add missing file --- modules/my_preferences/locale/my_preferences.pot | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/my_preferences/locale/my_preferences.pot b/modules/my_preferences/locale/my_preferences.pot index df3675532a0..21c846b80c1 100644 --- a/modules/my_preferences/locale/my_preferences.pot +++ b/modules/my_preferences/locale/my_preferences.pot @@ -56,3 +56,6 @@ msgstr "" msgid "Configure MFA" msgstr "" + +msgid "Use the following key in your authenticator app: {code}" +msgstr "" From 08728fcdf019505d82eb27371153e873faba92b3 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 10:19:18 -0400 Subject: [PATCH 22/28] WIP --- modules/my_preferences/jsx/mfa.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index d7ae63f5c9c..945c961664d 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -5,7 +5,7 @@ import QRCode from 'react-qr-code'; import * as base32 from 'hi-base32'; import Modal from 'Modal'; import MFAPrompt from 'jsx/MFAPrompt'; -import {withTranslation} from 'react-i18next'; +import {useTranslation, Trans} from 'react-i18next'; import i18n from 'I18nSetup'; declare const loris: any; @@ -71,6 +71,7 @@ function CodeValidator(props: { function MFAIndex(): React.ReactElement { const [showModal, setShowModal] = useState(false); const [key] = useState(genPotentialSecret()); + const {t} = useTranslation(); const studyTitle = loris.config('studyTitle'); const mfaUrl = 'otpauth://totp/' + encodeURI(studyTitle) @@ -83,7 +84,10 @@ function MFAIndex(): React.ReactElement { onClose={() => setShowModal(false)} show={showModal} throwWarning={false}> -

Use the following key in your authenticator app: {key}

+ +

Scan the following QR code below in your MFA authenticator and enter the code to validate.

@@ -102,15 +106,12 @@ function MFAIndex(): React.ReactElement { window.addEventListener('load', () => { i18n.addResourceBundle('ja', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); i18n.addResourceBundle('hi', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); - const TranslatedMFAIndex = withTranslation( - ['my_preferences', 'loris'] - )(MFAIndex); const element = document.getElementById('lorisworkspace'); if (!element) { throw new Error('Missing lorisworkspace'); } createRoot(element).render( - + ); }); From a39c060f3ef7045378d6b3104e1d680f04d90478 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 10:24:02 -0400 Subject: [PATCH 23/28] add mising file --- .../locale/ja/LC_MESSAGES/my_preferences.po | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po diff --git a/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po new file mode 100644 index 00000000000..0cb4fab45ba --- /dev/null +++ b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po @@ -0,0 +1,62 @@ +# Default LORIS strings to be translated (English). +# Copy this to a language specific file and add translations to the +# new file. +# Copyright (C) 2025 +# This file is distributed under the same license as the LORIS package. +# Dave MacFarlane , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: LORIS 27\n" +"Report-Msgid-Bugs-To: https://github.com/aces/Loris/issues\n" +"POT-Creation-Date: 2025-04-08 14:37-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "Edit My Information" +msgstr "私の情報を編集する" + +msgid "Notifications" +msgstr "通知" + +msgid "Your email address must be less than 255 characters long" +msgstr "メールアドレスは255文字未満でなければなりません" + +msgid "Language preference" +msgstr "言語設定" + +msgid "Operation" +msgstr "作用" + +msgid "Description" +msgstr "説明" + +msgid "The email address already exists" +msgstr "メールアドレスは既に存在します" + +msgid "Your password cannot be your email" +msgstr "パスワードにメールアドレスは使用できません" + +msgid "Your password cannot be your username" +msgstr "パスワードをユーザー名と同じにすることはできません。" + +msgid "The passwords do not match" +msgstr "パスワードが一致しません" + +msgid "New and old passwords are identical" +msgstr "新しいパスワードと古いパスワードは同一です" + +msgid "Configure multi-factor authentication (MFA)" +msgstr "多要素認証を設定する" + +msgid "Configure MFA" +msgstr "多要素認証を設定する" + +msgid "Use the following key in your authenticator app: {{code}}" +msgstr "認証アプリで次のコードを使用してください: {{code}}" From 559675c7f713c97a32445f819ab1293d9a9342e0 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 11:13:36 -0400 Subject: [PATCH 24/28] WIP --- modules/my_preferences/jsx/mfa.tsx | 14 +++++++------- .../locale/ja/LC_MESSAGES/my_preferences.po | 10 ++++++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 945c961664d..52cc1b4e880 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -80,17 +80,17 @@ function MFAIndex(): React.ReactElement { + '&period=30&digits=6&issuer=' + encodeURI(studyTitle); return
setShowModal(false)} show={showModal} throwWarning={false}> - - +

CODE]} + values={{code: key}} />

-

Scan the following QR code below in your MFA authenticator and - enter the code to validate.

+

{t('Scan the following QR code below in your MFA authenticator and enter the code to validate.', {ns: 'my_preferences'})}

Note that this will overwrite any previously setup MFA in LORIS! diff --git a/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po index 0cb4fab45ba..9512897c619 100644 --- a/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po +++ b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po @@ -58,5 +58,11 @@ msgstr "多要素認証を設定する" msgid "Configure MFA" msgstr "多要素認証を設定する" -msgid "Use the following key in your authenticator app: {{code}}" -msgstr "認証アプリで次のコードを使用してください: {{code}}" +msgid "Manual MFA Setup" +msgstr "手動多要素認証の設定" + +msgid "Use the following key in your authenticator app: <0>{{code}}" +msgstr "認証アプリで次のコードを使用してください: <0>{{code}}" + +msgid "Scan the following QR code below in your MFA authenticator and enter the code to validate." +msgstr "多要素認証システムで以下の QR コードをスキャンし、コードを入力して検証します。" From 78fb8da71f693ad08822e41171d97f168ea1e8d0 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 11:52:46 -0400 Subject: [PATCH 25/28] Fix Translations --- modules/login/php/mfa.class.inc | 1 + modules/my_preferences/jsx/mfa.tsx | 23 +++++++++++++------ .../locale/ja/LC_MESSAGES/my_preferences.po | 12 ++++++++++ modules/my_preferences/php/mfa.class.inc | 9 ++++++-- php/libraries/SinglePointLogin.class.inc | 1 - php/libraries/User.class.inc | 1 - test/unittests/security/TOTP_Test.php | 1 - 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/modules/login/php/mfa.class.inc b/modules/login/php/mfa.class.inc index 9ebff33848c..743e58ec08d 100644 --- a/modules/login/php/mfa.class.inc +++ b/modules/login/php/mfa.class.inc @@ -50,6 +50,7 @@ class MFA extends \NDB_Page [$baseURL . '/login/css/login.css'] ); } + /** * This function will return a json object for login module. * diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 52cc1b4e880..307c73c30de 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -29,6 +29,7 @@ function genPotentialSecret() { function CodeValidator(props: { secret: string }): React.ReactElement { + const {t} = useTranslation(); const formSubmit = useCallback( (code: string, onError: (msg: string) => void) => { const formObject = new FormData(); @@ -46,7 +47,12 @@ function CodeValidator(props: { return resp.json(); }).then( (json) => { if (json.ok == 'success') { - swal.fire('Success!', json.message, 'success').then( () => { + swal.fire({ + title: t('Success!', {ns: 'loris'}), + text: json.message, + type: 'success', + confirmButtonText: t('OK', {ns: 'loris'}), + }).then( () => { window.location.href = loris.BaseURL + '/my_preferences/'; }); } else if (json.error) { @@ -60,7 +66,7 @@ function CodeValidator(props: { }, [props.secret]); return (

-

Validate Code

+

{t('Validate Code', {ns: 'my_preferences'})}

); @@ -92,13 +98,16 @@ function MFAIndex(): React.ReactElement {

{t('Scan the following QR code below in your MFA authenticator and enter the code to validate.', {ns: 'my_preferences'})}

- Note that this will overwrite any previously - setup MFA in LORIS! + overwrite]} + defaults="Note that this will <0>overwrite any previously setup MFA in LORIS!" />

-

Can't scan the QR code? setShowModal(true)}> - Setup manually. -

+

setShowModal(true)} />]} />

; } diff --git a/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po index 9512897c619..35212dfd974 100644 --- a/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po +++ b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po @@ -66,3 +66,15 @@ msgstr "認証アプリで次のコードを使用してください: <0>{{code} msgid "Scan the following QR code below in your MFA authenticator and enter the code to validate." msgstr "多要素認証システムで以下の QR コードをスキャンし、コードを入力して検証します。" + +msgid "Note that this will <0>overwrite any previously setup MFA in LORIS!" +msgstr "これにより、ロリスで以前に設定された多要素認証が <0>上書き されることに注意してください。" + +msgid "Can't scan the QR code? <0>Setup manually." +msgstr "QR コードをスキャンできませんか? <0>手動で設定してください。" + +msgid "Validate Code" +msgstr "コードの検証" + +msgid "Successfully registered multifactor authenticator" +msgstr "多要素認証の登録に成功しました" diff --git a/modules/my_preferences/php/mfa.class.inc b/modules/my_preferences/php/mfa.class.inc index bb308f7cab6..3e6981e3264 100644 --- a/modules/my_preferences/php/mfa.class.inc +++ b/modules/my_preferences/php/mfa.class.inc @@ -55,6 +55,7 @@ class MFA extends \NDB_Page } } + /** * {@inheritDoc} * @@ -111,8 +112,12 @@ class MFA extends \NDB_Page $login = $_SESSION['State']->getProperty('login'); $login->setPassedMFA(); return new \LORIS\Http\Response\JSON\OK( - ['ok' => 'success', - 'message' => 'Successfully registered multifactor authenticator' + [ + 'ok' => 'success', + 'message' => dgettext( + 'my_preferences', + 'Successfully registered multifactor authenticator' + ) ] ); } diff --git a/php/libraries/SinglePointLogin.class.inc b/php/libraries/SinglePointLogin.class.inc index d1ffa353642..6eb42f3c627 100644 --- a/php/libraries/SinglePointLogin.class.inc +++ b/php/libraries/SinglePointLogin.class.inc @@ -623,5 +623,4 @@ class SinglePointLogin $_SESSION['PassedMFA'] = true; session_write_close(); } - } diff --git a/php/libraries/User.class.inc b/php/libraries/User.class.inc index f3a243de72d..ca230682c45 100644 --- a/php/libraries/User.class.inc +++ b/php/libraries/User.class.inc @@ -689,7 +689,6 @@ class User extends UserPermissions implements return $this->userInfo['Pending_approval'] == 'Y'; } - /** * Returns a TOTP validator for this account, or null if 2FA * has never been enabled by the user. diff --git a/test/unittests/security/TOTP_Test.php b/test/unittests/security/TOTP_Test.php index 78a4fb348eb..46daa34d590 100644 --- a/test/unittests/security/TOTP_Test.php +++ b/test/unittests/security/TOTP_Test.php @@ -15,7 +15,6 @@ */ class TOTP_Test extends TestCase { - /** * Test that test vector counters from RFC6238 pass * From ab5e16596466eb5b89f2214d13409a6b03dcea21 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 12:29:40 -0400 Subject: [PATCH 26/28] phan --- modules/user_accounts/php/edit_user.class.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/user_accounts/php/edit_user.class.inc b/modules/user_accounts/php/edit_user.class.inc index c5379a5ffa3..3c9450b7677 100644 --- a/modules/user_accounts/php/edit_user.class.inc +++ b/modules/user_accounts/php/edit_user.class.inc @@ -1139,9 +1139,9 @@ class Edit_User extends \NDB_Form // already handled in email/password check // case 3 - Edit user if ((isset($values['UserID']) && !isset($values['NA_UserID']) - && $values['UserID'] === $values['Password_hash']) + && $values['UserID'] === ($values['Password_hash'] ?? '')) || (!empty($this->identifier) - && $this->identifier === $values['Password_hash']) + && $this->identifier === ($values['Password_hash'] ?? '')) ) { $errors['Password'] = self::PASSWORD_ERROR_IS_USER; } From de802f3f996693b68d83b6c957a230491a5a228f Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 12:50:25 -0400 Subject: [PATCH 27/28] lint js --- modules/my_preferences/jsx/mfa.tsx | 35 +++++++++++++++++------------- tsconfig.json | 1 + 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx index 307c73c30de..c6cd6b7f6b6 100644 --- a/modules/my_preferences/jsx/mfa.tsx +++ b/modules/my_preferences/jsx/mfa.tsx @@ -7,6 +7,8 @@ import Modal from 'Modal'; import MFAPrompt from 'jsx/MFAPrompt'; import {useTranslation, Trans} from 'react-i18next'; import i18n from 'I18nSetup'; +import jaStrings from '../locale/ja/LC_MESSAGES/my_preferences.json'; +import hiStrings from '../locale/hi/LC_MESSAGES/my_preferences.json'; declare const loris: any; @@ -50,9 +52,9 @@ function CodeValidator(props: { swal.fire({ title: t('Success!', {ns: 'loris'}), text: json.message, - type: 'success', - confirmButtonText: t('OK', {ns: 'loris'}), - }).then( () => { + type: 'success', + confirmButtonText: t('OK', {ns: 'loris'}), + }).then( () => { window.location.href = loris.BaseURL + '/my_preferences/'; }); } else if (json.error) { @@ -91,30 +93,33 @@ function MFAIndex(): React.ReactElement { show={showModal} throwWarning={false}>

CODE]} + defaults={'Use the following key in your authenticator app: ' + + '<0>{{code}}'} + ns="my_preferences" + components={[CODE]} values={{code: key}} />

-

{t('Scan the following QR code below in your MFA authenticator and enter the code to validate.', {ns: 'my_preferences'})}

+

{t('Scan the following QR code below in your MFA authenticator and ' + + 'enter the code to validate.', {ns: 'my_preferences'})}

overwrite]} - defaults="Note that this will <0>overwrite any previously setup MFA in LORIS!" /> + ns="my_preferences" + components={[overwrite]} + defaults={'Note that this will <0>overwrite any ' + + 'previously setup MFA in LORIS!'} />

setShowModal(true)} />]} />

+ ns="my_preferences" + defaults="Can't scan the QR code? <0>Setup manually." + components={[ setShowModal(true)} />]} />

; } window.addEventListener('load', () => { - i18n.addResourceBundle('ja', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); - i18n.addResourceBundle('hi', 'my_preferences', require("../locale/ja/LC_MESSAGES/my_preferences.json")); + i18n.addResourceBundle('ja', 'my_preferences', jaStrings); + i18n.addResourceBundle('hi', 'my_preferences', hiStrings); const element = document.getElementById('lorisworkspace'); if (!element) { diff --git a/tsconfig.json b/tsconfig.json index 86af508d2d1..68d36de7035 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "paths": { "*": ["*", "jsx/*"] }, + "resolveJsonModule": true, "sourceMap": true, "jsx": "preserve", "jsxFactory": "h", From 4a505ac9f81b97364f51ee2842613b3c5b114b32 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 30 Oct 2025 13:45:12 -0400 Subject: [PATCH 28/28] Update template --- .../my_preferences/locale/my_preferences.pot | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/my_preferences/locale/my_preferences.pot b/modules/my_preferences/locale/my_preferences.pot index 21c846b80c1..a7474d1c51c 100644 --- a/modules/my_preferences/locale/my_preferences.pot +++ b/modules/my_preferences/locale/my_preferences.pot @@ -57,5 +57,24 @@ msgstr "" msgid "Configure MFA" msgstr "" -msgid "Use the following key in your authenticator app: {code}" +msgid "Manual MFA Setup" msgstr "" + +msgid "Use the following key in your authenticator app: <0>{{code}}" +msgstr "" + +msgid "Scan the following QR code below in your MFA authenticator and enter the code to validate." +msgstr "" + +msgid "Note that this will <0>overwrite any previously setup MFA in LORIS!" +msgstr "" + +msgid "Can't scan the QR code? <0>Setup manually." +msgstr "" + +msgid "Validate Code" +msgstr "" + +msgid "Successfully registered multifactor authenticator" +msgstr "" +