diff --git a/Makefile b/Makefile index c614da9d7bb..6d5946b5f8d 100755 --- a/Makefile +++ b/Makefile @@ -166,3 +166,6 @@ server_processes_manager: conflict_resolver: target=conflict_resolver npm run compile + +my_preferences: + target=my_preferences npm run compile diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index 3196f384902..c9687de32aa 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -105,6 +105,7 @@ CREATE TABLE `users` ( `Active` enum('Y','N') NOT NULL default 'Y', `Password_hash` varchar(255) default NULL, `PasswordChangeRequired` tinyint(1) NOT NULL default 0, + `TOTPSecret` binary(64) DEFAULT NULL, `Pending_approval` enum('Y','N') default 'Y', `Doc_Repo_Notifications` enum('Y','N') default 'N', `language_preference` integer unsigned default NULL, diff --git a/SQL/New_patches/2025-08-21-add_totp_support.sql b/SQL/New_patches/2025-08-21-add_totp_support.sql new file mode 100644 index 00000000000..ebc5cfef759 --- /dev/null +++ b/SQL/New_patches/2025-08-21-add_totp_support.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN TOTPSecret binary(64) DEFAULT NULL AFTER PasswordChangeRequired; diff --git a/composer.json b/composer.json index b9ec865d0ed..c9b0c618120 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,9 @@ "laminas/laminas-diactoros" : "^3.5", "ext-json": "*", "bjeavons/zxcvbn-php": "^1.0", - "aws/aws-sdk-php": "^3.209" + "aws/aws-sdk-php": "^3.209", + "selective/base32": "^2.0", + "chillerlan/php-qrcode": "^5.0" }, "require-dev" : { "squizlabs/php_codesniffer" : "^3.5", diff --git a/composer.lock b/composer.lock index 094e7cf5ba4..dac25e15568 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": "41781cfccc430391fb656d9c45d9bb1d", + "content-hash": "3a70cc7db52d96770486c18e83576b3a", "packages": [ { "name": "aws/aws-crt-php", @@ -160,16 +160,16 @@ }, { "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 +210,181 @@ ], "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": "chillerlan/php-qrcode", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-qrcode.git", + "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/42e215640e9ebdd857570c9e4e52245d1ee51de2", + "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2", + "shasum": "" + }, + "require": { + "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "chillerlan/php-authenticator": "^4.3.1 || ^5.2.1", + "ext-fileinfo": "*", + "phan/phan": "^5.4.5", + "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" + }, + "suggest": { + "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\QRCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "Apache-2.0" + ], + "authors": [ + { + "name": "Kazuhiko Arase", + "homepage": "https://github.com/kazuhikoarase/qrcode-generator" + }, + { + "name": "ZXing Authors", + "homepage": "https://github.com/zxing/zxing" + }, + { + "name": "Ashot Khanamiryan", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" + }, + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + }, + { + "name": "Contributors", + "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" + } + ], + "description": "A QR Code generator and reader with a user-friendly API. PHP 7.4+", + "homepage": "https://github.com/chillerlan/php-qrcode", + "keywords": [ + "phpqrcode", + "qr", + "qr code", + "qr-reader", + "qrcode", + "qrcode-generator", + "qrcode-reader" + ], + "support": { + "docs": "https://php-qrcode.readthedocs.io", + "issues": "https://github.com/chillerlan/php-qrcode/issues", + "source": "https://github.com/chillerlan/php-qrcode" + }, + "funding": [ + { + "url": "https://ko-fi.com/codemasher", + "type": "Ko-Fi" + } + ], + "time": "2024-11-21T16:12:34+00:00" + }, + { + "name": "chillerlan/php-settings-container", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-settings-container.git", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\Settings\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + } + ], + "description": "A container class for immutable settings objects. Not a DI container.", + "homepage": "https://github.com/chillerlan/php-settings-container", + "keywords": [ + "Settings", + "configuration", + "container", + "helper" + ], + "support": { + "issues": "https://github.com/chillerlan/php-settings-container/issues", + "source": "https://github.com/chillerlan/php-settings-container" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2024-07-16T11:13:48+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 +432,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,20 +486,20 @@ "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.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -437,7 +596,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.9.3" }, "funding": [ { @@ -453,20 +612,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { @@ -520,7 +679,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.2.0" }, "funding": [ { @@ -536,20 +695,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -636,7 +795,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.7.1" }, "funding": [ { @@ -652,20 +811,20 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "laminas/laminas-diactoros", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" + "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f" }, "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/b068eac123f21c0e592de41deeb7403b88e0a89f", + "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f", "shasum": "" }, "require": { @@ -686,7 +845,7 @@ "ext-gd": "*", "ext-libxml": "*", "http-interop/http-factory-tests": "^2.2.0", - "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-coding-standard": "~3.0.0", "php-http/psr7-integration-tests": "^1.4.0", "phpunit/phpunit": "^10.5.36", "psalm/plugin-phpunit": "^0.19.0", @@ -740,7 +899,7 @@ "type": "community_bridge" } ], - "time": "2024-10-14T11:59:49+00:00" + "time": "2025-05-05T16:03:34+00:00" }, { "name": "mtdowling/jmespath.php", @@ -1343,6 +1502,54 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "selective/base32", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/selective-php/base32.git", + "reference": "8da7955d3cc835f653c25dd516e39f61f2a6046a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/selective-php/base32/zipball/8da7955d3cc835f653c25dd516e39f61f2a6046a", + "reference": "8da7955d3cc835f653c25dd516e39f61f2a6046a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "overtrue/phplint": "^1.1", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^6|^7", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Selective\\Base32\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Base32 based on RFC 4648", + "homepage": "https://github.com/selective-php/base32", + "keywords": [ + "base32", + "decode", + "encode", + "encoder", + "encoding" + ], + "support": { + "issues": "https://github.com/selective-php/base32/issues", + "source": "https://github.com/selective-php/base32/tree/master" + }, + "time": "2020-04-03T20:48:25+00:00" + }, { "name": "smarty/smarty", "version": "v4.5.5", @@ -1411,16 +1618,16 @@ }, { "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 +1640,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1458,7 +1665,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 +1681,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 +1746,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 +1757,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 +1851,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 +1912,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 +1922,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", @@ -2076,16 +2284,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -2124,7 +2332,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -2132,7 +2340,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "netresearch/jsonmapper", @@ -2187,16 +2395,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -2215,7 +2423,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -2239,22 +2447,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phan/phan", - "version": "5.4.5", + "version": "5.5.1", "source": { "type": "git", "url": "https://github.com/phan/phan.git", - "reference": "2b15302175931a0629a85c57d0c1f91d68b26a4d" + "reference": "2b6a846eff1a65dd0229ffa2370b4c35a96b7f3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phan/phan/zipball/2b15302175931a0629a85c57d0c1f91d68b26a4d", - "reference": "2b15302175931a0629a85c57d0c1f91d68b26a4d", + "url": "https://api.github.com/repos/phan/phan/zipball/2b6a846eff1a65dd0229ffa2370b4c35a96b7f3c", + "reference": "2b6a846eff1a65dd0229ffa2370b4c35a96b7f3c", "shasum": "" }, "require": { @@ -2265,7 +2473,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", @@ -2319,9 +2527,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.1" }, - "time": "2024-08-13T21:41:35+00:00" + "time": "2025-08-05T20:10:06+00:00" }, { "name": "phar-io/manifest", @@ -2447,12 +2655,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": { @@ -2504,9 +2712,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", @@ -2675,29 +2883,29 @@ }, { "name": "phpspec/prophecy", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93" + "reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a0165c648cab6a80311c74ffc708a07bb53ecc93", - "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/35f1adb388946d92e6edab2aa2cb2b60e132ebd5", + "reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2 || ^2.0", - "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", + "php": "^7.4 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.40", "phpspec/phpspec": "^6.0 || ^7.0", - "phpstan/phpstan": "^1.9", + "phpstan/phpstan": "^2.1.13", "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" }, "type": "library", @@ -2739,9 +2947,9 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.20.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.22.0" }, - "time": "2024-11-19T13:12:41+00:00" + "time": "2025-04-29T14:58:06+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -2798,16 +3006,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.16", + "version": "1.12.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e0bb5cb78545aae631220735aa706eac633a6be9", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "shasum": "" }, "require": { @@ -2852,7 +3060,7 @@ "type": "github" } ], - "time": "2025-01-21T14:50:05+00:00" + "time": "2025-07-17T17:15:39+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3564,16 +3772,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -3626,15 +3834,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "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/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -3901,16 +4121,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -3953,15 +4173,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "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": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -4134,16 +4366,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -4185,15 +4417,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "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": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -4421,16 +4665,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.3", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "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/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -4497,31 +4741,32 @@ "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-06-17T22:17:01+00:00" }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "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", @@ -4578,7 +4823,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.3.2" }, "funding": [ { @@ -4589,16 +4834,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-07-30T17:13:41+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", @@ -4657,7 +4906,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": [ { @@ -4668,6 +4917,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" @@ -4677,16 +4930,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": { @@ -4735,7 +4988,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": [ { @@ -4746,16 +4999,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", @@ -4816,7 +5073,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": [ { @@ -4827,6 +5084,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" @@ -4836,16 +5097,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": { @@ -4896,7 +5157,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": [ { @@ -4907,25 +5168,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.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "shasum": "" }, "require": { @@ -4957,7 +5222,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.0" }, "funding": [ { @@ -4973,20 +5238,20 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-04-17T09:11:12+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": { @@ -5004,7 +5269,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5040,7 +5305,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": [ { @@ -5056,20 +5321,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.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { @@ -5127,7 +5392,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -5138,12 +5403,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-07-10T08:47:49+00:00" }, { "name": "theseer/tokenizer", diff --git a/htdocs/index.php b/htdocs/index.php index 0169c82f05a..50ce7838681 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/jsx/MFAPrompt.tsx b/jsx/MFAPrompt.tsx new file mode 100644 index 00000000000..1d72f216df9 --- /dev/null +++ b/jsx/MFAPrompt.tsx @@ -0,0 +1,127 @@ +import {useState, useEffect, useCallback} from 'react'; +import swal from 'sweetalert2'; + +/** + * Render a single digit of an MFA prompt + * + * @param props - React props + * @param props.value - The current value of the digit to display + * @param props.onChange - A callback to call when the number is changed + */ +function Digit(props: { + value: number|null|string, + onChange: (newvalue: number) => boolean} +) { + return { + e.preventDefault(); + if (e.keyCode >= 48 /* '0' */ && e.keyCode <= 57 /* '9' */) { + if (props.onChange(e.keyCode-48)) { + const target = e.target as HTMLElement; + (target.nextSibling as HTMLElement)?.focus(); + } + return; + } + if (e.key == 'ArrowLeft') { + const target = e.target as HTMLElement; + (target.previousSibling as HTMLElement)?.focus(); + } if (e.key == 'ArrowRight') { + const target = e.target as HTMLElement; + (target.nextSibling as HTMLElement)?.focus(); + } + } + } + value={props.value || ''} + />; +} + +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 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. + * If the code is invalid, the callback should call + * the onError callback to reset the prompt state. + */ +function MFAPrompt(props: {validate: + (code: string, onError: errorCallback) => void +}) { + 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; + }); + return true; + } + return false; + }, + [] + ); + const errorCallback = useCallback( (msg: string) => { + swal.fire('Error', msg, 'error'); + setCode([null, null, null, null, null, null]); + }, []); + useEffect( () => { + if (code.indexOf(null) >= 0) { + return; + } + props.validate(code.join(''), errorCallback); + }, + [code, errorCallback] + ); + + + // nb. React treats the number 0 as falsey and doesn't display it when passed + // to an input value but *does* display the string "0". + return
+ digitCallback(0, newval)} + /> + digitCallback(1, newval)} + /> + digitCallback(2, newval)} + /> + digitCallback(3, newval)} + /> + digitCallback(4, newval)} + /> + digitCallback(5, newval)} + /> +
; +} + +export default MFAPrompt; diff --git a/modules/login/jsx/mfaPrompt.tsx b/modules/login/jsx/mfaPrompt.tsx new file mode 100644 index 00000000000..9a46ac64a8f --- /dev/null +++ b/modules/login/jsx/mfaPrompt.tsx @@ -0,0 +1,43 @@ +import {createRoot} from 'react-dom/client'; +import MFAPrompt from 'jsx/MFAPrompt'; + +type errorCallback = (msg: string) => void; +/** + * Prompt for an MFA code to login. + */ +function LoginMFAPrompt() { + return (
+

Multifactor authentication required

+

Enter the code from your authenticator app below to proceed.

+ { + fetch('/login/mfa', + { + method: 'POST', + body: JSON.stringify({'code': code}), + credentials: 'same-origin', + }).then((resp) => { + if (!resp.ok) { + console.warn('invalid response'); + } + return resp.json(); + }).then( (json) => { + if (json['success']) { + window.location.reload(); + } else if (json['error']) { + onError(json['error']); + } + }).catch( () => { + onError('Error validating code'); + console.error('error validating code'); + }); + }} /> +
); +} + +window.addEventListener('load', () => { + createRoot( + document.getElementsByClassName('main-content')[0] + ).render( + + ); +}); diff --git a/modules/login/php/mfa.class.inc b/modules/login/php/mfa.class.inc new file mode 100644 index 00000000000..9ebff33848c --- /dev/null +++ b/modules/login/php/mfa.class.inc @@ -0,0 +1,124 @@ +settings()->getBaseURL(); + $deps = parent::getJSDependencies(); + return array_merge( + $deps, + [ + $baseURL . '/login/js/mfaPrompt.js', + ] + ); + } + + /** + * {@inheritDoc} + * + * @return array + */ + function getCSSDependencies() + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $deps = parent::getCSSDependencies(); + return array_merge( + $deps, + [$baseURL . '/login/css/login.css'] + ); + } + /** + * This function will return a json object for login module. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + // Ensure POST request. + switch ($request->getMethod()) { + case 'GET': + return parent::handle($request); + case 'POST': + return $this->_handlePOST($request); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * Processes the values & saves to database and return a json response. + * + * @param ServerRequestInterface $request The incoming PSR7 request. + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePOST(ServerRequestInterface $request) : ResponseInterface + { + $requestdata = json_decode((string )$request->getBody(), true); + $user = $request->getAttribute("user"); + if (!isset($requestdata['code'])) { + return new \LORIS\Http\Response\JSON\Unauthorized("Missing code"); + } + + $validator = $user->getTOTPValidator(); + $counter = $validator->getTimeCounter(); + $wantCode = $validator->getCode($counter, 6); + if ($wantCode === $requestdata['code']) { + $login = $_SESSION['State']->getProperty('login'); + $login->setPassedMFA(); + return new \LORIS\Http\Response\JSON\OK(["success" => "validated code"]); + } else { + return new \LORIS\Http\Response\JSON\Unauthorized("Invalid MFA code"); + } + } + + /** + * Return an array of valid HTTP methods for this endpoint + * + * @return string[] Valid versions + */ + protected function allowedMethods(): array + { + return ['GET', 'POST']; + } + + /** + * Returns true if the user has permission to access + * the Login module + * + * @param \User $user The user whose access is being checked + * + * @return bool true if user has permission + */ + function _hasAccess(\User $user) : bool + { + return true; + } +} diff --git a/modules/my_preferences/jsx/mfa.tsx b/modules/my_preferences/jsx/mfa.tsx new file mode 100644 index 00000000000..3217b62290d --- /dev/null +++ b/modules/my_preferences/jsx/mfa.tsx @@ -0,0 +1,113 @@ +import {createRoot} from 'react-dom/client'; +import {useState, useCallback} from 'react'; +import swal from 'sweetalert2'; +import QRCode from 'react-qr-code'; +import * as base32 from 'hi-base32'; +import Modal from 'Modal'; +import MFAPrompt from 'jsx/MFAPrompt'; + +declare const loris: any; + +/** + * Get a secret that could be used as a secret + */ +function genPotentialSecret() { + const array = new Uint8Array(20); + crypto.getRandomValues(array); + + return base32.encode(array); +} + +/** + * React props + * + * @param props - react props + * @param props.secret - the shared secret key + */ +function CodeValidator(props: { + secret: string +}): React.ReactElement { + const formSubmit = useCallback( + (code: string, onError: (msg: string) => void) => { + const formObject = new FormData(); + formObject.append('code', code); + formObject.append('secret', props.secret); + fetch(loris.BaseURL + '/my_preferences/mfa', { + method: 'POST', + cache: 'no-cache', + credentials: 'same-origin', + body: formObject, + }).then( (resp) => { + if (resp.status !== 400 && !resp.ok) { + throw new Error('Bad server response'); + } + return resp.json(); + }).then( (json) => { + if (json.ok == 'success') { + swal.fire('Success!', json.message, 'success').then( () => { + window.location.href = loris.BaseURL + '/my_preferences/'; + }); + } else if (json.error) { + onError(json.error); + } else { + throw new Error('Unexpected JSON response'); + } + }).catch( (e: Error) => { + onError(e.message); + }); + }, [props.secret]); + return ( +
+

Validate Code

+ +
+ ); +} +/** + * + */ +function MFAIndex(): React.ReactElement { + const [showModal, setShowModal] = useState(false); + const [key] = useState(genPotentialSecret()); + const studyTitle = loris.config('studyTitle'); + const mfaUrl = 'otpauth://totp/' + + encodeURI(studyTitle) + + ':' + encodeURI(loris.user.username) + + '?secret=' + encodeURI(key) + + '&period=30&digits=6&issuer=' + encodeURI(studyTitle); + return
+ 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.

+

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

+ +

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

+ +
; +} + +window.addEventListener('load', () => { + /* + const MFAIndex = 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/php/mfa.class.inc b/modules/my_preferences/php/mfa.class.inc new file mode 100644 index 00000000000..bb308f7cab6 --- /dev/null +++ b/modules/my_preferences/php/mfa.class.inc @@ -0,0 +1,120 @@ +getMethod()) { + case 'GET': + return parent::handle($request); + case 'POST': + return $this->validateCodeAndSave( + $request->getAttribute("user"), + $request->getParsedBody() + ); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed(['GET', 'POST']); + + } + } + /** + * {@inheritDoc} + * + * @return array of javascript to be inserted + */ + function getJSDependencies() + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $deps = parent::getJSDependencies(); + return array_merge( + $deps, + [ + $baseURL . "/my_preferences/js/mfa.js", + ] + ); + } + + /** + * Validates the code passed by the user matches the secret key that they + * provided and save the secret key to the database if it matches + * + * @param \User $user The user providing the 2FA code + * @param array $values The parsed values submitted by the user + * + * @return ResponseInterface + */ + function validateCodeAndSave(\User $user, array $values): ResponseInterface + { + if (!isset($values['code']) || !isset($values['secret'])) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'Missing code or secret to validate' + ); + } + $base32Decoder = new Base32(); + $secret = $base32Decoder->decode($values['secret']); + $validator = new \LORIS\Security\OTP\TOTP(secret: $secret); + $counter = $validator->getTimeCounter(); + $wantCode = $validator->getCode($counter, 6); + if ($wantCode !== strval($values['code'])) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'Invalid code provided. MFA not registered.' + ); + } + $db = $this->loris->getDatabaseConnection(); + $db->_trackChanges = false; + // We are dealing with binary data that never gets exposed to the user + $db->unsafeUpdate( + "users", + ['TOTPSecret' => $secret], + ['ID' => $user->getId()] + ); + + $login = $_SESSION['State']->getProperty('login'); + $login->setPassedMFA(); + return new \LORIS\Http\Response\JSON\OK( + ['ok' => 'success', + 'message' => 'Successfully registered multifactor authenticator' + ] + ); + } +} + diff --git a/modules/my_preferences/php/my_preferences.class.inc b/modules/my_preferences/php/my_preferences.class.inc index 047676e72d2..aa20ea6b260 100644 --- a/modules/my_preferences/php/my_preferences.class.inc +++ b/modules/my_preferences/php/my_preferences.class.inc @@ -352,6 +352,7 @@ class My_Preferences extends \NDB_Form unset($nGroup); } } + $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 7a1534f6740..80e454af415 100644 --- a/modules/my_preferences/templates/form_my_preferences.tpl +++ b/modules/my_preferences/templates/form_my_preferences.tpl @@ -62,6 +62,17 @@ +
+
+ +
+