Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
- 'setup_features'
- 'sharees_features'
- 'sharing_features'
- 'theming_features'
- 'videoverification_features'

php-versions: ['8.1']
Expand Down
12 changes: 11 additions & 1 deletion build/integration/config/behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,14 @@ default:
admin:
- admin
- admin
regular_user_password: 123456
regular_user_password: 123456
theming:
paths:
- "%paths.base%/../theming_features"
contexts:
- FeatureContext:
baseUrl: http://localhost:8080
admin:
- admin
- admin
regular_user_password: 123456
1 change: 1 addition & 0 deletions build/integration/features/bootstrap/BasicStructure.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ trait BasicStructure {
use Avatar;
use Download;
use Mail;
use Theming;

/** @var string */
private $currentUser = '';
Expand Down
49 changes: 49 additions & 0 deletions build/integration/features/bootstrap/Theming.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
require __DIR__ . '/../../vendor/autoload.php';

trait Theming {

private bool $undoAllThemingChangesAfterScenario = false;

/**
* @AfterScenario
*/
public function undoAllThemingChanges() {
if (!$this->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');
}
}
131 changes: 131 additions & 0 deletions build/integration/theming_features/theming.feature
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 22 additions & 1 deletion lib/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions lib/private/User/DisabledUserException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\User;

class DisabledUserException extends LoginException {
}
2 changes: 1 addition & 1 deletion lib/private/User/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ public function completeLogin(IUser $user, array $loginDetails, $regenerateSessi
// disabled users can not log in
// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
$message = \OCP\Util::getL10N('lib')->t('Account disabled');
throw new LoginException($message);
throw new DisabledUserException($message);
}

if ($regenerateSessionId) {
Expand Down
4 changes: 2 additions & 2 deletions lib/private/legacy/OC_User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Loading