diff --git a/Makefile b/Makefile index 2bda9969e0a..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 @@ -198,3 +201,8 @@ server_processes_manager: modules/server_processes_manager/locale/ja/LC_MESSAGES conflict_resolver: target=conflict_resolver npm run compile + +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/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index 7fe98d1b067..ce79fbb9089 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 841af8656b4..5ea57eca61d 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 cb72c4768a5..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": "962186512cf6f4ef19d97b2738569257", + "content-hash": "a588887142a42bc949d04d77c3540042", "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,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.4", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-qrcode.git", + "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/390393e97a6e42ccae0e0d6205b8d4200f7ddc43", + "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43", + "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.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.23.0", + "squizlabs/php_codesniffer": "^4.0.0" + }, + "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": "2025-09-19T17:30:27+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 +431,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 +485,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 +595,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 +611,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 +632,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 +678,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 +694,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 +723,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 +794,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 +810,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 +844,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 +898,7 @@ "type": "community_bridge" } ], - "time": "2024-10-14T11:59:49+00:00" + "time": "2025-10-12T15:31:36+00:00" }, { "name": "mtdowling/jmespath.php", @@ -1343,18 +1501,66 @@ }, "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", + "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 +1611,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 +1645,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1458,7 +1670,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 +1686,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 +1751,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 +1762,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 +1856,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 +1917,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 +1927,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 +2219,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 +2267,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 +2275,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "netresearch/jsonmapper", @@ -2117,16 +2330,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 +2358,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -2169,22 +2382,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 +2408,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 +2462,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 +2590,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 +2647,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 +2871,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 +2920,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 +2960,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.3.x-dev" + "dev-main": "12.4.x-dev" } }, "autoload": { @@ -2781,7 +2989,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 +3009,7 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:58:13+00:00" + "time": "2025-09-24T13:44:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3266,16 +3474,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 +3495,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -3311,28 +3519,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 +3611,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 +3631,7 @@ "type": "tidelift" } ], - "time": "2025-06-17T07:41:58+00:00" + "time": "2025-08-20T11:27:00+00:00" }, { "name": "sebastian/complexity", @@ -3540,16 +3760,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 +3812,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 +3832,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 +3902,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 +3976,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 +4172,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 +4224,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 +4293,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 +4432,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 +4508,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 +4568,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 +4642,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 +4653,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 +4725,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 +4736,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 +4749,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 +4807,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 +4818,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 +4892,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 +4903,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 +4916,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 +4976,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 +4987,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 +5041,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 +5052,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 +5092,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4835,7 +5128,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 +5144,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 +5172,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 +5214,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 +5225,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 +5350,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 +5402,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 +5417,6 @@ "platform": { "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } 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/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/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/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/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..743e58ec08d --- /dev/null +++ b/modules/login/php/mfa.class.inc @@ -0,0 +1,125 @@ +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..c6cd6b7f6b6 --- /dev/null +++ b/modules/my_preferences/jsx/mfa.tsx @@ -0,0 +1,131 @@ +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'; +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; + +/** + * 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 {t} = useTranslation(); + 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({ + 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) { + onError(json.error); + } else { + throw new Error('Unexpected JSON response'); + } + }).catch( (e: Error) => { + onError(e.message); + }); + }, [props.secret]); + return ( +
+

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

+ +
+ ); +} +/** + * + */ +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) + + ':' + encodeURI(loris.user.username) + + '?secret=' + encodeURI(key) + + '&period=30&digits=6&issuer=' + encodeURI(studyTitle); + return
+ setShowModal(false)} + show={showModal} + throwWarning={false}> +

{{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'})}

+

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

+ +

setShowModal(true)} />]} />

+ +
; +} + +window.addEventListener('load', () => { + i18n.addResourceBundle('ja', 'my_preferences', jaStrings); + i18n.addResourceBundle('hi', 'my_preferences', hiStrings); + + 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/ja/LC_MESSAGES/my_preferences.po b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po new file mode 100644 index 00000000000..35212dfd974 --- /dev/null +++ b/modules/my_preferences/locale/ja/LC_MESSAGES/my_preferences.po @@ -0,0 +1,80 @@ +# 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 "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 コードをスキャンし、コードを入力して検証します。" + +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/locale/my_preferences.pot b/modules/my_preferences/locale/my_preferences.pot index b83fd0bb954..a7474d1c51c 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,31 @@ msgstr "" msgid "New and old passwords are identical" msgstr "" + +msgid "Configure multi-factor authentication (MFA)" +msgstr "" + +msgid "Configure MFA" +msgstr "" + +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 "" + diff --git a/modules/my_preferences/php/mfa.class.inc b/modules/my_preferences/php/mfa.class.inc new file mode 100644 index 00000000000..3e6981e3264 --- /dev/null +++ b/modules/my_preferences/php/mfa.class.inc @@ -0,0 +1,125 @@ +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' => dgettext( + 'my_preferences', + '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 f1ac6509250..fdbe7f05687 100644 --- a/modules/my_preferences/php/my_preferences.class.inc +++ b/modules/my_preferences/php/my_preferences.class.inc @@ -384,6 +384,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 437bf525ba9..a8146d1b35d 100644 --- a/modules/my_preferences/templates/form_my_preferences.tpl +++ b/modules/my_preferences/templates/form_my_preferences.tpl @@ -63,6 +63,17 @@ +
+
+ +
+