diff --git a/.github/workflows/integration-sqlite.yml b/.github/workflows/integration-sqlite.yml index 5eb89c23761f4..c0d3f701a498e 100644 --- a/.github/workflows/integration-sqlite.yml +++ b/.github/workflows/integration-sqlite.yml @@ -69,6 +69,7 @@ jobs: - 'setup_features' - 'sharees_features' - 'sharing_features' + - 'theming_features' - 'videoverification_features' php-versions: ['8.1'] diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index a3018c3966c0a..f27183e920c48 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -255,4 +255,14 @@ default: admin: - admin - admin - regular_user_password: 123456 \ No newline at end of file + regular_user_password: 123456 + theming: + paths: + - "%paths.base%/../theming_features" + contexts: + - FeatureContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index a8c232d6fe744..0290297dd884f 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -19,6 +19,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 0000000000000..f44a6533a1bda --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/theming_features/theming.feature b/build/integration/theming_features/theming.feature new file mode 100644 index 0000000000000..2ae5d4f75c377 --- /dev/null +++ b/build/integration/theming_features/theming.feature @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: theming + + Background: + Given user "user0" exists + + Scenario: themed stylesheets are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + # Themes that can not be explicitly set by a guest could have been + # globally set too through "enforce_theme". + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed images are available for users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for guests + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for disabled users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" diff --git a/lib/base.php b/lib/base.php index f8a90bd63f269..63f2e758428f7 100644 --- a/lib/base.php +++ b/lib/base.php @@ -11,6 +11,7 @@ use OC\Share20\Hooks; use OC\Share20\UserDeletedListener; use OC\Share20\UserRemovedListener; +use OC\User\DisabledUserException; use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Events\GroupDeletedEvent; use OCP\Group\Events\UserRemovedEvent; @@ -1026,7 +1027,27 @@ public static function handleRequest(): void { // OAuth needs to support basic auth too, so the login is not valid // inside Nextcloud and the Login exception would ruin it. if ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') { - self::handleLogin($request); + try { + self::handleLogin($request); + } catch (DisabledUserException $e) { + // Disabled users would not be seen as logged in and + // trying to log them in would fail, so the login + // exception is ignored for the themed stylesheets and + // images. + if ($request->getRawPathInfo() !== '/apps/theming/theme/default.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/opendyslexic.css' + && $request->getRawPathInfo() !== '/apps/theming/image/background' + && $request->getRawPathInfo() !== '/apps/theming/image/logo' + && $request->getRawPathInfo() !== '/apps/theming/image/logoheader' + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/favicon') + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/icon')) { + throw $e; + } + } } } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fc0694124f8c9..5b7821a74387b 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2090,6 +2090,7 @@ 'OC\\User\\Backend' => $baseDir . '/lib/private/User/Backend.php', 'OC\\User\\BackgroundJobs\\CleanupDeletedUsers' => $baseDir . '/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php', 'OC\\User\\Database' => $baseDir . '/lib/private/User/Database.php', + 'OC\\User\\DisabledUserException' => $baseDir . '/lib/private/User/DisabledUserException.php', 'OC\\User\\DisplayNameCache' => $baseDir . '/lib/private/User/DisplayNameCache.php', 'OC\\User\\LazyUser' => $baseDir . '/lib/private/User/LazyUser.php', 'OC\\User\\Listeners\\BeforeUserDeletedListener' => $baseDir . '/lib/private/User/Listeners/BeforeUserDeletedListener.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 98d74bd5dce94..1c22c0957f2f2 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2139,6 +2139,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\User\\Backend' => __DIR__ . '/../../..' . '/lib/private/User/Backend.php', 'OC\\User\\BackgroundJobs\\CleanupDeletedUsers' => __DIR__ . '/../../..' . '/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php', 'OC\\User\\Database' => __DIR__ . '/../../..' . '/lib/private/User/Database.php', + 'OC\\User\\DisabledUserException' => __DIR__ . '/../../..' . '/lib/private/User/DisabledUserException.php', 'OC\\User\\DisplayNameCache' => __DIR__ . '/../../..' . '/lib/private/User/DisplayNameCache.php', 'OC\\User\\LazyUser' => __DIR__ . '/../../..' . '/lib/private/User/LazyUser.php', 'OC\\User\\Listeners\\BeforeUserDeletedListener' => __DIR__ . '/../../..' . '/lib/private/User/Listeners/BeforeUserDeletedListener.php', diff --git a/lib/private/User/DisabledUserException.php b/lib/private/User/DisabledUserException.php new file mode 100644 index 0000000000000..db8a23d202752 --- /dev/null +++ b/lib/private/User/DisabledUserException.php @@ -0,0 +1,10 @@ +t('Account disabled'); - throw new LoginException($message); + throw new DisabledUserException($message); } if ($regenerateSessionId) { diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index 4cc102d6672b4..6fcf740397881 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -6,7 +6,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ use OC\Authentication\Token\IProvider; -use OC\User\LoginException; +use OC\User\DisabledUserException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; @@ -157,7 +157,7 @@ public static function loginWithApache(\OCP\Authentication\IApacheBackend $backe if ($userSession->getUser() && !$userSession->getUser()->isEnabled()) { $message = \OC::$server->getL10N('lib')->t('Account disabled'); - throw new LoginException($message); + throw new DisabledUserException($message); } $userSession->setLoginName($uid); $request = OC::$server->getRequest();