diff --git a/appinfo/info.xml b/appinfo/info.xml index a7403eac9..59a08bc6f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -3,7 +3,7 @@ - SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors - SPDX-License-Identifier: AGPL-3.0-or-later --> - + user_saml SSO & SAML authentication Authenticate using single sign-on @@ -20,7 +20,7 @@ The following providers are supported and tested at the moment: * Any other provider that authenticates using the environment variable While theoretically any other authentication provider implementing either one of those standards is compatible, we like to note that they are not part of any internal test matrix.]]> - 7.1.1 + 7.1.2-beta.1 agpl Lukas Reschke User_SAML @@ -39,6 +39,9 @@ While theoretically any other authentication provider implementing either one of + + OCA\User_SAML\Jobs\CleanSessionData + OCA\User_SAML\Migration\RememberLocalGroupsForPotentialMigrations diff --git a/composer.json b/composer.json index db31c35be..75ac3008b 100644 --- a/composer.json +++ b/composer.json @@ -23,11 +23,11 @@ "post-update-cmd": [ "[ $COMPOSER_DEV_MODE -eq 0 ] || composer bin all update --ansi" ], - "cs:fix": "php-cs-fixer fix", - "cs:check": "php-cs-fixer fix --dry-run --diff", - "psalm": "psalm", - "psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType", - "psalm:update-baseline": "psalm --threads=1 --update-baseline", + "cs:fix": "@php php-cs-fixer fix", + "cs:check": "@php php-cs-fixer fix --dry-run --diff", + "psalm": "@php psalm", + "psalm:fix": "@php psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType", + "psalm:update-baseline": "@php psalm --threads=1 --update-baseline", "lint": "find . -name \\*.php -not -path '*/vendor/*' -print0 | xargs -0 -n1 php -l", "rector:check": "rector --dry-run", "rector:fix": "rector", diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 1d770f40b..5cb8aedd8 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -16,10 +16,13 @@ use OCA\User_SAML\DavPlugin; use OCA\User_SAML\GroupBackend; use OCA\User_SAML\GroupManager; +use OCA\User_SAML\Listener\CookieLoginEventListener; use OCA\User_SAML\Listener\LoadAdditionalScriptsListener; +use OCA\User_SAML\Listener\LoginEventListener; use OCA\User_SAML\Listener\SabrePluginEventListener; use OCA\User_SAML\Middleware\OnlyLoggedInMiddleware; use OCA\User_SAML\SAMLSettings; +use OCA\User_SAML\Service\SessionService; use OCA\User_SAML\UserBackend; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -38,6 +41,9 @@ use OCP\IUserSession; use OCP\L10N\IFactory; use OCP\Server; +use OCP\User\Events\BeforeUserLoggedInWithCookieEvent; +use OCP\User\Events\UserLoggedInEvent; +use OCP\User\Events\UserLoggedInWithCookieEvent; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Throwable; @@ -53,11 +59,15 @@ public function register(IRegistrationContext $context): void { $context->registerMiddleware(OnlyLoggedInMiddleware::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalScriptsListener::class); $context->registerEventListener(SabrePluginAddEvent::class, SabrePluginEventListener::class); + $context->registerEventListener(BeforeUserLoggedInWithCookieEvent::class, CookieLoginEventListener::class); + $context->registerEventListener(UserLoggedInWithCookieEvent::class, CookieLoginEventListener::class); + $context->registerEventListener(UserLoggedInEvent::class, LoginEventListener::class); $context->registerService(DavPlugin::class, fn (ContainerInterface $c) => new DavPlugin( $c->get(ISession::class), $c->get(IConfig::class), $_SERVER, - $c->get(SAMLSettings::class) + $c->get(SAMLSettings::class), + $c->get(SessionService::class), )); } diff --git a/lib/Controller/SAMLController.php b/lib/Controller/SAMLController.php index 30c980630..dd48473e7 100644 --- a/lib/Controller/SAMLController.php +++ b/lib/Controller/SAMLController.php @@ -17,6 +17,7 @@ use OCA\User_SAML\Exceptions\UserFilterViolationException; use OCA\User_SAML\Helper\TXmlHelper; use OCA\User_SAML\SAMLSettings; +use OCA\User_SAML\Service\SessionService; use OCA\User_SAML\UserBackend; use OCA\User_SAML\UserData; use OCA\User_SAML\UserResolver; @@ -57,6 +58,7 @@ public function __construct( private UserData $userData, private ICrypto $crypto, private ITrustedDomainHelper $trustedDomainHelper, + private SessionService $sessionService, ) { parent::__construct($appName, $request); } @@ -232,7 +234,7 @@ public function login(int $idp = 1): Http\RedirectResponse|Http\TemplateResponse if (empty($ssoUrl)) { $ssoUrl = $this->urlGenerator->getAbsoluteURL('/'); } - $this->session->set('user_saml.samlUserData', $_SERVER); + $this->sessionService->prepareEnvironmentBasedSession($_SERVER); try { $this->userData->setAttributes($this->session->get('user_saml.samlUserData')); $this->autoprovisionIfPossible(); @@ -335,8 +337,8 @@ public function assertionConsumerService(): Http\RedirectResponse { $AuthNRequestID = $data['AuthNRequestID']; $idp = $data['Idp']; // need to keep the IdP config ID during session lifetime (SAMLSettings::getPrefix) - $this->session->set('user_saml.Idp', $idp); - if (is_null($AuthNRequestID) || $AuthNRequestID === '' || is_null($idp)) { + $this->sessionService->storeIdentityProviderInSession($idp); + if (is_null($AuthNRequestID) || $AuthNRequestID === '') { $this->logger->debug('Invalid auth payload', ['app' => 'user_saml']); return new Http\RedirectResponse($this->urlGenerator->getAbsoluteURL('/')); } @@ -383,14 +385,8 @@ public function assertionConsumerService(): Http\RedirectResponse { return $response; } - $this->session->set('user_saml.samlUserData', $auth->getAttributes()); - $this->session->set('user_saml.samlNameId', $auth->getNameId()); - $this->session->set('user_saml.samlNameIdFormat', $auth->getNameIdFormat()); - $this->session->set('user_saml.samlNameIdNameQualifier', $auth->getNameIdNameQualifier()); - $this->session->set('user_saml.samlNameIdSPNameQualifier', $auth->getNameIdSPNameQualifier()); - $this->session->set('user_saml.samlSessionIndex', $auth->getSessionIndex()); - $this->session->set('user_saml.samlSessionExpiration', $auth->getSessionExpiration()); - $this->logger->debug('Session values set', ['app' => 'user_saml']); + $this->sessionService->storeAuthDataInSession($auth); + try { $user = $this->userResolver->findExistingUser($this->userBackend->getCurrentUserId()); $firstLogin = $user->updateLastLoginTimestamp(); @@ -510,14 +506,14 @@ public function singleLogoutService(): Http\RedirectResponse { */ private function tryProcessSLOResponse(?int $idp): array { $idps = ($idp !== null) ? [$idp] : array_keys($this->samlSettings->getListOfIdps()); - foreach ($idps as $idp) { + foreach ($idps as $identityProviderId) { try { - $auth = new Auth($this->samlSettings->getOneLoginSettingsArray($idp)); + $auth = new Auth($this->samlSettings->getOneLoginSettingsArray($identityProviderId)); // validator (called with processSLO()) needs an XML entity loader $targetUrl = $this->callWithXmlEntityLoader(fn (): string => $auth->processSLO( true, // do not let processSLO to delete the entire session. Let userSession->logout do the job null, - $this->samlSettings->usesSloWebServerDecode($idp), + $this->samlSettings->usesSloWebServerDecode($identityProviderId), null, true )); diff --git a/lib/DavPlugin.php b/lib/DavPlugin.php index 118062d1d..da0352c6f 100644 --- a/lib/DavPlugin.php +++ b/lib/DavPlugin.php @@ -8,7 +8,8 @@ namespace OCA\User_SAML; use OCA\DAV\Connector\Sabre\Auth; -use OCP\IConfig; +use OCA\User_SAML\Service\SessionService; +use OCP\AppFramework\Services\IAppConfig; use OCP\ISession; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -16,32 +17,32 @@ use Sabre\HTTP\ResponseInterface; class DavPlugin extends ServerPlugin { - /** @var Server */ - private $server; + /** @noinspection PhpPropertyOnlyWrittenInspection */ + private Server $server; public function __construct( private readonly ISession $session, - private readonly IConfig $config, - private array $auth, + private readonly IAppConfig $config, + private readonly array $auth, private readonly SAMLSettings $samlSettings, + private readonly SessionService $sessionService, ) { } - public function initialize(Server $server) { - // before auth + public function initialize(Server $server): void { $server->on('beforeMethod:*', $this->beforeMethod(...), 9); $this->server = $server; } - public function beforeMethod(RequestInterface $request, ResponseInterface $response) { + public function beforeMethod(RequestInterface $request, ResponseInterface $response): void { if ( - $this->config->getAppValue('user_saml', 'type') === 'environment-variable' + $this->config->getAppValueString('type', 'unset') === 'environment-variable' && !$this->session->exists('user_saml.samlUserData') ) { $uidMapping = $this->samlSettings->get(1)['general-uid_mapping']; if (isset($this->auth[$uidMapping])) { $this->session->set(Auth::DAV_AUTHENTICATED, $this->auth[$uidMapping]); - $this->session->set('user_saml.samlUserData', $this->auth); + $this->sessionService->prepareEnvironmentBasedSession($this->auth); } } } diff --git a/lib/Db/SessionData.php b/lib/Db/SessionData.php new file mode 100644 index 000000000..77de4b2d7 --- /dev/null +++ b/lib/Db/SessionData.php @@ -0,0 +1,46 @@ +addType('id', Types::STRING); + // technically tokenId is BIGINT, effectively no difference in the Entity context. + // It can be set to BIGINT once dropping NC 30 support. + $this->addType('tokenId', Types::INTEGER); + $this->addType('data', Types::TEXT); + } + + public function setId(string $id): void { + $this->id = $id; + $this->markFieldUpdated('id'); + } + + public function setData(SessionDataModel $input): void { + $this->data = json_encode($input); + $this->markFieldUpdated('data'); + } + + public function getData(): SessionDataModel { + $deserialized = json_decode($this->data, true); + return SessionDataModel::fromInputArray($deserialized); + } +} diff --git a/lib/Db/SessionDataMapper.php b/lib/Db/SessionDataMapper.php new file mode 100644 index 000000000..c2c565e6a --- /dev/null +++ b/lib/Db/SessionDataMapper.php @@ -0,0 +1,36 @@ + */ +class SessionDataMapper extends QBMapper { + public const SESSION_DATA_TABLE_NAME = 'user_saml_session_data'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::SESSION_DATA_TABLE_NAME, SessionData::class); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function retrieve(string $sessionId): SessionData { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from(self::SESSION_DATA_TABLE_NAME) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($sessionId))); + return $this->findEntity($qb); + } +} diff --git a/lib/Jobs/CleanSessionData.php b/lib/Jobs/CleanSessionData.php new file mode 100644 index 000000000..787158a17 --- /dev/null +++ b/lib/Jobs/CleanSessionData.php @@ -0,0 +1,62 @@ +setInterval(86400); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + $this->setAllowParallelRuns(false); + } + + protected function run(mixed $argument): void { + $missingSessionIds = $this->findInvalidatedSessions(); + $this->deleteInvalidatedSessions($missingSessionIds); + } + + protected function findInvalidatedSessions(): array { + $qb = $this->dbc->getQueryBuilder(); + $qb->select('s.id') + ->from(SessionDataMapper::SESSION_DATA_TABLE_NAME, 's') + ->leftJoin('s', self::NC_AUTH_TOKEN_TABLE, 'a', + $qb->expr()->eq('s.token_id', 'a.id'), + ) + ->where($qb->expr()->isNull('a.id')) + ->setMaxResults(1000); + + $invalidatedSessionsResult = $qb->executeQuery(); + $invalidatedSessionIds = $invalidatedSessionsResult->fetchAll(\PDO::FETCH_COLUMN); + $invalidatedSessionsResult->closeCursor(); + + return $invalidatedSessionIds; + } + + protected function deleteInvalidatedSessions(array $invalidatedSessionIds): void { + $qb = $this->dbc->getQueryBuilder(); + $qb->delete(SessionDataMapper::SESSION_DATA_TABLE_NAME) + ->where($qb->expr()->in( + 'id', + $qb->createNamedParameter($invalidatedSessionIds, IQueryBuilder::PARAM_STR_ARRAY) + )); + $qb->executeStatement(); + } +} diff --git a/lib/Listener/CookieLoginEventListener.php b/lib/Listener/CookieLoginEventListener.php new file mode 100644 index 000000000..1389bc663 --- /dev/null +++ b/lib/Listener/CookieLoginEventListener.php @@ -0,0 +1,48 @@ + */ +class CookieLoginEventListener implements IEventListener { + protected ?string $oldSessionId = null; + + public function __construct( + protected readonly SessionService $sessionService, + ) { + } + + public function handle(Event $event): void { + if ($event instanceof BeforeUserLoggedInWithCookieEvent) { + $this->prepareRestoreOfSession(); + return; + } + + if ($event instanceof UserLoggedInWithCookieEvent) { + $this->restoreSession(); + return; + } + } + + protected function prepareRestoreOfSession(): void { + if (isset($_COOKIE['nc_session_id'])) { + $this->oldSessionId = $_COOKIE['nc_session_id']; + } + } + + protected function restoreSession(): void { + if ($this->oldSessionId !== null) { + $this->sessionService->restoreSession($this->oldSessionId); + } + } +} diff --git a/lib/Listener/LoginEventListener.php b/lib/Listener/LoginEventListener.php new file mode 100644 index 000000000..dfaedf540 --- /dev/null +++ b/lib/Listener/LoginEventListener.php @@ -0,0 +1,31 @@ + */ +class LoginEventListener implements IEventListener { + public function __construct( + protected SessionService $sessionService, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof UserLoggedInEvent + || $event->isTokenLogin() + || !$this->sessionService->isActiveSamlSession() + ) { + return; + } + $this->sessionService->storeSessionDataInDatabase(); + } +} diff --git a/lib/Migration/Version7001Date20251215192613.php b/lib/Migration/Version7001Date20251215192613.php new file mode 100644 index 000000000..1da30d1f9 --- /dev/null +++ b/lib/Migration/Version7001Date20251215192613.php @@ -0,0 +1,47 @@ +hasTable(self::SESSION_DATA_TABLE_NAME)) { + return null; + } + + $table = $schema->createTable(self::SESSION_DATA_TABLE_NAME); + $table->addColumn('id', Types::STRING, [ + 'notnull' => true, + 'length' => 200, + ]); + $table->addColumn('token_id', Types::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('data', Types::TEXT, [ + 'notnull' => true, + ]); + $table->setPrimaryKey(['id']); + + return $schema; + } + +} diff --git a/lib/Model/SessionData.php b/lib/Model/SessionData.php new file mode 100644 index 000000000..e70772b52 --- /dev/null +++ b/lib/Model/SessionData.php @@ -0,0 +1,89 @@ + 'user_saml.Idp', + 'KEY_SAML_NAME_ID' => 'user_saml.samlNameId', + 'KEY_SAML_NAME_ID_FORMAT' => 'user_saml.samlNameIdFormat', + 'KEY_SAML_NAME_ID_QUALIFIER' => 'user_saml.samlNameIdNameQualifier', + 'KEY_SAML_NAME_ID_SP_QUALIFIER' => 'user_saml.samlNameIdSPNameQualifier', + 'KEY_SAML_SESSION_INDEX' => 'user_saml.samlSessionIndex', + ]; + + public function __construct( + protected int $identityProviderId, + protected string $samlNameId, + protected string $samlNameIdFormat, + protected string $samlNameIdNameQualifier, + protected string $samlNameIdSPNameQualifier, + protected ?string $samlSessionIndex, + ) { + if ($this->identityProviderId < 0) { + throw new \InvalidArgumentException('IdentityProviderId has to be a positive integer'); + } + } + + public function storeInSession(ISession $session): void { + foreach ($this->jsonSerialize() as $sessionKey => $value) { + $session->set($sessionKey, $value); + } + } + + public function jsonSerialize(): array { + return [ + self::KEY_IDENTITY_PROVIDER_ID => $this->identityProviderId, + self::KEY_SAML_NAME_ID => $this->samlNameId, + self::KEY_SAML_NAME_ID_FORMAT => $this->samlNameIdFormat, + self::KEY_SAML_NAME_ID_QUALIFIER => $this->samlNameIdNameQualifier, + self::KEY_SAML_NAME_ID_SP_QUALIFIER => $this->samlNameIdSPNameQualifier, + self::KEY_SAML_SESSION_INDEX => $this->samlSessionIndex, + ]; + } + + public static function fromInputArray(array $rawData): self { + if (!isset($rawData[self::KEY_IDENTITY_PROVIDER_ID])) { + throw new \InvalidArgumentException('Expected and required Identity Provider ID is missing'); + } + + return new self( + $rawData[self::KEY_IDENTITY_PROVIDER_ID], + $rawData[self::KEY_SAML_NAME_ID] ?? '', + $rawData[self::KEY_SAML_NAME_ID_FORMAT] ?? '', + $rawData[self::KEY_SAML_NAME_ID_QUALIFIER] ?? '', + $rawData[self::KEY_SAML_NAME_ID_SP_QUALIFIER] ?? '', + $rawData[self::KEY_SAML_SESSION_INDEX] ?? null, + ); + } + + public static function fromSession(ISession $session): self { + $retrievedData = []; + foreach (self::SESSION_KEYS as $sessionKey) { + $value = $session->get($sessionKey); + if ($value === null) { + continue; + } + $retrievedData[$sessionKey] = $session->get($sessionKey); + } + return self::fromInputArray($retrievedData); + } + + public function __toString(): string { + return \json_encode($this); + } +} diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php new file mode 100644 index 000000000..5638000e9 --- /dev/null +++ b/lib/Service/SessionService.php @@ -0,0 +1,98 @@ +session->get(SessionDataModel::KEY_IDENTITY_PROVIDER_ID) !== null; + } + + public function storeIdentityProviderInSession(int $idp): void { + $this->session->set(SessionDataModel::KEY_IDENTITY_PROVIDER_ID, $idp); + } + + public function restoreSession(string $oldSessionId): void { + try { + $sessionIdHash = $this->hashSessionId($oldSessionId); + $sessionData = $this->sessionDataMapper->retrieve($sessionIdHash); + $this->storeSessionDataInSession($sessionData); + $this->storeSessionDataInDatabase(); + $this->logger->debug('SAML session successfully restored'); + // we do not delete the old session automatically to avoid race conditions + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + return; + } + } + + public function prepareEnvironmentBasedSession(array $env): void { + $this->storeIdentityProviderInSession(self::ENVIRONMENT_IDENTITY_PROVIDER_ID); + $this->session->set('user_saml.samlUserData', $env); + } + + public function storeAuthDataInSession(Auth $auth): void { + $this->session->set('user_saml.samlUserData', $auth->getAttributes()); + $this->session->set('user_saml.samlNameId', $auth->getNameId()); + $this->session->set('user_saml.samlNameIdFormat', $auth->getNameIdFormat()); + $this->session->set('user_saml.samlNameIdNameQualifier', $auth->getNameIdNameQualifier()); + $this->session->set('user_saml.samlNameIdSPNameQualifier', $auth->getNameIdSPNameQualifier()); + $this->session->set('user_saml.samlSessionIndex', $auth->getSessionIndex()); + $this->session->set('user_saml.samlSessionExpiration', $auth->getSessionExpiration()); + } + + protected function hashSessionId(string $sessionId): string { + // As of writing same implementation as in private's PublicKeyProvider`s + // hashToken() method. It is not public API and a private detail, so we + // cannot assume it always stays the same. Hence, even though it is + // at this moment identical to oc_authtoken.token (also not exposed), + // it is not something we can take for granted and therefore store the + // token ID as well. + $secret = $this->config->getSystemValueString('secret'); + return hash('sha512', $sessionId . $secret); + } + + public function storeSessionDataInDatabase(): void { + $sessionDataModel = SessionDataModel::fromSession($this->session); + + $sessionData = new SessionData(); + $sessionData->setId($this->hashSessionId($this->session->getId())); + $sessionData->setTokenId($this->tokenProvider->getToken($this->session->getId())->getId()); + $sessionData->setData($sessionDataModel); + + $this->sessionDataMapper->insert($sessionData); + } + + protected function storeSessionDataInSession(SessionData $sessionData): void { + $model = $sessionData->getData(); + foreach ($model->jsonSerialize() as $sessionKey => $value) { + $this->session->set($sessionKey, $value); + } + } +} diff --git a/lib/UserBackend.php b/lib/UserBackend.php index ddff91afe..2b4c9c593 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -8,6 +8,7 @@ namespace OCA\User_SAML; use OC\Security\CSRF\CsrfTokenManager; +use OCA\User_SAML\Model\SessionData; use OCP\Authentication\IApacheBackend; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\NotPermittedException; @@ -287,7 +288,7 @@ public function hasUserListings() { * @since 6.0.0 */ public function isSessionActive() { - return $this->session->get('user_saml.samlUserData') !== null; + return $this->session->get(SessionData::KEY_IDENTITY_PROVIDER_ID) !== null; } /** diff --git a/tests/unit/Controller/SAMLControllerTest.php b/tests/unit/Controller/SAMLControllerTest.php index dc06ad75e..bcba9df70 100644 --- a/tests/unit/Controller/SAMLControllerTest.php +++ b/tests/unit/Controller/SAMLControllerTest.php @@ -12,6 +12,7 @@ use OCA\User_SAML\Exceptions\NoUserFoundException; use OCA\User_SAML\Exceptions\UserFilterViolationException; use OCA\User_SAML\SAMLSettings; +use OCA\User_SAML\Service\SessionService; use OCA\User_SAML\UserBackend; use OCA\User_SAML\UserData; use OCA\User_SAML\UserResolver; @@ -58,6 +59,7 @@ class SAMLControllerTest extends TestCase { /** @var SAMLController */ private $samlController; private ITrustedDomainHelper|MockObject $trustedDomainController; + private SessionService|MockObject $sessionService; protected function setUp(): void { parent::setUp(); @@ -75,6 +77,7 @@ protected function setUp(): void { $this->userData = $this->createMock(UserData::class); $this->crypto = $this->createMock(ICrypto::class); $this->trustedDomainController = $this->createMock(ITrustedDomainHelper::class); + $this->sessionService = $this->createMock(SessionService::class); $this->l->expects($this->any())->method('t')->willReturnCallback( fn ($param) => $param @@ -97,7 +100,8 @@ protected function setUp(): void { $this->userResolver, $this->userData, $this->crypto, - $this->trustedDomainController + $this->trustedDomainController, + $this->sessionService ); } diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json index b205e3442..679e5612a 100644 --- a/vendor-bin/psalm/composer.json +++ b/vendor-bin/psalm/composer.json @@ -1,6 +1,6 @@ { "require-dev": { - "nextcloud/ocp": "dev-stable28", + "nextcloud/ocp": "dev-stable30", "vimeo/psalm": "^5.26", "sabre/dav": "4.7.0" }, diff --git a/vendor-bin/psalm/composer.lock b/vendor-bin/psalm/composer.lock index bfa92e5e0..726a1b52f 100644 --- a/vendor-bin/psalm/composer.lock +++ b/vendor-bin/psalm/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ed78ed32a8083cfab48ae511a90522ad", + "content-hash": "2194273f38cf70c0f59635d68154f249", "packages": [], "packages-dev": [ { @@ -693,16 +693,16 @@ }, { "name": "nextcloud/ocp", - "version": "dev-stable28", + "version": "dev-stable30", "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "53924b92b5e16225b0bc222068c29fe9e91d445a" + "reference": "d93fc10fea3db4b4896e37db312fae685cff54c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/53924b92b5e16225b0bc222068c29fe9e91d445a", - "reference": "53924b92b5e16225b0bc222068c29fe9e91d445a", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/d93fc10fea3db4b4896e37db312fae685cff54c4", + "reference": "d93fc10fea3db4b4896e37db312fae685cff54c4", "shasum": "" }, "require": { @@ -710,12 +710,12 @@ "psr/clock": "^1.0", "psr/container": "^2.0.2", "psr/event-dispatcher": "^1.0", - "psr/log": "^1.1.4" + "psr/log": "^2.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-stable28": "28.0.0-dev" + "dev-stable30": "30.0.0-dev" } }, "notification-url": "https://packagist.org/downloads/", @@ -731,9 +731,9 @@ "description": "Composer package containing Nextcloud's public API (classes, interfaces)", "support": { "issues": "https://github.com/nextcloud-deps/ocp/issues", - "source": "https://github.com/nextcloud-deps/ocp/tree/stable28" + "source": "https://github.com/nextcloud-deps/ocp/tree/stable30" }, - "time": "2025-03-05T00:45:45+00:00" + "time": "2025-12-02T00:53:40+00:00" }, { "name": "nikic/php-parser", @@ -1166,30 +1166,30 @@ }, { "name": "psr/log", - "version": "1.1.4", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1210,9 +1210,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/2.0.0" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2021-07-14T16:41:46+00:00" }, { "name": "sabre/dav", @@ -2690,5 +2690,5 @@ "platform-overrides": { "php": "8.1.32" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" }