From 9d71e9e7ad6cabd78060bc0e8f2b93a6966619d7 Mon Sep 17 00:00:00 2001 From: "Sebastian L." Date: Sat, 1 Feb 2025 10:31:24 +0100 Subject: [PATCH 01/12] Add email with IMAP backend Signed-off-by: Sebastian L. --- lib/Base.php | 7 ++++++- lib/IMAP.php | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Base.php b/lib/Base.php index 5e27d68..410ccfb 100644 --- a/lib/Base.php +++ b/lib/Base.php @@ -173,7 +173,7 @@ public function setDisplayName($uid, $displayName) { * * @return void */ - protected function storeUser($uid, $groups = []) { + protected function storeUser($uid, $groups = [], $email = '') { if (!$this->userExists($uid)) { $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); $query->insert('users_external') @@ -189,6 +189,11 @@ protected function storeUser($uid, $groups = []) { \OC::$server->getGroupManager()->createGroup($group)->addUser($createduser); } } + + if ($email) { + $config = \OC::$server->getConfig(); + $config->setUserValue( $uid, 'settings', 'email', $email); + } } } diff --git a/lib/IMAP.php b/lib/IMAP.php index 2ad784f..abebc2d 100644 --- a/lib/IMAP.php +++ b/lib/IMAP.php @@ -61,6 +61,9 @@ public function checkPassword($uid, $password) { $uid = str_replace("%40", "@", $uid); } + // Get email if uid contains @ symbol + if ( str_contains( $uid, '@' ) ) $email = $uid; + $pieces = explode('@', $uid); if ($this->domain !== '') { if (count($pieces) === 1) { @@ -104,7 +107,7 @@ public function checkPassword($uid, $password) { if ($errorcode === 0) { curl_close($ch); $uid = mb_strtolower($uid); - $this->storeUser($uid, $groups); + $this->storeUser($uid, $groups, mb_strtolower($email)); return $uid; } elseif ($errorcode === CURLE_COULDNT_CONNECT || $errorcode === CURLE_SSL_CONNECT_ERROR || From f67a4f6408b6884f2c966e38dd1f73a787bcf2c9 Mon Sep 17 00:00:00 2001 From: Ralf Date: Mon, 28 Oct 2024 20:18:45 +0100 Subject: [PATCH 02/12] Make NC30 compatible Make the app compatible with Nextcloud 30 Signed-off-by: Ralf Signed-off-by: Sebastian L. --- appinfo/info.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 949c081..34cbe12 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to learn how to configure it! ]]> - 3.4.0 + 3.5.0 agpl Robin Appelman UserExternal @@ -33,6 +33,6 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l https://github.com/nextcloud/user_external/issues https://github.com/nextcloud/user_external.git - + From 567d62080ef56833ea3c1872279fb9e5bc017754 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 12 Aug 2025 15:00:06 +0200 Subject: [PATCH 03/12] Update README.md Signed-off-by: Anna Signed-off-by: Sebastian L. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e4114dc..271ab6f 100644 --- a/README.md +++ b/README.md @@ -225,4 +225,3 @@ Alternatives Other extensions allow connecting to external user databases directly via SQL, which may be faster: * [user_sql](https://github.com/nextcloud/user_sql) -* [user_backend_sql_raw](https://github.com/PanCakeConnaisseur/user_backend_sql_raw) From a664f21b4d4618e9bd0b226644698b3e2b3ce369 Mon Sep 17 00:00:00 2001 From: hschletz Date: Tue, 22 Apr 2025 17:51:32 +0200 Subject: [PATCH 04/12] Set up logger instance in base constructor. Fixes #270 Signed-off-by: Holger Schletz Signed-off-by: Sebastian L. --- lib/Base.php | 5 +++++ lib/BasicAuth.php | 8 ++++---- lib/FTP.php | 2 +- lib/IMAP.php | 8 ++++---- lib/SMB.php | 4 ++-- lib/SSH.php | 2 +- lib/WebDavAuth.php | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/Base.php b/lib/Base.php index 410ccfb..a619754 100644 --- a/lib/Base.php +++ b/lib/Base.php @@ -9,6 +9,9 @@ */ namespace OCA\UserExternal; +use OCP\Server; +use Psr\Log\LoggerInterface; + /** * Base class for external auth implementations that stores users * on their first login in a local table. @@ -23,6 +26,7 @@ */ abstract class Base extends \OC\User\Backend { protected $backend = ''; + protected readonly LoggerInterface $logger; /** * Create new instance, set backend name @@ -31,6 +35,7 @@ abstract class Base extends \OC\User\Backend { */ public function __construct($backend) { $this->backend = $backend; + $this->logger = Server::get(LoggerInterface::class); } /** diff --git a/lib/BasicAuth.php b/lib/BasicAuth.php index fee7a06..f198059 100644 --- a/lib/BasicAuth.php +++ b/lib/BasicAuth.php @@ -37,14 +37,14 @@ public function checkPassword($uid, $password) { ); $canary = get_headers($this->authUrl, 1, $context); if (!$canary) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl, ['app' => 'user_external'] ); return false; } if (!isset(array_change_key_case($canary, CASE_LOWER)['www-authenticate'])) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Mis-configured BasicAuth Url: '.$this->authUrl.', provided URL does not do authentication!', ['app' => 'user_external'] ); @@ -61,7 +61,7 @@ public function checkPassword($uid, $password) { $headers = get_headers($this->authUrl, 1, $context); if (!$headers) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl, ['app' => 'user_external'] ); @@ -82,7 +82,7 @@ public function checkPassword($uid, $password) { $this->storeUser($uid); return $uid; case "3": - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Too many redirects from BasicAuth Url: '.$this->authUrl, ['app' => 'user_external'] ); diff --git a/lib/FTP.php b/lib/FTP.php index 1aa7e38..c1c95e5 100644 --- a/lib/FTP.php +++ b/lib/FTP.php @@ -48,7 +48,7 @@ public function __construct($host, $secure = false) { */ public function checkPassword($uid, $password) { if (false === array_search($this->protocol, stream_get_wrappers())) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Stream wrapper not available: ' . $this->protocol, ['app' => 'user_external'] ); diff --git a/lib/IMAP.php b/lib/IMAP.php index abebc2d..9a15d5b 100644 --- a/lib/IMAP.php +++ b/lib/IMAP.php @@ -74,7 +74,7 @@ public function checkPassword($uid, $password) { $uid = $pieces[0]; } } else { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: User has a wrong domain! Expecting: '.$this->domain, ['app' => 'user_external'] ); @@ -114,7 +114,7 @@ public function checkPassword($uid, $password) { $errorcode === 28) { # This is not defined in PHP-8.x # 28: CURLE_OPERATION_TIMEDOUT - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: Could not connect to imap server via curl: ' . curl_strerror($errorcode), ['app' => 'user_external'] ); @@ -125,12 +125,12 @@ public function checkPassword($uid, $password) { # 9: CURLE_REMOTE_ACCESS_DENIED # 67: CURLE_LOGIN_DENIED # 94: CURLE_AUTH_ERROR) - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: IMAP Login failed via curl: ' . curl_strerror($errorcode), ['app' => 'user_external'] ); } else { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: IMAP server returned an error: ' . $errorcode . ' / ' . curl_strerror($errorcode), ['app' => 'user_external'] ); diff --git a/lib/SMB.php b/lib/SMB.php index ec6e7e7..ac4bda9 100644 --- a/lib/SMB.php +++ b/lib/SMB.php @@ -43,7 +43,7 @@ private function tryAuthentication($uid, $password) { $command = self::SMBCLIENT.' '.escapeshellarg('//' . $this->host . '/dummy').' -U '.$uidEscaped.'%'.$password; $lastline = exec($command, $output, $retval); if ($retval === 127) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: smbclient executable missing', ['app' => 'user_external'] ); @@ -56,7 +56,7 @@ private function tryAuthentication($uid, $password) { goto login; } elseif ($retval !== 0) { //some other error - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: smbclient error: ' . trim($lastline), ['app' => 'user_external'] ); diff --git a/lib/SSH.php b/lib/SSH.php index 0a95a3b..ea5b09f 100644 --- a/lib/SSH.php +++ b/lib/SSH.php @@ -44,7 +44,7 @@ public function __construct($host, $port = 22) { */ public function checkPassword($uid, $password) { if (!extension_loaded('ssh2')) { - \OC::$server->getLogger()->error( + $this->logger->error( 'ERROR: php-ssh2 PECL module missing', ['app' => 'user_external'] ); diff --git a/lib/WebDavAuth.php b/lib/WebDavAuth.php index 73c0556..92876ca 100644 --- a/lib/WebDavAuth.php +++ b/lib/WebDavAuth.php @@ -27,14 +27,14 @@ public function __construct($webDavAuthUrl) { public function checkPassword($uid, $password) { $arr = explode('://', $this->webDavAuthUrl, 2); if (! isset($arr) or count($arr) !== 2) { - \OC::$server->getLogger()->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']); + $this->logger->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']); return false; } list($protocol, $path) = $arr; $url = $protocol.'://'.urlencode($uid).':'.urlencode($password).'@'.$path; $headers = get_headers($url); if ($headers === false) { - \OC::$server->getLogger()->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']); + $this->logger->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']); return false; } $returnCode = substr($headers[0], 9, 3); From c2b4dd7969064ec6a635d3f2970ae3b4b5a07114 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Fri, 20 Mar 2026 01:08:03 +0100 Subject: [PATCH 05/12] Add NC31/32 compatibility and rewrite test suite - Bump version to 3.6.0, set NC compatibility to min=31 max=32 - Replace deprecated OC::$server service locators with constructor-injected IDBConnection, IUserManager, IGroupManager (optional params for BC) - Replace $query->execute() with executeQuery()/executeStatement() and fetchColumn() with fetchOne() (NC32 DB API changes) - Add resolveUid() to Base: maps email login names back to stored uids for NC32 email-to-uid resolution; call it in all checkPassword() impls - Remove dead old tests (wrong class names, removed NC APIs, never ran) - Add PHPUnit 11 test infrastructure via vendor-bin pattern - Add 19 unit tests for Base with mocks (tests/unit/BaseTest.php) - Add 14 integration tests against live NC DB (tests/integration/) - Align bootstrap, phpunit.xml and composer scripts with NC app conventions Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- .gitignore | 2 + appinfo/info.xml | 4 +- composer.json | 40 +- composer.lock | 4224 +-------------------- lib/Base.php | 108 +- lib/BasicAuth.php | 37 +- lib/FTP.php | 9 +- lib/IMAP.php | 33 +- lib/SMB.php | 7 +- lib/SSH.php | 5 +- lib/WebDavAuth.php | 13 +- lib/XMPP.php | 25 +- tests/basic_auth.php | 35 - tests/bootstrap.php | 15 +- tests/config.php | 35 - tests/configuration.xml | 30 - tests/ftp.php | 35 - tests/imap.php | 36 - tests/integration/BaseIntegrationTest.php | 249 ++ tests/phpunit.xml | 25 + tests/smb.php | 36 - tests/unit/BaseTest.php | 353 ++ vendor-bin/phpunit/composer.json | 11 + vendor-bin/phpunit/composer.lock | 1691 +++++++++ 24 files changed, 2607 insertions(+), 4451 deletions(-) delete mode 100644 tests/basic_auth.php delete mode 100644 tests/config.php delete mode 100644 tests/configuration.xml delete mode 100644 tests/ftp.php delete mode 100644 tests/imap.php create mode 100644 tests/integration/BaseIntegrationTest.php create mode 100644 tests/phpunit.xml delete mode 100644 tests/smb.php create mode 100644 tests/unit/BaseTest.php create mode 100644 vendor-bin/phpunit/composer.json create mode 100644 vendor-bin/phpunit/composer.lock diff --git a/.gitignore b/.gitignore index 35ba9ea..ee35d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ tests/clover.xml # packaged app build/artifacts vendor +vendor-bin/*/vendor .php-cs-fixer.cache +tests/.phpunit.cache diff --git a/appinfo/info.xml b/appinfo/info.xml index 34cbe12..bb2b166 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to learn how to configure it! ]]> - 3.5.0 + 3.6.0 agpl Robin Appelman UserExternal @@ -33,6 +33,6 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l https://github.com/nextcloud/user_external/issues https://github.com/nextcloud/user_external.git - + diff --git a/composer.json b/composer.json index faae7b3..be6e075 100644 --- a/composer.json +++ b/composer.json @@ -2,19 +2,45 @@ "name": "nextcloud/user_external", "config": { "optimize-autoloader": true, - "classmap-authoritative": true, "platform": { - "php": "7.3" + "php": "8.1" + }, + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "autoload": { + "psr-4": { + "OCA\\UserExternal\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "OCA\\UserExternal\\Tests\\": "tests/" } }, "scripts": { - "cs:fix": "php-cs-fixer fix", + "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -print0 | xargs -0 -n1 php -l", "cs:check": "php-cs-fixer fix --dry-run --diff", - "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l" + "cs:fix": "php-cs-fixer fix", + "test:unit": "phpunit -c tests/phpunit.xml --testsuite 'user_external unit tests' --color --fail-on-warning --fail-on-risky", + "test:integration": "phpunit -c tests/phpunit.xml --testsuite 'user_external integration tests' --color --fail-on-warning --fail-on-risky", + "post-install-cmd": [ + "[ $COMPOSER_DEV_MODE -eq 0 ] || composer bin all install --ansi" + ], + "post-update-cmd": [ + "[ $COMPOSER_DEV_MODE -eq 0 ] || composer bin all update --ansi" + ] }, "require-dev": { - "nextcloud/coding-standard": "^1.0.0", - "phpunit/phpunit": "^9.5", - "christophwurst/nextcloud_testing": "^0.12.4" + "bamarni/composer-bin-plugin": "^1.8", + "nextcloud/coding-standard": "^1.0.0" + }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } } } diff --git a/composer.lock b/composer.lock index df57c57..871752e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,375 +4,93 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "873fdd7ab6fbab5d42b54237eec0d841", + "content-hash": "1ef9cbfed89c36428913363131d90400", "packages": [], "packages-dev": [ { - "name": "christophwurst/nextcloud_testing", - "version": "v0.12.4", + "name": "bamarni/composer-bin-plugin", + "version": "1.9.1", "source": { "type": "git", - "url": "https://github.com/ChristophWurst/nextcloud_testing.git", - "reference": "9c189b01dbcc3508108f08c417de6aaea7005fb0" + "url": "https://github.com/bamarni/composer-bin-plugin.git", + "reference": "641d0663f5ac270b1aeec4337b7856f76204df47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ChristophWurst/nextcloud_testing/zipball/9c189b01dbcc3508108f08c417de6aaea7005fb0", - "reference": "9c189b01dbcc3508108f08c417de6aaea7005fb0", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/641d0663f5ac270b1aeec4337b7856f76204df47", + "reference": "641d0663f5ac270b1aeec4337b7856f76204df47", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "php-webdriver/webdriver": "^1.9", - "phpunit/phpunit": "^8.0|^9.0" + "composer-plugin-api": "^2.0", + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "christophwurst/nextcloud": "^17.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "ChristophWurst\\Nextcloud\\Testing\\": "/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christoph Wurst", - "email": "christoph@winzerhof-wurst.at" - } - ], - "description": "Simple and fast unit and integration testing framework for Nextcloud, based on PHPUnit", - "support": { - "issues": "https://github.com/ChristophWurst/nextcloud_testing/issues", - "source": "https://github.com/ChristophWurst/nextcloud_testing/tree/v0.12.4" - }, - "time": "2021-02-18T08:41:09+00:00" - }, - { - "name": "composer/pcre", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.1" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-01-21T20:24:37+00:00" - }, - { - "name": "composer/semver", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "composer/composer": "^2.2.26", + "ext-json": "*", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8 || ^2.0", + "phpstan/phpstan-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.0", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" }, - "type": "library", + "type": "composer-plugin", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-04-01T19:23:25+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", - "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", - "shasum": "" - }, - "require": { - "composer/pcre": "^1", - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" }, - "type": "library", "autoload": { "psr-4": { - "Composer\\XdebugHandler\\": "src" + "Bamarni\\Composer\\Bin\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", + "description": "No conflicts for your bin dependencies", "keywords": [ - "Xdebug", - "performance" + "composer", + "conflict", + "dependency", + "executable", + "isolation", + "tool" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.9.1" }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-02-24T20:20:32+00:00" + "time": "2026-02-04T10:18:12+00:00" }, { - "name": "doctrine/annotations", - "version": "1.13.2", + "name": "kubawerlos/php-cs-fixer-custom-fixers", + "version": "v3.36.1", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git", + "reference": "ec9776e80f5ea9bf0d8ec16d662e431bfab19a24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/ec9776e80f5ea9bf0d8ec16d662e431bfab19a24", + "reference": "ec9776e80f5ea9bf0d8ec16d662e431bfab19a24", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", + "ext-filter": "*", "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" - }, - "time": "2021-08-05T19:00:23+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" + "friendsofphp/php-cs-fixer": "^3.87", + "php": "^7.4 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.44" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "PhpCsFixerCustomFixers\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -381,64 +99,46 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Kuba Werłos", + "email": "werlos@gmail.com" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], + "description": "A set of custom fixers for PHP CS Fixer", "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues", + "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.36.1" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://github.com/kubawerlos", + "type": "github" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2026-03-07T11:35:13+00:00" }, { - "name": "doctrine/lexer", - "version": "1.2.3", + "name": "nextcloud/coding-standard", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + "url": "https://github.com/nextcloud/coding-standard.git", + "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/8e06808c1423e9208d63d1bd205b9a38bd400011", + "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" + "kubawerlos/php-cs-fixer-custom-fixers": "^3.22", + "php": "^8.0", + "php-cs-fixer/shim": "^3.17" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Nextcloud\\CodingStandard\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -447,109 +147,51 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" } ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "description": "Nextcloud coding standards for the php cs fixer", "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" + "dev" ], "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" + "issues": "https://github.com/nextcloud/coding-standard/issues", + "source": "https://github.com/nextcloud/coding-standard/tree/v1.4.0" }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2022-02-28T11:07:21+00:00" + "time": "2025-06-19T12:27:27+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.4.0", + "name": "php-cs-fixer/shim", + "version": "v3.94.2", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad" + "url": "https://github.com/PHP-CS-Fixer/shim.git", + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1", + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1", "shasum": "" }, "require": { - "composer/semver": "^3.2", - "composer/xdebug-handler": "^2.0", - "doctrine/annotations": "^1.12", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.2.5 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", - "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", - "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", - "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23", - "symfony/process": "^4.4.20 || ^5.0 || ^6.0", - "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" + "php": "^7.4 || ^8.0" }, - "require-dev": { - "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.8", - "php-coveralls/php-coveralls": "^2.5.2", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^5.2.4 || ^6.0", - "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" + "replace": { + "friendsofphp/php-cs-fixer": "self.version" }, "suggest": { "ext-dom": "For handling output formats in XML", "ext-mbstring": "For handling non-UTF8 characters." }, "bin": [ - "php-cs-fixer" + "php-cs-fixer", + "php-cs-fixer.phar" ], "type": "application", - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -566,3715 +208,21 @@ ], "description": "A tool to automatically fix PHP code style", "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2021-12-11T16:25:08+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2022-03-03T13:19:32+00:00" - }, - { - "name": "nextcloud/coding-standard", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/nextcloud/coding-standard.git", - "reference": "f3d1f9375e89c605deb1734f59a9f51ecbe80578" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/f3d1f9375e89c605deb1734f59a9f51ecbe80578", - "reference": "f3d1f9375e89c605deb1734f59a9f51ecbe80578", - "shasum": "" - }, - "require": { - "friendsofphp/php-cs-fixer": "^3.2", - "php": "^7.3|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Nextcloud\\CodingStandard\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christoph Wurst", - "email": "christoph@winzerhof-wurst.at" - } - ], - "description": "Nextcloud coding standards for the php cs fixer", - "support": { - "issues": "https://github.com/nextcloud/coding-standard/issues", - "source": "https://github.com/nextcloud/coding-standard/tree/v1.0.0" - }, - "time": "2021-11-10T08:44:10+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.13.2", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" - }, - "time": "2021-11-30T19:35:32+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" - }, - "time": "2021-07-20T11:28:43+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "php-cs-fixer/diff", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "sebastian/diff v3 backport support for PHP 5.6+", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" - }, - "time": "2020-10-14T08:32:19+00:00" - }, - { - "name": "php-webdriver/webdriver", - "version": "1.12.0", - "source": { - "type": "git", - "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "99d4856ed7dffcdf6a52eccd6551e83d8d557ceb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/99d4856ed7dffcdf6a52eccd6551e83d8d557ceb", - "reference": "99d4856ed7dffcdf6a52eccd6551e83d8d557ceb", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "php": "^5.6 || ~7.0 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0 || ^6.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "ondram/ci-detector": "^2.1 || ^3.5 || ^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^1.1 || ^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0 || ^6.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Exception/TimeoutException.php" - ], - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.12.0" - }, - "time": "2021-10-14T09:30:02+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, - "time": "2022-03-15T21:29:03+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "9.2.15", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-03-07T09:28:20+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-12-02T12:48:52+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "5.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:16:10+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "9.5.20", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.3.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", - "sebastian/version": "^3.0.2" - }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.5-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-04-01T12:37:26+00:00" - }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/master" - }, - "time": "2016-08-06T20:24:11+00:00" - }, - { - "name": "psr/container", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" - }, - "time": "2021-03-05T17:36:06+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:08:49+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" - }, - { - "name": "sebastian/comparator", - "version": "4.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:49:45+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" - }, - { - "name": "sebastian/diff", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:10:38+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-04-03T09:37:03+00:00" - }, - { - "name": "sebastian/exporter", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-11-11T14:18:36+00:00" - }, - { - "name": "sebastian/global-state", - "version": "5.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-02-14T08:28:10+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:42:11+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:12:34+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:14:26+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:17:30+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:45:17+00:00" - }, - { - "name": "sebastian/type", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-03-15T09:54:48+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.7", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/900275254f0a1a2afff1ab0e11abd5587a10e1d6", - "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.7" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-03-31T17:09:19+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "dec8a9f58d20df252b9cd89f1c6c1530f747685d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dec8a9f58d20df252b9cd89f1c6c1530f747685d", - "reference": "dec8a9f58d20df252b9cd89f1c6c1530f747685d", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/event-dispatcher": "^1" - }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v5.4.7", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3a4442138d80c9f7b600fb297534ac718b61d37f", - "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.7" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-04-01T12:33:59+00:00" - }, - { - "name": "symfony/finder", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-26T16:34:36+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cc1147cb11af1b43f503ac18f31aa3bec213aba8", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-10-20T20:35:02+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-11-23T21:10:46+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-02-19T12:13:01+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-11-30T18:21:41+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-06-05T21:20:04+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-09-13T13:58:11+00:00" - }, - { - "name": "symfony/process", - "version": "v5.4.46", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.46" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-06T09:18:28+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v2.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", - "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-03-13T20:07:29+00:00" - }, - { - "name": "symfony/stopwatch", - "version": "v5.4.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", - "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1|^2|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a way to profile code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-02-18T16:06:09+00:00" - }, - { - "name": "symfony/string", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" - }, - "conflict": { - "symfony/translation-contracts": ">=3.0" - }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "issues": "https://github.com/PHP-CS-Fixer/shim/issues", + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.94.2" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2026-02-20T16:14:17+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "platform-overrides": { - "php": "7.3" + "php": "8.1" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/lib/Base.php b/lib/Base.php index a619754..a518a30 100644 --- a/lib/Base.php +++ b/lib/Base.php @@ -1,4 +1,5 @@ * @author Christian Weiske @@ -9,6 +10,9 @@ */ namespace OCA\UserExternal; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; use OCP\Server; use Psr\Log\LoggerInterface; @@ -25,17 +29,32 @@ * @link http://github.com/owncloud/apps */ abstract class Base extends \OC\User\Backend { - protected $backend = ''; + protected string $backend = ''; protected readonly LoggerInterface $logger; + private readonly IDBConnection $db; + private readonly IUserManager $userManager; + private readonly IGroupManager $groupManager; /** - * Create new instance, set backend name + * Create new instance, set backend name. + * + * Dependencies are optional to allow backends to be instantiated with just + * a backend string (as before), while still being injectable for testing. * * @param string $backend Identifier of the backend */ - public function __construct($backend) { + public function __construct( + string $backend, + ?IDBConnection $db = null, + ?IUserManager $userManager = null, + ?IGroupManager $groupManager = null, + ?LoggerInterface $logger = null, + ) { $this->backend = $backend; - $this->logger = Server::get(LoggerInterface::class); + $this->logger = $logger ?? Server::get(LoggerInterface::class); + $this->db = $db ?? Server::get(IDBConnection::class); + $this->userManager = $userManager ?? Server::get(IUserManager::class); + $this->groupManager = $groupManager ?? Server::get(IGroupManager::class); } /** @@ -46,11 +65,11 @@ public function __construct($backend) { * @return bool */ public function deleteUser($uid) { - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->delete('users_external') ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); - $query->execute(); + $query->executeStatement(); return true; } @@ -62,12 +81,12 @@ public function deleteUser($uid) { * @return string display name */ public function getDisplayName($uid) { - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select('displayname') ->from('users_external') ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); - $result = $query->execute(); + $result = $query->executeQuery(); $user = $result->fetch(); $result->closeCursor(); @@ -85,12 +104,11 @@ public function getDisplayName($uid) { * @return array with all displayNames (value) and the corresponding uids (key) */ public function getDisplayNames($search = '', $limit = null, $offset = null) { - $connection = \OC::$server->getDatabaseConnection(); - $query = $connection->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select('uid', 'displayname') ->from('users_external') - ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $connection->escapeLikeParameter($search) . '%'))) - ->orWhere($query->expr()->iLike('uid', $query->createNamedParameter('%' . $connection->escapeLikeParameter($search) . '%'))) + ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%'))) + ->orWhere($query->expr()->iLike('uid', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%'))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); if ($limit) { $query->setMaxResults($limit); @@ -98,7 +116,7 @@ public function getDisplayNames($search = '', $limit = null, $offset = null) { if ($offset) { $query->setFirstResult($offset); } - $result = $query->execute(); + $result = $query->executeQuery(); $displayNames = []; while ($row = $result->fetch()) { @@ -115,11 +133,10 @@ public function getDisplayNames($search = '', $limit = null, $offset = null) { * @return array with all uids */ public function getUsers($search = '', $limit = null, $offset = null) { - $connection = \OC::$server->getDatabaseConnection(); - $query = $connection->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select('uid') ->from('users_external') - ->where($query->expr()->iLike('uid', $query->createNamedParameter($connection->escapeLikeParameter($search) . '%'))) + ->where($query->expr()->iLike('uid', $query->createNamedParameter($this->db->escapeLikeParameter($search) . '%'))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); if ($limit) { $query->setMaxResults($limit); @@ -127,7 +144,7 @@ public function getUsers($search = '', $limit = null, $offset = null) { if ($offset) { $query->setFirstResult($offset); } - $result = $query->execute(); + $result = $query->executeQuery(); $users = []; while ($row = $result->fetch()) { @@ -150,7 +167,7 @@ public function hasUserListings() { /** * Change the display name of a user * - * @param string $uid The username + * @param string $uid The username * @param string $displayName The new display name * * @return true/false @@ -160,12 +177,12 @@ public function setDisplayName($uid, $displayName) { return false; } - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->update('users_external') ->set('displayname', $query->createNamedParameter($displayName)) ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); - $query->execute(); + $query->executeStatement(); return true; } @@ -180,18 +197,18 @@ public function setDisplayName($uid, $displayName) { */ protected function storeUser($uid, $groups = [], $email = '') { if (!$this->userExists($uid)) { - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->insert('users_external') ->values([ 'uid' => $query->createNamedParameter($uid), 'backend' => $query->createNamedParameter($this->backend), ]); - $query->execute(); + $query->executeStatement(); if ($groups) { - $createduser = \OC::$server->getUserManager()->get($uid); + $createduser = $this->userManager->get($uid); foreach ($groups as $group) { - \OC::$server->getGroupManager()->createGroup($group)->addUser($createduser); + $this->groupManager->createGroup($group)->addUser($createduser); } } @@ -210,14 +227,13 @@ protected function storeUser($uid, $groups = [], $email = '') { * @return boolean */ public function userExists($uid) { - $connection = \OC::$server->getDatabaseConnection(); - $query = $connection->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('*', 'num_users')) ->from('users_external') - ->where($query->expr()->iLike('uid', $query->createNamedParameter($connection->escapeLikeParameter($uid)))) + ->where($query->expr()->iLike('uid', $query->createNamedParameter($this->db->escapeLikeParameter($uid)))) ->andWhere($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); - $result = $query->execute(); - $users = $result->fetchColumn(); + $result = $query->executeQuery(); + $users = $result->fetchOne(); $result->closeCursor(); return $users > 0; @@ -229,15 +245,41 @@ public function userExists($uid) { * @return int the number of users */ public function countUsers() { - $connection = \OC::$server->getDatabaseConnection(); - $query = $connection->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('*', 'num_users')) ->from('users_external') ->where($query->expr()->eq('backend', $query->createNamedParameter($this->backend))); - $result = $query->execute(); - $users = $result->fetchColumn(); + $result = $query->executeQuery(); + $users = $result->fetchOne(); $result->closeCursor(); return $users; } + + /** + * Resolve a login name to a uid stored in this backend. + * + * Since Nextcloud 32, when a user logs in with their account email address, + * the email is passed directly to checkPassword() instead of the uid. + * This method maps the email back to the uid stored in users_external so + * that backends can authenticate correctly regardless of how the user logged in. + * + * @param string $loginName The login name (uid or email address) + * @return string The resolved uid, or the original loginName if no match is found + */ + protected function resolveUid(string $loginName): string { + if (strpos($loginName, '@') === false) { + return $loginName; + } + + $users = $this->userManager->getByEmail($loginName); + foreach ($users as $user) { + $uid = $user->getUID(); + if ($this->userExists($uid)) { + return $uid; + } + } + + return $loginName; + } } diff --git a/lib/BasicAuth.php b/lib/BasicAuth.php index f198059..06a14f4 100644 --- a/lib/BasicAuth.php +++ b/lib/BasicAuth.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -19,50 +20,52 @@ public function __construct($authUrl) { /** * Check if the password is correct without logging in the user * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { + $uid = $this->resolveUid($uid); + /* * Connect without user/name password to make sure * URL is indeed authenticating or not... */ - $context = stream_context_create(array( - 'http' => array( - 'method' => "GET", + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', 'follow_location' => 0 - )) + ]] ); $canary = get_headers($this->authUrl, 1, $context); if (!$canary) { $this->logger->error( - 'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl, + 'ERROR: Not possible to connect to BasicAuth Url: ' . $this->authUrl, ['app' => 'user_external'] ); return false; } if (!isset(array_change_key_case($canary, CASE_LOWER)['www-authenticate'])) { $this->logger->error( - 'ERROR: Mis-configured BasicAuth Url: '.$this->authUrl.', provided URL does not do authentication!', + 'ERROR: Mis-configured BasicAuth Url: ' . $this->authUrl . ', provided URL does not do authentication!', ['app' => 'user_external'] ); return false; } - $context = stream_context_create(array( - 'http' => array( - 'method' => "GET", - 'header' => "authorization: Basic " . base64_encode("$uid:$password"), + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => 'authorization: Basic ' . base64_encode("$uid:$password"), 'follow_location' => 0 - )) + ]] ); $headers = get_headers($this->authUrl, 1, $context); if (!$headers) { $this->logger->error( - 'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl, + 'ERROR: Not possible to connect to BasicAuth Url: ' . $this->authUrl, ['app' => 'user_external'] ); return false; @@ -73,17 +76,17 @@ public function checkPassword($uid, $password) { */ $responseIdx = 0; foreach (array_keys($headers) as $key) { - if (gettype($key) === "integer" && $responseIdx < $key) { + if (gettype($key) === 'integer' && $responseIdx < $key) { $responseIdx = $key; } } switch (substr($headers[$responseIdx], 9, 1)) { - case "2": + case '2': $this->storeUser($uid); return $uid; - case "3": + case '3': $this->logger->error( - 'ERROR: Too many redirects from BasicAuth Url: '.$this->authUrl, + 'ERROR: Too many redirects from BasicAuth Url: ' . $this->authUrl, ['app' => 'user_external'] ); return false; diff --git a/lib/FTP.php b/lib/FTP.php index c1c95e5..757bc42 100644 --- a/lib/FTP.php +++ b/lib/FTP.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -25,7 +26,7 @@ class FTP extends Base { /** * Create new FTP authentication provider * - * @param string $host Hostname or IP of FTP server + * @param string $host Hostname or IP of FTP server * @param boolean $secure TRUE to enable SSL */ public function __construct($host, $secure = false) { @@ -41,13 +42,15 @@ public function __construct($host, $secure = false) { /** * Check if the password is correct without logging in the user * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { - if (false === array_search($this->protocol, stream_get_wrappers())) { + $uid = $this->resolveUid($uid); + + if (array_search($this->protocol, stream_get_wrappers()) === false) { $this->logger->error( 'ERROR: Stream wrapper not available: ' . $this->protocol, ['app' => 'user_external'] diff --git a/lib/IMAP.php b/lib/IMAP.php index 9a15d5b..e2aa574 100644 --- a/lib/IMAP.php +++ b/lib/IMAP.php @@ -1,4 +1,5 @@ * @author Jonas Sulzer @@ -32,7 +33,7 @@ class IMAP extends Base { * @param string $mailbox IMAP server domain/IP * @param int $port IMAP server $port * @param string $sslmode - * @param string $domain If provided, loging will be restricted to this domain + * @param string $domain If provided, loging will be restricted to this domain * @param boolean $stripeDomain (whether to stripe the domain part from the username or not) * @param boolean $groupDomain (whether to add the usere to a group corresponding to the domain of the address) */ @@ -49,16 +50,18 @@ public function __construct($mailbox, $port = null, $sslmode = null, $domain = n /** * Check if the password is correct without logging in the user * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { + $uid = $this->resolveUid($uid); + // Replace escaped @ symbol in uid (which is a mail address) // but only if there is no @ symbol and if there is a %40 inside the uid if (!(strpos($uid, '@') !== false) && (strpos($uid, '%40') !== false)) { - $uid = str_replace("%40", "@", $uid); + $uid = str_replace('%40', '@', $uid); } // Get email if uid contains @ symbol @@ -75,7 +78,7 @@ public function checkPassword($uid, $password) { } } else { $this->logger->error( - 'ERROR: User has a wrong domain! Expecting: '.$this->domain, + 'ERROR: User has a wrong domain! Expecting: ' . $this->domain, ['app' => 'user_external'] ); return false; @@ -89,7 +92,7 @@ public function checkPassword($uid, $password) { $groups[] = $pieces[1]; } - $protocol = ($this->sslmode === "ssl") ? "imaps" : "imap"; + $protocol = ($this->sslmode === 'ssl') ? 'imaps' : 'imap'; $url = "{$protocol}://{$this->mailbox}:{$this->port}"; $ch = curl_init(); if ($this->sslmode === 'tls') { @@ -97,7 +100,7 @@ public function checkPassword($uid, $password) { } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_USERPWD, $username.":".$password); + curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $password); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'CAPABILITY'); @@ -109,29 +112,29 @@ public function checkPassword($uid, $password) { $uid = mb_strtolower($uid); $this->storeUser($uid, $groups, mb_strtolower($email)); return $uid; - } elseif ($errorcode === CURLE_COULDNT_CONNECT || - $errorcode === CURLE_SSL_CONNECT_ERROR || - $errorcode === 28) { + } elseif ($errorcode === CURLE_COULDNT_CONNECT + || $errorcode === CURLE_SSL_CONNECT_ERROR + || $errorcode === 28) { # This is not defined in PHP-8.x # 28: CURLE_OPERATION_TIMEDOUT $this->logger->error( - 'ERROR: Could not connect to imap server via curl: ' . curl_strerror($errorcode), + 'ERROR: Could not connect to imap server via curl: ' . curl_strerror($errorcode), ['app' => 'user_external'] ); - } elseif ($errorcode === 9 || - $errorcode === 67 || - $errorcode === 94) { + } elseif ($errorcode === 9 + || $errorcode === 67 + || $errorcode === 94) { # These are not defined in PHP-8.x # 9: CURLE_REMOTE_ACCESS_DENIED # 67: CURLE_LOGIN_DENIED # 94: CURLE_AUTH_ERROR) $this->logger->error( - 'ERROR: IMAP Login failed via curl: ' . curl_strerror($errorcode), + 'ERROR: IMAP Login failed via curl: ' . curl_strerror($errorcode), ['app' => 'user_external'] ); } else { $this->logger->error( - 'ERROR: IMAP server returned an error: ' . $errorcode . ' / ' . curl_strerror($errorcode), + 'ERROR: IMAP server returned an error: ' . $errorcode . ' / ' . curl_strerror($errorcode), ['app' => 'user_external'] ); } diff --git a/lib/SMB.php b/lib/SMB.php index ac4bda9..aa04201 100644 --- a/lib/SMB.php +++ b/lib/SMB.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -40,7 +41,7 @@ public function __construct($host) { private function tryAuthentication($uid, $password) { $uidEscaped = escapeshellarg($uid); $password = escapeshellarg($password); - $command = self::SMBCLIENT.' '.escapeshellarg('//' . $this->host . '/dummy').' -U '.$uidEscaped.'%'.$password; + $command = self::SMBCLIENT . ' ' . escapeshellarg('//' . $this->host . '/dummy') . ' -U ' . $uidEscaped . '%' . $password; $lastline = exec($command, $output, $retval); if ($retval === 127) { $this->logger->error( @@ -70,12 +71,14 @@ private function tryAuthentication($uid, $password) { /** * Check if the password is correct without logging in the user * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { + $uid = $this->resolveUid($uid); + // Check with an invalid password, if the user authenticates then fail $attemptWithInvalidPassword = $this->tryAuthentication($uid, base64_encode($password)); if (is_string($attemptWithInvalidPassword)) { diff --git a/lib/SSH.php b/lib/SSH.php index ea5b09f..7024ee0 100644 --- a/lib/SSH.php +++ b/lib/SSH.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -37,12 +38,14 @@ public function __construct($host, $port = 22) { * Check if the password is correct without logging in * Requires the php-ssh2 pecl extension * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { + $uid = $this->resolveUid($uid); + if (!extension_loaded('ssh2')) { $this->logger->error( 'ERROR: php-ssh2 PECL module missing', diff --git a/lib/WebDavAuth.php b/lib/WebDavAuth.php index 92876ca..ab4cc72 100644 --- a/lib/WebDavAuth.php +++ b/lib/WebDavAuth.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -19,22 +20,24 @@ public function __construct($webDavAuthUrl) { /** * Check if the password is correct without logging in the user * - * @param string $uid The username + * @param string $uid The username * @param string $password The password * * @return true/false */ public function checkPassword($uid, $password) { + $uid = $this->resolveUid($uid); + $arr = explode('://', $this->webDavAuthUrl, 2); if (! isset($arr) or count($arr) !== 2) { - $this->logger->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']); + $this->logger->error('ERROR: Invalid WebdavUrl: "' . $this->webDavAuthUrl . '" ', ['app' => 'user_external']); return false; } - list($protocol, $path) = $arr; - $url = $protocol.'://'.urlencode($uid).':'.urlencode($password).'@'.$path; + [$protocol, $path] = $arr; + $url = $protocol . '://' . urlencode($uid) . ':' . urlencode($password) . '@' . $path; $headers = get_headers($url); if ($headers === false) { - $this->logger->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']); + $this->logger->error('ERROR: Not possible to connect to WebDAV Url: "' . $protocol . '://' . $path . '" ', ['app' => 'user_external']); return false; } $returnCode = substr($headers[0], 9, 3); diff --git a/lib/XMPP.php b/lib/XMPP.php index 50c6dbe..fb128a1 100644 --- a/lib/XMPP.php +++ b/lib/XMPP.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -48,18 +49,18 @@ public function hmacSha1($key, $data) { $oPad[$i] = $oPad[$i] ^ $key[$i]; $iPad[$i] = $iPad[$i] ^ $key[$i]; } - return sha1($oPad.sha1($iPad.$data, true)); + return sha1($oPad . sha1($iPad . $data, true)); } - + public function validateHashedPassword($user, $uid, $submittedPassword) { foreach ($user as $key) { - if ($key[3] === "salt") { + if ($key[3] === 'salt') { $internalSalt = $key['value']; } - if ($key[3] === "server_key") { + if ($key[3] === 'server_key') { $internalServerKey = $key['value']; } - if ($key[3] === "stored_key") { + if ($key[3] === 'stored_key') { $internalStoredKey = $key['value']; } } @@ -82,7 +83,7 @@ public function validateHashedPassword($user, $uid, $submittedPassword) { public function validatePlainPassword($user, $uid, $submittedPassword) { foreach ($user as $key) { - if ($key[3] === "password") { + if ($key[3] === 'password') { $internalPlainPassword = $key['value']; } } @@ -95,7 +96,7 @@ public function validatePlainPassword($user, $uid, $submittedPassword) { return false; } } - + public function checkPassword($uid, $password) { $pdo = new \PDO("mysql:host=$this->host;dbname=$this->xmppDb", $this->xmppDbUser, $this->xmppDbPassword); if (isset($uid) @@ -103,22 +104,22 @@ public function checkPassword($uid, $password) { if (!filter_var($uid, FILTER_VALIDATE_EMAIL) || !strpos($uid, $this->xmppDomain) || substr($uid, -strlen($this->xmppDomain)) !== $this->xmppDomain - ) { + ) { return false; } - $user = explode("@", $uid); + $user = explode('@', $uid); $userName = strtolower($user[0]); $submittedPassword = $password; $statement = $pdo->prepare("SELECT * FROM prosody WHERE user = :user AND host = :xmppDomain AND store = 'accounts'"); - $result = $statement->execute(array( + $result = $statement->execute([ 'user' => $userName, 'xmppDomain' => $this->xmppDomain - )); + ]); $user = $statement->fetchAll(); if (empty($user)) { return false; } - + if ($this->passwordHashed === true) { return $this->validateHashedPassword($user, $uid, $submittedPassword); } else { diff --git a/tests/basic_auth.php b/tests/basic_auth.php deleted file mode 100644 index e4156f2..0000000 --- a/tests/basic_auth.php +++ /dev/null @@ -1,35 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -class Test_User_BasicAuth extends \Test\TestCase { - /** - * @var OC_User_BasicAuth $instance - */ - private $instance; - - private function getConfig() { - return include(__DIR__.'/config.php'); - } - - public function skip() { - $config = $this->getConfig(); - $this->skipUnless($config['basic_auth']['run']); - } - - protected function setUp() { - parent::setUp(); - $config = $this->getConfig(); - $this->instance = new OC_User_BasicAuth($config['basic_auth']['url']); - } - - public function testLogin() { - $config = $this->getConfig(); - $this->assertEquals($config['basic_auth']['user'], $this->instance->checkPassword($config['basic_auth']['user'], $config['basic_auth']['password'])); - $this->assertFalse($this->instance->checkPassword($config['basic_auth']['user'], $config['basic_auth']['password'].'foo')); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3c21384..f51c740 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,16 +1,13 @@ addPsr4('OCA\\UserExternal\\', __DIR__ . '/../lib/', true); +\OC::$composerAutoloader->addPsr4('OCA\\UserExternal\\Tests\\', __DIR__ . '/', true); diff --git a/tests/config.php b/tests/config.php deleted file mode 100644 index 3cfadba..0000000 --- a/tests/config.php +++ /dev/null @@ -1,35 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -OC_App::loadApp('user_external'); -return array( - 'imap' => array( - 'run' => false, - 'mailbox' => '{imap.gmail.com:993/imap/ssl}INBOX', //see http://php.net/manual/en/function.imap-open.php - 'user' => 'foo',//valid username/password combination - 'password' => 'bar', - ), - 'smb' => array( - 'run' => false, - 'host' => 'localhost', - 'user' => 'test',//valid username/password combination - 'password' => 'test', - ), - 'ftp' => array( - 'run' => false, - 'host' => 'localhost', - 'user' => 'test',//valid username/password combination - 'password' => 'test', - ), - 'basic_auth' => array( - 'run' => false, - 'url' => 'localhost/basic_auth', - 'user' => 'test',//valid username/password combination - 'password' => 'test', - ), -); diff --git a/tests/configuration.xml b/tests/configuration.xml deleted file mode 100644 index 083ba86..0000000 --- a/tests/configuration.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - . - config.php - - - - - - - ../../user_external - - ../../user_external/l10n - ../../user_external/tests - - - - - - - - - diff --git a/tests/ftp.php b/tests/ftp.php deleted file mode 100644 index 7afc288..0000000 --- a/tests/ftp.php +++ /dev/null @@ -1,35 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -class Test_User_FTP extends \Test\TestCase { - /** - * @var OC_User_IMAP $instance - */ - private $instance; - - private function getConfig() { - return include(__DIR__.'/config.php'); - } - - public function skip() { - $config = $this->getConfig(); - $this->skipUnless($config['ftp']['run']); - } - - protected function setUp() { - parent::setUp(); - $config = $this->getConfig(); - $this->instance = new OC_User_FTP($config['ftp']['host']); - } - - public function testLogin() { - $config = $this->getConfig(); - $this->assertEquals($config['ftp']['user'], $this->instance->checkPassword($config['ftp']['user'], $config['ftp']['password'])); - $this->assertFalse($this->instance->checkPassword($config['ftp']['user'], $config['ftp']['password'].'foo')); - } -} diff --git a/tests/imap.php b/tests/imap.php deleted file mode 100644 index 162cabf..0000000 --- a/tests/imap.php +++ /dev/null @@ -1,36 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -class Test_User_Imap extends \Test\TestCase { - /** - * @var OC_User_IMAP $instance - */ - private $instance; - - private function getConfig() { - return include(__DIR__.'/config.php'); - } - - public function skip() { - $config = $this->getConfig(); - $this->skipUnless($config['imap']['run']); - } - - protected function setUp() { - parent::setUp(); - - $config = $this->getConfig(); - $this->instance = new OC_User_IMAP($config['imap']['mailbox']); - } - - public function testLogin() { - $config = $this->getConfig(); - $this->assertEquals($config['imap']['user'], $this->instance->checkPassword($config['imap']['user'], $config['imap']['password'])); - $this->assertFalse($this->instance->checkPassword($config['imap']['user'], $config['imap']['password'].'foo')); - } -} diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php new file mode 100644 index 0000000..865bfb4 --- /dev/null +++ b/tests/integration/BaseIntegrationTest.php @@ -0,0 +1,249 @@ +setUpUserTrait(); + + $this->backendName = 'integration-test-' . uniqid(); + $this->backend = new ConcreteBase($this->backendName); + $this->db = Server::get(IDBConnection::class); + $this->config = Server::get(IConfig::class); + } + + protected function tearDown(): void { + $this->cleanupBackend(); + $this->tearDownUserTrait(); + parent::tearDown(); + } + + private function cleanupBackend(): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('users_external') + ->where($qb->expr()->eq('backend', $qb->createNamedParameter($this->backendName))); + $qb->executeStatement(); + } + + private function storeUser(string $uid, array $groups = []): void { + $method = new \ReflectionMethod(Base::class, 'storeUser'); + $method->invoke($this->backend, $uid, $groups); + } + + private function resolveUid(string $loginName): string { + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + return $method->invoke($this->backend, $loginName); + } + + // ------------------------------------------------------------------------- + // storeUser / userExists + // ------------------------------------------------------------------------- + + public function testStoreUserCreatesRecord(): void { + $this->assertFalse($this->backend->userExists('alice')); + + $this->storeUser('alice'); + + $this->assertTrue($this->backend->userExists('alice')); + } + + public function testStoreUserIsIdempotent(): void { + $this->storeUser('alice'); + $this->storeUser('alice'); // second call must not throw or duplicate + + $this->assertSame(1, $this->backend->countUsers()); + } + + // ------------------------------------------------------------------------- + // deleteUser + // ------------------------------------------------------------------------- + + public function testDeleteUserRemovesRecord(): void { + $this->storeUser('alice'); + $this->assertTrue($this->backend->userExists('alice')); + + $this->backend->deleteUser('alice'); + + $this->assertFalse($this->backend->userExists('alice')); + } + + public function testDeleteUserReturnsTrueEvenIfUserMissing(): void { + $this->assertTrue($this->backend->deleteUser('nonexistent')); + } + + // ------------------------------------------------------------------------- + // countUsers + // ------------------------------------------------------------------------- + + public function testCountUsersReflectsStoredUsers(): void { + $this->assertSame(0, $this->backend->countUsers()); + + $this->storeUser('alice'); + $this->assertSame(1, $this->backend->countUsers()); + + $this->storeUser('bob'); + $this->assertSame(2, $this->backend->countUsers()); + + $this->backend->deleteUser('alice'); + $this->assertSame(1, $this->backend->countUsers()); + } + + // ------------------------------------------------------------------------- + // getDisplayName / setDisplayName + // ------------------------------------------------------------------------- + + public function testGetDisplayNameFallsBackToUidBeforeSet(): void { + $this->storeUser('alice'); + + $this->assertSame('alice', $this->backend->getDisplayName('alice')); + } + + public function testSetAndGetDisplayName(): void { + $this->storeUser('alice'); + + $this->backend->setDisplayName('alice', 'Alice Wonderland'); + + $this->assertSame('Alice Wonderland', $this->backend->getDisplayName('alice')); + } + + public function testSetDisplayNameReturnsFalseForUnknownUser(): void { + $this->assertFalse($this->backend->setDisplayName('ghost', 'Ghost')); + } + + // ------------------------------------------------------------------------- + // getUsers + // ------------------------------------------------------------------------- + + public function testGetUsersReturnsStoredUids(): void { + $this->storeUser('alice'); + $this->storeUser('bob'); + + $users = $this->backend->getUsers(); + sort($users); + + $this->assertSame(['alice', 'bob'], $users); + } + + public function testGetUsersSearchFilters(): void { + $this->storeUser('alice'); + $this->storeUser('alex'); + $this->storeUser('bob'); + + $users = $this->backend->getUsers('al'); + sort($users); + + $this->assertSame(['alex', 'alice'], $users); + } + + public function testGetUsersRespectsLimit(): void { + $this->storeUser('alice'); + $this->storeUser('bob'); + $this->storeUser('charlie'); + + $users = $this->backend->getUsers('', 2); + + $this->assertCount(2, $users); + } + + // ------------------------------------------------------------------------- + // getDisplayNames + // ------------------------------------------------------------------------- + + public function testGetDisplayNamesReturnsMap(): void { + $this->storeUser('alice'); + $this->storeUser('bob'); + $this->backend->setDisplayName('alice', 'Alice A'); + $this->backend->setDisplayName('bob', 'Bob B'); + + $names = $this->backend->getDisplayNames(); + + $this->assertSame('Alice A', $names['alice']); + $this->assertSame('Bob B', $names['bob']); + } + + // ------------------------------------------------------------------------- + // resolveUid — NC32 email login handling + // ------------------------------------------------------------------------- + + public function testResolveUidReturnsPlainUsernameUnchanged(): void { + $this->assertSame('alice', $this->resolveUid('alice')); + } + + public function testResolveUidReturnsEmailWhenNoMatchFound(): void { + $result = $this->resolveUid('unknown@example.com'); + + $this->assertSame('unknown@example.com', $result); + } + + public function testResolveUidResolvesEmailToUidForKnownUser(): void { + // Create a real NC user via UserTrait's dummy backend + $this->createUser('alice', 'password'); + // Set their NC account email (what IUserManager::getByEmail() searches) + $this->config->setUserValue('alice', 'settings', 'email', 'alice@example.com'); + // Register alice in this external backend + $this->storeUser('alice'); + + $result = $this->resolveUid('alice@example.com'); + + $this->assertSame('alice', $result); + } + + public function testResolveUidReturnsEmailWhenUserNotInThisBackend(): void { + // NC user exists with that email, but they are NOT in our users_external backend + $this->createUser('alice', 'password'); + $this->config->setUserValue('alice', 'settings', 'email', 'alice@example.com'); + // Note: no storeUser() call + + $result = $this->resolveUid('alice@example.com'); + + // Falls back to the email since alice isn't in this backend + $this->assertSame('alice@example.com', $result); + } + + public function testResolveUidPicksCorrectUserWhenMultipleShareEmail(): void { + // Two NC users with the same email (edge case); only bob is in this backend + $this->createUser('alice', 'password'); + $this->createUser('bob', 'password'); + $this->config->setUserValue('alice', 'settings', 'email', 'shared@example.com'); + $this->config->setUserValue('bob', 'settings', 'email', 'shared@example.com'); + $this->storeUser('bob'); + + $result = $this->resolveUid('shared@example.com'); + + $this->assertSame('bob', $result); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 0000000..48aba70 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,25 @@ + + + + + + unit + + + integration + + + + + + ../lib + + + + diff --git a/tests/smb.php b/tests/smb.php deleted file mode 100644 index 9e6cdb1..0000000 --- a/tests/smb.php +++ /dev/null @@ -1,36 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -class Test_User_SMB extends \Test\TestCase { - /** - * @var OC_User_IMAP $instance - */ - private $instance; - - private function getConfig() { - return include(__DIR__.'/config.php'); - } - - public function skip() { - $config = $this->getConfig(); - $this->skipUnless($config['smb']['run']); - } - - protected function setUp() { - parent::setUp(); - - $config = $this->getConfig(); - $this->instance = new OC_User_SMB($config['smb']['host']); - } - - public function testLogin() { - $config = $this->getConfig(); - $this->assertEquals($config['smb']['user'], $this->instance->checkPassword($config['smb']['user'], $config['smb']['password'])); - $this->assertFalse($this->instance->checkPassword($config['smb']['user'], $config['smb']['password'].'foo')); - } -} diff --git a/tests/unit/BaseTest.php b/tests/unit/BaseTest.php new file mode 100644 index 0000000..4e10370 --- /dev/null +++ b/tests/unit/BaseTest.php @@ -0,0 +1,353 @@ +db = $this->createMock(IDBConnection::class); + $this->db->method('escapeLikeParameter')->willReturnArgument(0); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->backend = new ConcreteBase( + 'test-backend', + $this->db, + $this->userManager, + $this->groupManager, + $this->logger, + ); + } + + /** + * Returns a fully configured fluent IQueryBuilder mock. + * All fluent methods return $this; executeQuery/executeStatement must be configured per-test. + */ + private function mockQueryBuilder(): MockObject&IQueryBuilder { + $expr = $this->createMock(IExpressionBuilder::class); + $expr->method('eq')->willReturn('1=1'); + $expr->method('iLike')->willReturn('1=1'); + + $queryFunction = $this->createMock(IQueryFunction::class); + $funcBuilder = $this->createMock(IFunctionBuilder::class); + $funcBuilder->method('count')->willReturn($queryFunction); + + $qb = $this->createMock(IQueryBuilder::class); + $qb->method('expr')->willReturn($expr); + $qb->method('func')->willReturn($funcBuilder); + $qb->method('select')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('orWhere')->willReturnSelf(); + $qb->method('delete')->willReturnSelf(); + $qb->method('update')->willReturnSelf(); + $qb->method('insert')->willReturnSelf(); + $qb->method('values')->willReturnSelf(); + $qb->method('set')->willReturnSelf(); + $qb->method('setMaxResults')->willReturnSelf(); + $qb->method('setFirstResult')->willReturnSelf(); + $qb->method('createNamedParameter')->willReturnArgument(0); + + return $qb; + } + + /** Returns a result mock that reports a given user count. */ + private function mockCountResult(int $count): MockObject&IResult { + $result = $this->createMock(IResult::class); + $result->method('fetchOne')->willReturn($count); + $result->method('closeCursor')->willReturn(true); + return $result; + } + + // ------------------------------------------------------------------------- + // resolveUid + // ------------------------------------------------------------------------- + + public function testResolveUidPlainUsernameIsReturnedUnchanged(): void { + $this->userManager->expects($this->never())->method('getByEmail'); + + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + $result = $method->invoke($this->backend, 'john'); + + $this->assertSame('john', $result); + } + + public function testResolveUidEmailNotFoundInNcReturnsEmail(): void { + $this->userManager->method('getByEmail')->with('john@example.com')->willReturn([]); + + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + $result = $method->invoke($this->backend, 'john@example.com'); + + $this->assertSame('john@example.com', $result); + } + + public function testResolveUidEmailFoundInBackendReturnsUid(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('john'); + $this->userManager->method('getByEmail')->with('john@example.com')->willReturn([$user]); + + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(1)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + $result = $method->invoke($this->backend, 'john@example.com'); + + $this->assertSame('john', $result); + } + + public function testResolveUidEmailFoundInNcButNotThisBackendReturnsEmail(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('john'); + $this->userManager->method('getByEmail')->with('john@example.com')->willReturn([$user]); + + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(0)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + $result = $method->invoke($this->backend, 'john@example.com'); + + $this->assertSame('john@example.com', $result); + } + + public function testResolveUidPicksFirstMatchInThisBackend(): void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID')->willReturn('alice'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('bob'); + $this->userManager->method('getByEmail')->willReturn([$user1, $user2]); + + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturnOnConsecutiveCalls( + $this->mockCountResult(0), // alice → not found + $this->mockCountResult(1), // bob → found + ); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $method = new \ReflectionMethod(Base::class, 'resolveUid'); + $result = $method->invoke($this->backend, 'shared@example.com'); + + $this->assertSame('bob', $result); + } + + // ------------------------------------------------------------------------- + // userExists + // ------------------------------------------------------------------------- + + public function testUserExistsReturnsTrueWhenFound(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(1)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertTrue($this->backend->userExists('john')); + } + + public function testUserExistsReturnsFalseWhenNotFound(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(0)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertFalse($this->backend->userExists('john')); + } + + // ------------------------------------------------------------------------- + // deleteUser + // ------------------------------------------------------------------------- + + public function testDeleteUserExecutesDeleteAndReturnsTrue(): void { + $qb = $this->mockQueryBuilder(); + $qb->expects($this->once())->method('delete')->with('users_external')->willReturnSelf(); + $qb->expects($this->once())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertTrue($this->backend->deleteUser('john')); + } + + // ------------------------------------------------------------------------- + // countUsers + // ------------------------------------------------------------------------- + + public function testCountUsersReturnsCount(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetchOne')->willReturn(42); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame(42, $this->backend->countUsers()); + } + + // ------------------------------------------------------------------------- + // getDisplayName + // ------------------------------------------------------------------------- + + public function testGetDisplayNameReturnsStoredDisplayName(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetch')->willReturn(['displayname' => 'John Doe']); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('John Doe', $this->backend->getDisplayName('john')); + } + + public function testGetDisplayNameFallsBackToUidWhenEmpty(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetch')->willReturn(['displayname' => '']); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('john', $this->backend->getDisplayName('john')); + } + + public function testGetDisplayNameFallsBackToUidWhenNull(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetch')->willReturn(['displayname' => null]); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('john', $this->backend->getDisplayName('john')); + } + + // ------------------------------------------------------------------------- + // setDisplayName + // ------------------------------------------------------------------------- + + public function testSetDisplayNameReturnsFalseForUnknownUser(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(0)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertFalse($this->backend->setDisplayName('john', 'John Doe')); + } + + public function testSetDisplayNameUpdatesAndReturnsTrue(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(1)); + $qb->expects($this->once())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertTrue($this->backend->setDisplayName('john', 'John Doe')); + } + + // ------------------------------------------------------------------------- + // storeUser + // ------------------------------------------------------------------------- + + public function testStoreUserInsertsWhenUserIsNew(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(0)); + $qb->expects($this->once())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $method = new \ReflectionMethod(Base::class, 'storeUser'); + $method->invoke($this->backend, 'john'); + } + + public function testStoreUserSkipsInsertWhenUserAlreadyExists(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(1)); + $qb->expects($this->never())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $method = new \ReflectionMethod(Base::class, 'storeUser'); + $method->invoke($this->backend, 'john'); + } + + public function testStoreUserAddsGroupsOnFirstLogin(): void { + $qb = $this->mockQueryBuilder(); + $qb->method('executeQuery')->willReturn($this->mockCountResult(0)); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once())->method('get')->with('john')->willReturn($user); + + $group = $this->createMock(\OCP\IGroup::class); + $group->expects($this->once())->method('addUser')->with($user); + $this->groupManager->expects($this->once())->method('createGroup')->with('editors')->willReturn($group); + + $method = new \ReflectionMethod(Base::class, 'storeUser'); + $method->invoke($this->backend, 'john', ['editors']); + } + + // ------------------------------------------------------------------------- + // getUsers + // ------------------------------------------------------------------------- + + public function testGetUsersReturnsUidList(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetch')->willReturnOnConsecutiveCalls( + ['uid' => 'alice'], + ['uid' => 'bob'], + false, + ); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame(['alice', 'bob'], $this->backend->getUsers()); + } + + // ------------------------------------------------------------------------- + // getDisplayNames + // ------------------------------------------------------------------------- + + public function testGetDisplayNamesReturnsMap(): void { + $qb = $this->mockQueryBuilder(); + $result = $this->createMock(IResult::class); + $result->method('fetch')->willReturnOnConsecutiveCalls( + ['uid' => 'alice', 'displayname' => 'Alice A'], + ['uid' => 'bob', 'displayname' => 'Bob B'], + false, + ); + $result->expects($this->once())->method('closeCursor'); + $qb->method('executeQuery')->willReturn($result); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame( + ['alice' => 'Alice A', 'bob' => 'Bob B'], + $this->backend->getDisplayNames(), + ); + } +} diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json new file mode 100644 index 0000000..964bf28 --- /dev/null +++ b/vendor-bin/phpunit/composer.json @@ -0,0 +1,11 @@ +{ + "config": { + "sort-packages": true, + "platform": { + "php": "8.1" + } + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + } +} diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock new file mode 100644 index 0000000..66efdef --- /dev/null +++ b/vendor-bin/phpunit/composer.lock @@ -0,0 +1,1691 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "fb78960ff7e774a72d424270c4bd3d90", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "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/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "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/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "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": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "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-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "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/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "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/5.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-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.1" + }, + "plugin-api-version": "2.9.0" +} From b75613791ae0cb934eb888e0f770aafac5b8ea91 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Fri, 20 Mar 2026 01:15:24 +0100 Subject: [PATCH 06/12] Add PHPUnit CI workflows for SQLite, MySQL, MariaDB, PostgreSQL Each workflow uses the nextcloud-version-matrix action to test against all supported NC/PHP version combinations, runs both unit and integration test suites via composer scripts, and includes a path-filter to skip unchanged code. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- .github/workflows/phpunit-mariadb.yml | 190 ++++++++++++++++++++++++++ .github/workflows/phpunit-mysql.yml | 188 +++++++++++++++++++++++++ .github/workflows/phpunit-pgsql.yml | 186 +++++++++++++++++++++++++ .github/workflows/phpunit-sqlite.yml | 175 ++++++++++++++++++++++++ 4 files changed, 739 insertions(+) create mode 100644 .github/workflows/phpunit-mariadb.yml create mode 100644 .github/workflows/phpunit-mysql.yml create mode 100644 .github/workflows/phpunit-pgsql.yml create mode 100644 .github/workflows/phpunit-sqlite.yml diff --git a/.github/workflows/phpunit-mariadb.yml b/.github/workflows/phpunit-mariadb.yml new file mode 100644 index 0000000..2f5af5d --- /dev/null +++ b/.github/workflows/phpunit-mariadb.yml @@ -0,0 +1,190 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit MariaDB + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-mariadb-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + php-version: ${{ steps.versions.outputs.php-available-list }} + server-max: ${{ steps.versions.outputs.branches-max-list }} + steps: + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 + + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'tests/**' + - 'vendor-bin/**' + - 'composer.json' + - 'composer.lock' + + phpunit-mariadb: + runs-on: ubuntu-latest + + needs: [changes, matrix] + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: + php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} + server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + mariadb-versions: ['10.6', '11.4'] + + name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + + services: + mariadb: + image: ghcr.io/nextcloud/continuous-integration-mariadb-${{ matrix.mariadb-versions }}:latest # zizmor: ignore[unpinned-images] + ports: + - 4444:3306/tcp + env: + MARIADB_ROOT_PASSWORD: rootpassword + options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5 + + steps: + - name: Set app env + if: ${{ env.APP_NAME == '' }} + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql + coverage: none + ini-file: development + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable ONLY_FULL_GROUP_BY MariaDB option + run: | + echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + echo 'SELECT @@sql_mode;' | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up dependencies + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: composer install + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Check PHPUnit script is defined + id: check_phpunit + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:unit ' | wc -l | grep 1 + + - name: PHPUnit + if: steps.check_phpunit.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:unit + + - name: Check PHPUnit integration script is defined + id: check_integration + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:integration ' | wc -l | grep 1 + + - name: Run Nextcloud + if: steps.check_integration.outcome == 'success' + run: php -S localhost:8080 & + + - name: PHPUnit integration + if: steps.check_integration.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:integration + + - name: Print logs + if: always() + run: | + cat data/nextcloud.log + + - name: Skipped + if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' + run: | + echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' + exit 1 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, phpunit-mariadb] + + if: always() + + name: phpunit-mariadb-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-mariadb.result != 'success' }}; then exit 1; fi diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml new file mode 100644 index 0000000..026b1eb --- /dev/null +++ b/.github/workflows/phpunit-mysql.yml @@ -0,0 +1,188 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit MySQL + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-mysql-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + matrix: ${{ steps.versions.outputs.sparse-matrix }} + steps: + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 + with: + matrix: '{"mysql-versions": ["8.4"]}' + + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'tests/**' + - 'vendor-bin/**' + - 'composer.json' + - 'composer.lock' + + phpunit-mysql: + runs-on: ubuntu-latest + + needs: [changes, matrix] + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + + name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + + services: + mysql: + image: ghcr.io/nextcloud/continuous-integration-mysql-${{ matrix.mysql-versions }}:latest # zizmor: ignore[unpinned-images] + ports: + - 4444:3306/tcp + env: + MYSQL_ROOT_PASSWORD: rootpassword + options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 + + steps: + - name: Set app env + if: ${{ env.APP_NAME == '' }} + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql + coverage: none + ini-file: development + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable ONLY_FULL_GROUP_BY MySQL option + run: | + echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + echo 'SELECT @@sql_mode;' | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up dependencies + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: composer install + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Check PHPUnit script is defined + id: check_phpunit + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:unit ' | wc -l | grep 1 + + - name: PHPUnit + if: steps.check_phpunit.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:unit + + - name: Check PHPUnit integration script is defined + id: check_integration + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:integration ' | wc -l | grep 1 + + - name: Run Nextcloud + if: steps.check_integration.outcome == 'success' + run: php -S localhost:8080 & + + - name: PHPUnit integration + if: steps.check_integration.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:integration + + - name: Print logs + if: always() + run: | + cat data/nextcloud.log + + - name: Skipped + if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' + run: | + echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' + exit 1 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, phpunit-mysql] + + if: always() + + name: phpunit-mysql-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-mysql.result != 'success' }}; then exit 1; fi diff --git a/.github/workflows/phpunit-pgsql.yml b/.github/workflows/phpunit-pgsql.yml new file mode 100644 index 0000000..fc9700e --- /dev/null +++ b/.github/workflows/phpunit-pgsql.yml @@ -0,0 +1,186 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit PostgreSQL + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-pgsql-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + php-version: ${{ steps.versions.outputs.php-available-list }} + server-max: ${{ steps.versions.outputs.branches-max-list }} + steps: + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 + + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src }} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'tests/**' + - 'vendor-bin/**' + - 'composer.json' + - 'composer.lock' + + phpunit-pgsql: + runs-on: ubuntu-latest + + needs: [changes, matrix] + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: + php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} + server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + + name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + + services: + postgres: + image: ghcr.io/nextcloud/continuous-integration-postgres-16:latest # zizmor: ignore[unpinned-images] + ports: + - 4444:5432/tcp + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: rootpassword + POSTGRES_DB: nextcloud + options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 + + steps: + - name: Set app env + if: ${{ env.APP_NAME == '' }} + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql + coverage: none + ini-file: development + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up dependencies + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: composer install + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Check PHPUnit script is defined + id: check_phpunit + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:unit ' | wc -l | grep 1 + + - name: PHPUnit + if: steps.check_phpunit.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:unit + + - name: Check PHPUnit integration script is defined + id: check_integration + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:integration ' | wc -l | grep 1 + + - name: Run Nextcloud + if: steps.check_integration.outcome == 'success' + run: php -S localhost:8080 & + + - name: PHPUnit integration + if: steps.check_integration.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:integration + + - name: Print logs + if: always() + run: | + cat data/nextcloud.log + + - name: Skipped + if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' + run: | + echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' + exit 1 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, phpunit-pgsql] + + if: always() + + name: phpunit-pgsql-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-pgsql.result != 'success' }}; then exit 1; fi diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml new file mode 100644 index 0000000..c405617 --- /dev/null +++ b/.github/workflows/phpunit-sqlite.yml @@ -0,0 +1,175 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit SQLite + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-sqlite-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + php-version: ${{ steps.versions.outputs.php-available-list }} + server-max: ${{ steps.versions.outputs.branches-max-list }} + steps: + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 + + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'tests/**' + - 'vendor-bin/**' + - 'composer.json' + - 'composer.lock' + + phpunit-sqlite: + runs-on: ubuntu-latest + + needs: [changes, matrix] + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: + php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} + server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + + name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + + steps: + - name: Set app env + if: ${{ env.APP_NAME == '' }} + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up dependencies + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: composer install + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Check PHPUnit script is defined + id: check_phpunit + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:unit ' | wc -l | grep 1 + + - name: PHPUnit + if: steps.check_phpunit.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:unit + + - name: Check PHPUnit integration script is defined + id: check_integration + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep '^ test:integration ' | wc -l | grep 1 + + - name: Run Nextcloud + if: steps.check_integration.outcome == 'success' + run: php -S localhost:8080 & + + - name: PHPUnit integration + if: steps.check_integration.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:integration + + - name: Print logs + if: always() + run: | + cat data/nextcloud.log + + - name: Skipped + if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' + run: | + echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' + exit 1 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, phpunit-sqlite] + + if: always() + + name: phpunit-sqlite-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi From fbfcc270f0f07516eb2eb49c31d04bcaff30be93 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Mon, 23 Mar 2026 09:49:20 +0100 Subject: [PATCH 07/12] fix: app is also compatible with 33 and upcoming 34 Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index bb2b166..deb4b6a 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -33,6 +33,6 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l https://github.com/nextcloud/user_external/issues https://github.com/nextcloud/user_external.git - + From d08de773d8346fffe7d76ab68f6abae18c2d9e11 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 25 Mar 2026 20:33:58 +0100 Subject: [PATCH 08/12] chore: prepare Makefile for release Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 36d3eef..a6a88fe 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,12 @@ appstore: --exclude=/.scrutinizer.yml \ --exclude=/.travis.yml \ --exclude=/Makefile \ + --exclude=/node_modules \ + --exclude=/src \ + --exclude=/package.json \ + --exclude=/package-lock.json \ + --exclude=/tsconfig.json \ + --exclude=/vite.config.ts \ $(project_dir)/ $(sign_dir)/$(app_name) @if [ -f $(cert_dir)/$(app_name).key ]; then \ echo "Signing app files…"; \ From 1d674573319bf4af491ad4bc773ff77f525dd98d Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Fri, 20 Mar 2026 01:19:43 +0100 Subject: [PATCH 09/12] ci: add conventional commits enforcement workflow AI-assisted-by: Claude Sonnet 4.6 Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- .../block-unconventional-commits.yml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/block-unconventional-commits.yml diff --git a/.github/workflows/block-unconventional-commits.yml b/.github/workflows/block-unconventional-commits.yml new file mode 100644 index 0000000..01c4dea --- /dev/null +++ b/.github/workflows/block-unconventional-commits.yml @@ -0,0 +1,36 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Block unconventional commits + +on: + pull_request: + types: [opened, ready_for_review, reopened, synchronize] + +permissions: + contents: read + +concurrency: + group: block-unconventional-commits-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + block-unconventional-commits: + name: Block unconventional commits + + runs-on: ubuntu-latest-low + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: webiny/action-conventional-commits@faccb24fc2550dd15c0390d944379d2d8ed9690e # v1.3.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9bff8f743755ad64d363da4677008f77947a850a Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 25 Mar 2026 20:49:20 +0100 Subject: [PATCH 10/12] chore: update workflows Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- .github/workflows/block-merge-eol.yml | 58 +++++++++++++++++++++++++++ .github/workflows/fixup.yml | 14 +++---- .github/workflows/lint-info-xml.yml | 6 ++- .github/workflows/lint-php-cs.yml | 26 ++++++------ .github/workflows/lint-php.yml | 48 +++++++++++----------- 5 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/block-merge-eol.yml diff --git a/.github/workflows/block-merge-eol.yml b/.github/workflows/block-merge-eol.yml new file mode 100644 index 0000000..4c217e3 --- /dev/null +++ b/.github/workflows/block-merge-eol.yml @@ -0,0 +1,58 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Block merge to EOL + +on: pull_request + +permissions: + contents: read + +concurrency: + group: block-merge-eol-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + block-eol: + name: Block merge to EOL + runs-on: ubuntu-latest-low + steps: + - name: Parse base branch version + id: parse + run: | + branch="${{ github.base_ref }}" + if [[ $branch =~ ^stable([0-9]+)$ ]]; then + echo "major=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + fi + + - name: Get current date + if: ${{ !steps.parse.outputs.skip }} + id: date + run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Fetch EOL data + if: ${{ !steps.parse.outputs.skip }} + id: eol + run: | + data=$(curl -s https://raw.githubusercontent.com/nextcloud-releases/updater_server/production/config/major_versions.json) + eol=$(echo "$data" | jq -r ".[] | select(.majorVersion == ${{ steps.parse.outputs.major }}) | .eol") + echo "eol=$eol" >> $GITHUB_OUTPUT + + - name: Check EOL date + if: ${{ !steps.parse.outputs.skip }} + run: | + eol_date="${{ steps.eol.outputs.eol }}" + current_date="${{ steps.date.outputs.date }}" + if [[ "$eol_date" != "" && "$eol_date" != "null" ]]; then + if [[ "$eol_date" < "$current_date" || "$eol_date" == "$current_date" ]]; then + echo "This branch is EOL (End-of-Life) since $eol_date" + exit 1 + fi + fi diff --git a/.github/workflows/fixup.yml b/.github/workflows/fixup.yml index 69da2bb..dbaecdc 100644 --- a/.github/workflows/fixup.yml +++ b/.github/workflows/fixup.yml @@ -14,23 +14,19 @@ on: permissions: contents: read + pull-requests: write concurrency: group: fixup-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: - commit-message-check: - if: github.event.pull_request.draft == false - - permissions: - pull-requests: write - name: Block fixup and squash commits - + block-fixup-merge: runs-on: ubuntu-latest-low - + if: ${{ !github.event.pull_request.draft }} + name: block-fixup-merge steps: - - name: Run check + - name: Block fixup and squash commits uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-info-xml.yml b/.github/workflows/lint-info-xml.yml index 736a895..5da422a 100644 --- a/.github/workflows/lint-info-xml.yml +++ b/.github/workflows/lint-info-xml.yml @@ -3,7 +3,7 @@ # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization # -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT name: Lint info.xml @@ -24,7 +24,9 @@ jobs: name: info.xml lint steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Download schema run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd diff --git a/.github/workflows/lint-php-cs.yml b/.github/workflows/lint-php-cs.yml index b6f1232..e253a33 100644 --- a/.github/workflows/lint-php-cs.yml +++ b/.github/workflows/lint-php-cs.yml @@ -6,7 +6,7 @@ # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT -name: Lint php-cs +name: Lint code style on: pull_request @@ -20,29 +20,29 @@ concurrency: jobs: lint: runs-on: ubuntu-latest - - name: php-cs - + name: lint steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Get php version id: versions - uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 - - name: Set up php${{ steps.versions.outputs.php-available }} - uses: shivammathur/setup-php@2e947f1f6932d141d076ca441d0e1e881775e95b # v2.31.0 + - name: Set up php${{ steps.versions.outputs.php-min }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: - php-version: ${{ steps.versions.outputs.php-available }} + php-version: ${{ steps.versions.outputs.php-min }} extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite coverage: none ini-file: development env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies - run: composer i + - name: Install nextcloud/ocp + run: composer remove nextcloud/ocp --dev && composer i - - name: Lint - run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) + - name: Check code style + run: composer run cs:check || composer run cs:fix && git diff --exit-code diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml index fc43037..867984f 100644 --- a/.github/workflows/lint-php.yml +++ b/.github/workflows/lint-php.yml @@ -6,7 +6,7 @@ # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT -name: Lint php +name: Lint on: pull_request @@ -14,36 +14,40 @@ permissions: contents: read concurrency: - group: lint-php-${{ github.head_ref || github.run_id }} + group: lint-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: matrix: runs-on: ubuntu-latest-low + name: Determine PHP version outputs: php-versions: ${{ steps.versions.outputs.php-versions }} steps: - - name: Checkout app - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Get version matrix + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get PHP version id: versions - uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.0.0 + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 - php-lint: + lint: runs-on: ubuntu-latest needs: matrix strategy: matrix: - php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}} - - name: php-lint - + php-versions: ${{ fromJson(needs.matrix.outputs.php-versions) }} + name: PHP-${{ matrix.php-versions }} steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@2e947f1f6932d141d076ca441d0e1e881775e95b # v2.31.0 + - name: Set up php${{ matrix.php-versions }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: php-version: ${{ matrix.php-versions }} extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite @@ -52,19 +56,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies + run: composer i + - name: Lint run: composer run lint summary: - permissions: - contents: none runs-on: ubuntu-latest-low - needs: php-lint - + needs: lint if: always() - - name: php-lint-summary - + name: Lint summary steps: - - name: Summary status - run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi + - name: Check lint status + run: if [ "${{ needs.lint.result }}" != "success" ]; then exit 1; fi From 058ce29257af8e7bbf9edda935e10c901cb549f0 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 25 Mar 2026 19:53:25 +0100 Subject: [PATCH 11/12] feat: add HTTP Digest authentication support for WebDAV backend Implements the feature requested in PR #225 / proposed in PR #227. Adds an optional second constructor argument ('basic' or 'digest', defaulting to 'basic' for full backward compatibility). AI-assisted-by: Claude Sonnet 4.6 Signed-off-by: Anna Larch Signed-off-by: Sebastian L. --- lib/WebDavAuth.php | 254 ++++++++++++++++++++++-- tests/unit/WebDavAuthTest.php | 350 ++++++++++++++++++++++++++++++++++ 2 files changed, 587 insertions(+), 17 deletions(-) create mode 100644 tests/unit/WebDavAuthTest.php diff --git a/lib/WebDavAuth.php b/lib/WebDavAuth.php index ab4cc72..0344d51 100644 --- a/lib/WebDavAuth.php +++ b/lib/WebDavAuth.php @@ -1,5 +1,7 @@ * This file is licensed under the Affero General Public License version 3 or @@ -9,44 +11,262 @@ namespace OCA\UserExternal; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; + class WebDavAuth extends Base { - private $webDavAuthUrl; + private string $webDavAuthUrl; + private string $authType; - public function __construct($webDavAuthUrl) { - parent::__construct($webDavAuthUrl); + public function __construct( + string $webDavAuthUrl, + string $authType = 'basic', + ?IDBConnection $db = null, + ?IUserManager $userManager = null, + ?IGroupManager $groupManager = null, + ?LoggerInterface $logger = null, + ) { + parent::__construct($webDavAuthUrl, $db, $userManager, $groupManager, $logger); $this->webDavAuthUrl = $webDavAuthUrl; + $this->authType = $authType; } /** - * Check if the password is correct without logging in the user + * Check if the password is correct without logging in the user. * * @param string $uid The username * @param string $password The password - * - * @return true/false + * @return string|false The uid on success, false on failure */ public function checkPassword($uid, $password) { $uid = $this->resolveUid($uid); - $arr = explode('://', $this->webDavAuthUrl, 2); - if (! isset($arr) or count($arr) !== 2) { - $this->logger->error('ERROR: Invalid WebdavUrl: "' . $this->webDavAuthUrl . '" ', ['app' => 'user_external']); + $parsed = parse_url($this->webDavAuthUrl); + if ($parsed === false + || !isset($parsed['scheme'], $parsed['host']) + || !in_array($parsed['scheme'], ['http', 'https'], true) + || isset($parsed['user']) + ) { + $this->logger->error('Invalid WebDAV URL: "' . $this->webDavAuthUrl . '"', ['app' => 'user_external']); return false; } - [$protocol, $path] = $arr; - $url = $protocol . '://' . urlencode($uid) . ':' . urlencode($password) . '@' . $path; - $headers = get_headers($url); - if ($headers === false) { - $this->logger->error('ERROR: Not possible to connect to WebDAV Url: "' . $protocol . '://' . $path . '" ', ['app' => 'user_external']); + $url = $this->webDavAuthUrl; + + switch ($this->authType) { + case 'basic': + $responseHeaders = $this->fetchWithBasicAuth($url, $uid, $password); + break; + case 'digest': + $responseHeaders = $this->fetchWithDigestAuth($url, $uid, $password); + break; + default: + $this->logger->error( + 'Invalid WebDAV auth type: "' . $this->authType . '". Expected "basic" or "digest".', + ['app' => 'user_external'], + ); + return false; + } + + if ($responseHeaders === null) { return false; } - $returnCode = substr($headers[0], 9, 3); - if (substr($returnCode, 0, 1) === '2') { + $returnCode = substr($responseHeaders[0], 9, 3); + if (str_starts_with($returnCode, '2')) { $this->storeUser($uid); return $uid; + } + return false; + } + + /** + * Perform a HEAD request with HTTP Basic authentication. + * + * @return string[]|null Response headers, or null on connection failure. + */ + protected function fetchWithBasicAuth(string $url, string $uid, string $password): ?array { + $context = stream_context_create(['http' => [ + 'method' => 'HEAD', + 'header' => 'Authorization: Basic ' . base64_encode($uid . ':' . $password), + 'ignore_errors' => true, + 'follow_location' => 0, + ]]); + $responseHeaders = $this->fetchUrl($url, $context); + if ($responseHeaders === null) { + $this->logger->error('Not possible to connect to WebDAV URL: "' . $url . '"', ['app' => 'user_external']); + return null; + } + + $returnCode = substr($responseHeaders[0], 9, 3); + if (str_starts_with($returnCode, '3')) { + $this->logger->error( + 'WebDAV URL returned a redirect (' . $returnCode . '). Redirects are not followed for authenticated requests to prevent credential leaking.', + ['app' => 'user_external'], + ); + return null; + } + + return $responseHeaders; + } + + /** + * Perform a two-step HEAD request with HTTP Digest authentication. + * + * @return string[]|null Response headers, or null on connection failure or missing challenge. + */ + protected function fetchWithDigestAuth(string $url, string $uid, string $password): ?array { + // Step 1: unauthenticated request to receive the server challenge + $challengeContext = stream_context_create(['http' => [ + 'method' => 'HEAD', + 'ignore_errors' => true, + 'follow_location' => 0, + ]]); + $challengeHeaders = $this->fetchUrl($url, $challengeContext); + if ($challengeHeaders === null) { + $this->logger->error('Not possible to connect to WebDAV URL: "' . $url . '"', ['app' => 'user_external']); + return null; + } + + $challengeCode = substr($challengeHeaders[0], 9, 3); + if (str_starts_with($challengeCode, '3')) { + $this->logger->error( + 'WebDAV Digest challenge returned a redirect (' . $challengeCode . '). Redirects are not followed to prevent sending credentials to an unintended host.', + ['app' => 'user_external'], + ); + return null; + } + + // Step 2: find the WWW-Authenticate: Digest header + $authHeaderValue = null; + foreach ($challengeHeaders as $header) { + if (stripos($header, 'WWW-Authenticate: Digest ') === 0) { + $authHeaderValue = substr($header, strlen('WWW-Authenticate: Digest ')); + break; + } + } + + if ($authHeaderValue === null) { + $this->logger->error('No Digest challenge received from WebDAV URL: "' . $url . '"', ['app' => 'user_external']); + return null; + } + + // Step 3: parse the challenge parameters + $params = []; + preg_match_all('/(\w+)\s*=\s*(?:"([^"]*)"|([^\s,]+))/', $authHeaderValue, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $params[$m[1]] = $m[2] !== '' ? $m[2] : $m[3]; + } + + if (!isset($params['realm'], $params['nonce'])) { + $this->logger->error('Invalid Digest challenge from WebDAV URL: "' . $url . '"', ['app' => 'user_external']); + return null; + } + + $algorithm = $params['algorithm'] ?? 'MD5'; + if ($algorithm !== 'MD5') { + $this->logger->error( + 'Unsupported Digest algorithm: "' . $algorithm . '". Only MD5 is supported.', + ['app' => 'user_external'], + ); + return null; + } + + // Step 4: compute the digest response + $parsedUrl = parse_url($url); + $uri = $parsedUrl['path'] ?? '/'; + if (isset($parsedUrl['query'])) { + $uri .= '?' . $parsedUrl['query']; + } + + $qopTokens = isset($params['qop']) ? array_map('trim', explode(',', $params['qop'])) : []; + $useQop = in_array('auth', $qopTokens, true); + if (!empty($qopTokens) && !$useQop) { + $this->logger->error( + 'Unsupported Digest qop: "' . $params['qop'] . '". Only "auth" is supported.', + ['app' => 'user_external'], + ); + return null; + } + + try { + $A1 = md5($uid . ':' . $params['realm'] . ':' . $password); + $A2 = md5('HEAD:' . $uri); + + if ($useQop) { + $cnonce = bin2hex(random_bytes(8)); + $nc = '00000001'; + $response = md5($A1 . ':' . $params['nonce'] . ':' . $nc . ':' . $cnonce . ':auth:' . $A2); + } else { + $response = md5($A1 . ':' . $params['nonce'] . ':' . $A2); + } + } catch (\Throwable $e) { + $this->logger->error('Failed to compute Digest response: ' . $e->getMessage(), ['app' => 'user_external']); + return null; + } + + $digestHeader = sprintf( + 'Authorization: Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"', + $this->escapeDigestValue($uid), + $this->escapeDigestValue($params['realm']), + $this->escapeDigestValue($params['nonce']), + $this->escapeDigestValue($uri), + $response, + ); + if ($useQop) { + $digestHeader .= sprintf(', cnonce="%s", nc=%s, qop=auth', $cnonce, $nc); + } + if (isset($params['opaque'])) { + $digestHeader .= sprintf(', opaque="%s"', $this->escapeDigestValue($params['opaque'])); + } + + // Step 5: send the authenticated request + $context = stream_context_create(['http' => [ + 'method' => 'HEAD', + 'header' => $digestHeader, + 'ignore_errors' => true, + 'follow_location' => 0, + ]]); + $responseHeaders = $this->fetchUrl($url, $context); + if ($responseHeaders === null) { + $this->logger->error('Digest authenticated request failed for WebDAV URL: "' . $url . '"', ['app' => 'user_external']); + return null; + } + + $authCode = substr($responseHeaders[0], 9, 3); + if (str_starts_with($authCode, '3')) { + $this->logger->error( + 'WebDAV Digest authenticated request returned a redirect (' . $authCode . '). Redirects are not followed to prevent credential leaking.', + ['app' => 'user_external'], + ); + return null; + } + + return $responseHeaders; + } + + private function escapeDigestValue(string $value): string { + $value = str_replace(["\r", "\n"], '', $value); + return addcslashes($value, '"\\'); + } + + /** + * Perform an HTTP request and return the response headers. + * Extracted so tests can stub network calls without hitting the wire. + * + * @return string[]|null Response headers, or null if the server is unreachable. + */ + protected function fetchUrl(string $url, mixed $context = null): ?array { + $http_response_header = null; + if ($context !== null) { + $result = @file_get_contents($url, false, $context); } else { - return false; + $result = @file_get_contents($url); + } + if ($result === false && $http_response_header === null) { + return null; } + return $http_response_header; } } diff --git a/tests/unit/WebDavAuthTest.php b/tests/unit/WebDavAuthTest.php new file mode 100644 index 0000000..ac23bf0 --- /dev/null +++ b/tests/unit/WebDavAuthTest.php @@ -0,0 +1,350 @@ + */ + public array $fetchResponses = []; + /** @var list Captured stream contexts from fetchUrl() calls */ + public array $capturedContexts = []; + + protected function fetchUrl(string $url, mixed $context = null): ?array { + $this->capturedContexts[] = $context; + return array_shift($this->fetchResponses); + } +} + +class WebDavAuthTest extends TestCase { + private MockObject&IDBConnection $db; + private MockObject&IUserManager $userManager; + private MockObject&IGroupManager $groupManager; + private MockObject&LoggerInterface $logger; + + protected function setUp(): void { + $this->db = $this->createMock(IDBConnection::class); + $this->db->method('escapeLikeParameter')->willReturnArgument(0); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + } + + private function makeBackend(string $url = 'https://example.com/dav', string $authType = 'basic'): TestableWebDavAuth { + return new TestableWebDavAuth( + $url, + $authType, + $this->db, + $this->userManager, + $this->groupManager, + $this->logger, + ); + } + + private function mockQueryBuilder(int $existingUserCount = 0): MockObject&IQueryBuilder { + $expr = $this->createMock(IExpressionBuilder::class); + $expr->method('eq')->willReturn('1=1'); + $expr->method('iLike')->willReturn('1=1'); + + $queryFunction = $this->createMock(IQueryFunction::class); + $funcBuilder = $this->createMock(IFunctionBuilder::class); + $funcBuilder->method('count')->willReturn($queryFunction); + + $countResult = $this->createMock(IResult::class); + $countResult->method('fetchOne')->willReturn($existingUserCount); + $countResult->method('closeCursor')->willReturn(true); + + $qb = $this->createMock(IQueryBuilder::class); + $qb->method('expr')->willReturn($expr); + $qb->method('func')->willReturn($funcBuilder); + $qb->method('select')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('insert')->willReturnSelf(); + $qb->method('values')->willReturnSelf(); + $qb->method('createNamedParameter')->willReturnArgument(0); + $qb->method('executeQuery')->willReturn($countResult); + + return $qb; + } + + // ------------------------------------------------------------------------- + // URL validation + // ------------------------------------------------------------------------- + + public function testInvalidUrlReturnsFalse(): void { + $backend = $this->makeBackend('not-a-valid-url'); + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testNonHttpSchemeReturnsFalse(): void { + $backend = $this->makeBackend('ftp://example.com/dav'); + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testUrlWithUserinfoReturnsFalse(): void { + $backend = $this->makeBackend('https://user:pass@example.com/dav'); + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + // ------------------------------------------------------------------------- + // Invalid auth type + // ------------------------------------------------------------------------- + + public function testInvalidAuthTypeReturnsFalseAndLogsError(): void { + $backend = $this->makeBackend('https://example.com/dav', 'kerberos'); + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + // ------------------------------------------------------------------------- + // Basic auth + // ------------------------------------------------------------------------- + + public function testBasicAuthSuccessStoresAndReturnsUid(): void { + $backend = $this->makeBackend(); + $backend->fetchResponses = [['HTTP/1.1 200 OK']]; + + $qb = $this->mockQueryBuilder(0); // new user + $qb->expects($this->once())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('user', $backend->checkPassword('user', 'pass')); + } + + public function testBasicAuthSuccessDoesNotInsertExistingUser(): void { + $backend = $this->makeBackend(); + $backend->fetchResponses = [['HTTP/1.1 200 OK']]; + + $qb = $this->mockQueryBuilder(1); // already exists + $qb->expects($this->never())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('user', $backend->checkPassword('user', 'pass')); + } + + public function testBasicAuthWrongPasswordReturnsFalse(): void { + $backend = $this->makeBackend(); + $backend->fetchResponses = [['HTTP/1.1 401 Unauthorized']]; + + $this->assertFalse($backend->checkPassword('user', 'wrongpass')); + } + + public function testBasicAuthConnectionFailureReturnsFalse(): void { + $backend = $this->makeBackend(); + $backend->fetchResponses = [null]; + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + // ------------------------------------------------------------------------- + // Digest auth + // ------------------------------------------------------------------------- + + private function digestChallenge(string $realm = 'example', string $nonce = 'abc123', string $opaque = 'xyz'): array { + return [ + 'HTTP/1.1 401 Unauthorized', + "WWW-Authenticate: Digest realm=\"{$realm}\", nonce=\"{$nonce}\", qop=\"auth\", opaque=\"{$opaque}\"", + ]; + } + + public function testDigestAuthSuccessStoresAndReturnsUid(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge(), // challenge request + ['HTTP/1.1 200 OK'], // authenticated request + ]; + + $qb = $this->mockQueryBuilder(0); + $qb->expects($this->once())->method('executeStatement'); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('user', $backend->checkPassword('user', 'pass')); + } + + public function testDigestAuthWrongPasswordReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge(), + ['HTTP/1.1 401 Unauthorized'], + ]; + + $this->assertFalse($backend->checkPassword('user', 'wrongpass')); + } + + public function testDigestAuthConnectionFailureOnChallengeReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [null]; // server unreachable + $this->logger->expects($this->atLeast(1))->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testDigestAuthNoChallengeHeaderReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [['HTTP/1.1 200 OK']]; // no WWW-Authenticate header + $this->logger->expects($this->atLeast(1))->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testDigestAuthConnectionFailureOnAuthRequestReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge(), + null, // authenticated request fails + ]; + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testDigestAuthComputesCorrectResponseHashWithQop(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge('myrealm', 'mynonce'), + ['HTTP/1.1 200 OK'], + ]; + + $qb = $this->mockQueryBuilder(1); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('alice', $backend->checkPassword('alice', 's3cr3t')); + + $authContext = $backend->capturedContexts[1]; + $opts = stream_context_get_options($authContext); + $header = $opts['http']['header']; + + $this->assertStringContainsString('uri="/dav"', $header); + $this->assertStringContainsString('qop=auth', $header); + + $this->assertSame(1, preg_match('/response="([^"]+)"/', $header, $m)); + $this->assertSame(1, preg_match('/cnonce="([^"]+)"/', $header, $cm)); + $cnonce = $cm[1]; + + $A1 = md5('alice:myrealm:s3cr3t'); + $A2 = md5('HEAD:/dav'); + $expected = md5($A1 . ':mynonce:00000001:' . $cnonce . ':auth:' . $A2); + $this->assertSame($expected, $m[1]); + } + + public function testDigestAuthComputesCorrectResponseHashWithoutQop(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + ['HTTP/1.1 401 Unauthorized', 'WWW-Authenticate: Digest realm="myrealm", nonce="mynonce"'], + ['HTTP/1.1 200 OK'], + ]; + + $qb = $this->mockQueryBuilder(1); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('alice', $backend->checkPassword('alice', 's3cr3t')); + + $authContext = $backend->capturedContexts[1]; + $opts = stream_context_get_options($authContext); + $header = $opts['http']['header']; + + $this->assertStringContainsString('uri="/dav"', $header); + $this->assertStringNotContainsString('qop=', $header); + $this->assertStringNotContainsString('cnonce=', $header); + + $this->assertSame(1, preg_match('/response="([^"]+)"/', $header, $m)); + + $A1 = md5('alice:myrealm:s3cr3t'); + $A2 = md5('HEAD:/dav'); + $expected = md5($A1 . ':mynonce:' . $A2); + $this->assertSame($expected, $m[1]); + } + + public function testDigestAuthWithOpaqueIncludedInHeader(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge('realm', 'nonce', 'opaquevalue'), + ['HTTP/1.1 200 OK'], + ]; + + $qb = $this->mockQueryBuilder(1); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('user', $backend->checkPassword('user', 'pass')); + + $authContext = $backend->capturedContexts[1]; + $opts = stream_context_get_options($authContext); + $header = $opts['http']['header']; + $this->assertStringContainsString('opaque="opaquevalue"', $header); + } + + public function testDigestAuthUnsupportedAlgorithmReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + ['HTTP/1.1 401 Unauthorized', 'WWW-Authenticate: Digest realm="r", nonce="n", algorithm="SHA-256"'], + ]; + $this->logger->expects($this->atLeast(1))->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testDigestAuthEscapesSpecialCharactersInUsername(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + $this->digestChallenge(), + ['HTTP/1.1 200 OK'], + ]; + + $qb = $this->mockQueryBuilder(1); + $this->db->method('getQueryBuilder')->willReturn($qb); + + $this->assertSame('user"evil', $backend->checkPassword('user"evil', 'pass')); + + $authContext = $backend->capturedContexts[1]; + $opts = stream_context_get_options($authContext); + $header = $opts['http']['header']; + $this->assertStringContainsString('username="user\\"evil"', $header); + $this->assertStringNotContainsString("\r", $header); + $this->assertStringNotContainsString("\n", $header); + } + + public function testDigestAuthAuthIntOnlyReturnsFalse(): void { + $backend = $this->makeBackend('https://example.com/dav', 'digest'); + $backend->fetchResponses = [ + ['HTTP/1.1 401 Unauthorized', 'WWW-Authenticate: Digest realm="r", nonce="n", qop="auth-int"'], + ]; + $this->logger->expects($this->atLeast(1))->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } + + public function testBasicAuthRedirectLogsError(): void { + $backend = $this->makeBackend(); + $backend->fetchResponses = [['HTTP/1.1 302 Found']]; + $this->logger->expects($this->atLeast(1))->method('error'); + + $this->assertFalse($backend->checkPassword('user', 'pass')); + } +} From adaff08fb6ec429e666b06b1e093e497f5d24f6b Mon Sep 17 00:00:00 2001 From: "Sebastian L." Date: Sat, 2 May 2026 10:54:27 +0200 Subject: [PATCH 12/12] Use setSystemEMailAddress Move mbstrtolower for $email Use filter_var to check for valid email Signed-off-by: Sebastian L. --- lib/Base.php | 8 ++++---- lib/IMAP.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Base.php b/lib/Base.php index a518a30..5fbb108 100644 --- a/lib/Base.php +++ b/lib/Base.php @@ -211,11 +211,11 @@ protected function storeUser($uid, $groups = [], $email = '') { $this->groupManager->createGroup($group)->addUser($createduser); } } - + if ($email) { - $config = \OC::$server->getConfig(); - $config->setUserValue( $uid, 'settings', 'email', $email); - } + $createduser = \OC::$server->getUserManager()->get($uid); + $createduser->setSystemEMailAddress($email); + } } } diff --git a/lib/IMAP.php b/lib/IMAP.php index e2aa574..57edac9 100644 --- a/lib/IMAP.php +++ b/lib/IMAP.php @@ -64,8 +64,8 @@ public function checkPassword($uid, $password) { $uid = str_replace('%40', '@', $uid); } - // Get email if uid contains @ symbol - if ( str_contains( $uid, '@' ) ) $email = $uid; + // Get email if uid is a valid email address + if (filter_var($uid, FILTER_VALIDATE_EMAIL)) $email = mb_strtolower($uid); $pieces = explode('@', $uid); if ($this->domain !== '') { @@ -110,7 +110,7 @@ public function checkPassword($uid, $password) { if ($errorcode === 0) { curl_close($ch); $uid = mb_strtolower($uid); - $this->storeUser($uid, $groups, mb_strtolower($email)); + $this->storeUser($uid, $groups, $email); return $uid; } elseif ($errorcode === CURLE_COULDNT_CONNECT || $errorcode === CURLE_SSL_CONNECT_ERROR