From d2e032ff4e2a686d765dec3fc095cbed8139ef25 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 16 Dec 2025 00:46:46 +0100 Subject: [PATCH 1/3] fix(Session): restore session data after cookie login when a browser is closed and the session lost, a new session might be started with the help of the remember me cookie. For we keep some data in the session, like the IdP identifier, and some SAML data needed for SLO, those were lost in this process. This made especially logout behaviour not acting as expected. Now we keep the required data also in the database and can restore it on a cookie login event. Storing the session data in the database can only happen after the session token was created, i.e. when the UserLoggedInEvent was dispatched. Stored session data is deleted once the original authtoken has also disappeared. A daily check during maintenance times is scheduled. Because hashing of the session id is a private implementation detail of the current token provider, we track the id of the token as well so that any future change in hashing will not have any effect. Signed-off-by: Arthur Schiwon --- appinfo/info.xml | 7 +- lib/AppInfo/Application.php | 12 ++- lib/Controller/SAMLController.php | 24 ++--- lib/DavPlugin.php | 21 ++-- lib/Db/SessionData.php | 46 +++++++++ lib/Db/SessionDataMapper.php | 36 +++++++ lib/Jobs/CleanSessionData.php | 62 ++++++++++++ lib/Listener/CookieLoginEventListener.php | 48 +++++++++ lib/Listener/LoginEventListener.php | 31 ++++++ .../Version7001Date20251215192613.php | 47 +++++++++ lib/Model/SessionData.php | 89 +++++++++++++++++ lib/Service/SessionService.php | 98 +++++++++++++++++++ lib/UserBackend.php | 3 +- tests/unit/Controller/SAMLControllerTest.php | 6 +- 14 files changed, 501 insertions(+), 29 deletions(-) create mode 100644 lib/Db/SessionData.php create mode 100644 lib/Db/SessionDataMapper.php create mode 100644 lib/Jobs/CleanSessionData.php create mode 100644 lib/Listener/CookieLoginEventListener.php create mode 100644 lib/Listener/LoginEventListener.php create mode 100644 lib/Migration/Version7001Date20251215192613.php create mode 100644 lib/Model/SessionData.php create mode 100644 lib/Service/SessionService.php 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/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 ); } From 93ece7d630efbde04262c4b2301f7a14587382c7 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 17 Dec 2025 17:29:12 +0100 Subject: [PATCH 2/3] ci(dev-deps): bump nc/ocp to stable30 Signed-off-by: Arthur Schiwon --- vendor-bin/psalm/composer.json | 2 +- vendor-bin/psalm/composer.lock | 38 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) 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" } From cc0d7a692c561868c3cf9e01b2943342b27976b8 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 17 Dec 2025 17:29:58 +0100 Subject: [PATCH 3/3] ci(composer): run php-cs-fixer and psalm with PHP of the callee Signed-off-by: Arthur Schiwon --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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",