Revert "perf(base): Stop setting up the FS for every basic auth request"

pull/54717/head
Joas Schilling 2025-08-28 17:11:31 +07:00 committed by GitHub
parent 40117dced3
commit 3df6d90a4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 930 additions and 27 deletions

@ -196,7 +196,6 @@ abstract class TestCase extends \Test\TestCase {
Server::get(IUserSession::class)->setUser(null); Server::get(IUserSession::class)->setUser(null);
Filesystem::tearDown(); Filesystem::tearDown();
Server::get(IUserSession::class)->login($user, $password); Server::get(IUserSession::class)->login($user, $password);
Filesystem::initMountPoints($user);
\OC::$server->getUserFolder($user); \OC::$server->getUserFolder($user);
\OC_Util::setupFS($user); \OC_Util::setupFS($user);

@ -182,7 +182,8 @@ export default {
if (data.status === 'success') { if (data.status === 'success') {
this.handleAvatarUpdate(false) this.handleAvatarUpdate(false)
} else if (data.data === 'notsquare') { } else if (data.data === 'notsquare') {
this.$refs.cropper.replace(data.image) const tempAvatar = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
this.$refs.cropper.replace(tempAvatar)
this.showCropper = true this.showCropper = true
} else { } else {
showError(data.data.message) showError(data.data.message)

@ -21,9 +21,32 @@ Feature: avatar
And last avatar is a square of size 512 And last avatar is a square of size 512
And last avatar is not a single color And last avatar is not a single color
Scenario: get temporary non-square user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user gets temporary avatar
Then The following headers should be set
| Content-Type | image/png |
# "last avatar" also includes the last temporary avatar
And last avatar is not a square
And last avatar is not a single color
Scenario: get non-square user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
# Avatar needs to be cropped to finish setting it
When user "user0" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 512
And last avatar is not a single color
Scenario: set square user avatar from file Scenario: set square user avatar from file
Given Logging in using web as "user0" Given Logging in using web as "user0"
When logged in user posts avatar from file "data/green-square-256.png" When logged in user posts temporary avatar from file "data/green-square-256.png"
And user "user0" gets avatar for user "user0" And user "user0" gets avatar for user "user0"
And The following headers should be set And The following headers should be set
| Content-Type | image/png | | Content-Type | image/png |
@ -41,7 +64,7 @@ Feature: avatar
Scenario: set square user avatar from internal path Scenario: set square user avatar from internal path
Given user "user0" uploads file "data/green-square-256.png" to "/internal-green-square-256.png" Given user "user0" uploads file "data/green-square-256.png" to "/internal-green-square-256.png"
And Logging in using web as "user0" And Logging in using web as "user0"
When logged in user posts avatar from internal path "internal-green-square-256.png" When logged in user posts temporary avatar from internal path "internal-green-square-256.png"
And user "user0" gets avatar for user "user0" with size "64" And user "user0" gets avatar for user "user0" with size "64"
And The following headers should be set And The following headers should be set
| Content-Type | image/png | | Content-Type | image/png |
@ -55,21 +78,82 @@ Feature: avatar
And last avatar is a square of size 64 And last avatar is a square of size 64
And last avatar is a single "#00FF00" color And last avatar is a single "#00FF00" color
Scenario: delete user avatar Scenario: set non-square user avatar from file
Given Logging in using web as "user0" Given Logging in using web as "user0"
And logged in user posts avatar from file "data/green-square-256.png" When logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0" And user "user0" gets avatar for user "user0"
And The following headers should be set And The following headers should be set
| Content-Type | image/png | | Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 | | X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512 And last avatar is a square of size 512
And last avatar is a single "#00FF00" color And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0" And user "anonymous" gets avatar for user "user0"
And The following headers should be set And The following headers should be set
| Content-Type | image/png | | Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 | | X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512 And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: set non-square user avatar from internal path
Given user "user0" uploads file "data/coloured-pattern-non-square.png" to "/internal-coloured-pattern-non-square.png"
And Logging in using web as "user0"
When logged in user posts temporary avatar from internal path "internal-coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 704 |
| y | 320 |
| w | 64 |
| h | 64 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color And last avatar is a single "#00FF00" color
And user "anonymous" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
Scenario: cropped user avatar needs to be squared
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user crops temporary avatar with 400
| x | 384 |
| y | 256 |
| w | 192 |
| h | 128 |
Scenario: delete user avatar
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
When logged in user deletes the user avatar When logged in user deletes the user avatar
Then user "user0" gets avatar for user "user0" Then user "user0" gets avatar for user "user0"
And The following headers should be set And The following headers should be set
@ -84,6 +168,40 @@ Feature: avatar
And last avatar is a square of size 512 And last avatar is a square of size 512
And last avatar is not a single color And last avatar is not a single color
Scenario: get user avatar with a larger size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "192"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: get user avatar with a smaller size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "96"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: get default guest avatar Scenario: get default guest avatar
When user "user0" gets avatar for guest "guest0" When user "user0" gets avatar for guest "guest0"
Then The following headers should be set Then The following headers should be set

@ -4,6 +4,7 @@
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php'; require __DIR__ . '/../../vendor/autoload.php';
@ -67,11 +68,30 @@ trait Avatar {
} }
/** /**
* @When logged in user posts avatar from file :source * @When logged in user gets temporary avatar
*/
public function loggedInUserGetsTemporaryAvatar() {
$this->loggedInUserGetsTemporaryAvatarWith('200');
}
/**
* @When logged in user gets temporary avatar with :statusCode
*
* @param string $statusCode
*/
public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) {
$this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp');
$this->theHTTPStatusCodeShouldBe($statusCode);
$this->getLastAvatar();
}
/**
* @When logged in user posts temporary avatar from file :source
* *
* @param string $source * @param string $source
*/ */
public function loggedInUserPostsAvatarFromFile(string $source) { public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
$file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar', $this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
@ -87,15 +107,40 @@ trait Avatar {
} }
/** /**
* @When logged in user posts avatar from internal path :path * @When logged in user posts temporary avatar from internal path :path
* *
* @param string $path * @param string $path
*/ */
public function loggedInUserPostsAvatarFromInternalPath(string $path) { public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path); $this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
$this->theHTTPStatusCodeShouldBe('200'); $this->theHTTPStatusCodeShouldBe('200');
} }
/**
* @When logged in user crops temporary avatar
*
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatar(TableNode $crop) {
$this->loggedInUserCropsTemporaryAvatarWith('200', $crop);
}
/**
* @When logged in user crops temporary avatar with :statusCode
*
* @param string $statusCode
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) {
$parameters = [];
foreach ($crop->getRowsHash() as $key => $value) {
$parameters[] = 'crop[' . $key . ']=' . $value;
}
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters));
$this->theHTTPStatusCodeShouldBe($statusCode);
}
/** /**
* @When logged in user deletes the user avatar * @When logged in user deletes the user avatar
*/ */

@ -46,7 +46,7 @@ Feature: sharing
Scenario: Correct webdav share-permissions for owned folder Scenario: Correct webdav share-permissions for owned folder
Given user "user0" exists Given user "user0" exists
And user "user0" created a folder "/tmp" And user "user0" created a folder "/tmp"
When as "user0" gets properties of folder "/tmp" with When as "user0" gets properties of folder "/" with
|{http://open-collaboration-services.org/ns}share-permissions | |{http://open-collaboration-services.org/ns}share-permissions |
Then the single response should contain a property "{http://open-collaboration-services.org/ns}share-permissions" with value "31" Then the single response should contain a property "{http://open-collaboration-services.org/ns}share-permissions" with value "31"

@ -3440,6 +3440,17 @@
<code><![CDATA[$this->providers]]></code> <code><![CDATA[$this->providers]]></code>
</UndefinedInterfaceMethod> </UndefinedInterfaceMethod>
</file> </file>
<file src="lib/private/Cache/CappedMemoryCache.php">
<MissingTemplateParam>
<code><![CDATA[\ArrayAccess]]></code>
</MissingTemplateParam>
</file>
<file src="lib/private/Cache/File.php">
<LessSpecificImplementedReturnType>
<code><![CDATA[bool|mixed]]></code>
<code><![CDATA[bool|mixed]]></code>
</LessSpecificImplementedReturnType>
</file>
<file src="lib/private/Calendar/Manager.php"> <file src="lib/private/Calendar/Manager.php">
<LessSpecificReturnStatement> <LessSpecificReturnStatement>
<code><![CDATA[array_merge( <code><![CDATA[array_merge(

@ -8,6 +8,7 @@
namespace OC\Core\Controller; namespace OC\Core\Controller;
use OC\AppFramework\Utility\TimeFactory; use OC\AppFramework\Utility\TimeFactory;
use OC\NotSquareException;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Attribute\FrontpageRoute;
@ -15,6 +16,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\Response;
@ -22,6 +24,7 @@ use OCP\Files\File;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException; use OCP\Files\NotPermittedException;
use OCP\IAvatarManager; use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N; use OCP\IL10N;
use OCP\Image; use OCP\Image;
use OCP\IRequest; use OCP\IRequest;
@ -38,6 +41,7 @@ class AvatarController extends Controller {
string $appName, string $appName,
IRequest $request, IRequest $request,
protected IAvatarManager $avatarManager, protected IAvatarManager $avatarManager,
protected ICache $cache,
protected IL10N $l10n, protected IL10N $l10n,
protected IUserManager $userManager, protected IUserManager $userManager,
protected IRootFolder $rootFolder, protected IRootFolder $rootFolder,
@ -198,7 +202,8 @@ class AvatarController extends Controller {
Http::STATUS_BAD_REQUEST Http::STATUS_BAD_REQUEST
); );
} }
$content = file_get_contents($files['tmp_name'][0]); $this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200);
$content = $this->cache->get('avatar_upload');
unlink($files['tmp_name'][0]); unlink($files['tmp_name'][0]);
} else { } else {
$phpFileUploadErrors = [ $phpFileUploadErrors = [
@ -245,6 +250,8 @@ class AvatarController extends Controller {
try { try {
$avatar = $this->avatarManager->getAvatar($this->userId); $avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image); $avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']); return new JSONResponse(['status' => 'success']);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']); $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
@ -252,8 +259,9 @@ class AvatarController extends Controller {
} }
} }
$this->cache->set('tmpAvatar', $image->data(), 7200);
return new JSONResponse( return new JSONResponse(
['data' => 'notsquare', 'image' => 'data:' . $mimeType . ';base64,' . base64_encode($image->data())], ['data' => 'notsquare'],
Http::STATUS_OK Http::STATUS_OK
); );
} else { } else {
@ -280,4 +288,71 @@ class AvatarController extends Controller {
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST); return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
} }
} }
/**
* @return JSONResponse|DataDisplayResponse
*/
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/avatar/tmp')]
public function getTmpAvatar() {
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t('No temporary profile picture available, try again')
]],
Http::STATUS_NOT_FOUND);
}
$image = new Image();
$image->loadFromData($tmpAvatar);
$resp = new DataDisplayResponse(
$image->data() ?? '',
Http::STATUS_OK,
['Content-Type' => $image->mimeType()]);
$resp->setETag((string)crc32($image->data() ?? ''));
$resp->cacheFor(0);
$resp->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
return $resp;
}
#[NoAdminRequired]
#[FrontpageRoute(verb: 'POST', url: '/avatar/cropped')]
public function postCroppedAvatar(?array $crop = null): JSONResponse {
if (is_null($crop)) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('No crop data provided')]],
Http::STATUS_BAD_REQUEST);
}
if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('No valid crop data provided')]],
Http::STATUS_BAD_REQUEST);
}
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t('No temporary profile picture available, try again')
]],
Http::STATUS_BAD_REQUEST);
}
$image = new Image();
$image->loadFromData($tmpAvatar);
$image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h']));
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (NotSquareException $e) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('Crop is not square')]],
Http::STATUS_BAD_REQUEST);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -882,6 +882,23 @@ class OC {
$throttler = Server::get(IThrottler::class); $throttler = Server::get(IThrottler::class);
$throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]); $throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]);
} }
try {
$cache = new \OC\Cache\File();
$cache->gc();
} catch (\OC\ServerNotAvailableException $e) {
// not a GC exception, pass it on
throw $e;
} catch (\OC\ForbiddenException $e) {
// filesystem blocked for this request, ignore
} catch (\Exception $e) {
// a GC exception should not prevent users from using OC,
// so log the exception
Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [
'app' => 'core',
'exception' => $e,
]);
}
}); });
} }
} }

@ -1183,6 +1183,8 @@ return array(
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php', 'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php', 'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',

@ -1224,6 +1224,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php', 'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php', 'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php',

@ -0,0 +1,109 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Cache;
use OCP\ICache;
/**
* In-memory cache with a capacity limit to keep memory usage in check
*
* Uses a simple FIFO expiry mechanism
* @template T
* @deprecated 25.0.0 use OCP\Cache\CappedMemoryCache instead
*/
class CappedMemoryCache implements ICache, \ArrayAccess {
private $capacity;
/** @var T[] */
private $cache = [];
public function __construct($capacity = 512) {
$this->capacity = $capacity;
}
public function hasKey($key): bool {
return isset($this->cache[$key]);
}
/**
* @return ?T
*/
public function get($key) {
return $this->cache[$key] ?? null;
}
/**
* @param string $key
* @param T $value
* @param int $ttl
* @return bool
*/
public function set($key, $value, $ttl = 0): bool {
if (is_null($key)) {
$this->cache[] = $value;
} else {
$this->cache[$key] = $value;
}
$this->garbageCollect();
return true;
}
public function remove($key) {
unset($this->cache[$key]);
return true;
}
public function clear($prefix = '') {
$this->cache = [];
return true;
}
public function offsetExists($offset): bool {
return $this->hasKey($offset);
}
/**
* @return T
*/
#[\ReturnTypeWillChange]
public function &offsetGet($offset) {
return $this->cache[$offset];
}
/**
* @param string $offset
* @param T $value
* @return void
*/
public function offsetSet($offset, $value): void {
$this->set($offset, $value);
}
public function offsetUnset($offset): void {
$this->remove($offset);
}
/**
* @return T[]
*/
public function getData() {
return $this->cache;
}
private function garbageCollect() {
while (count($this->cache) > $this->capacity) {
reset($this->cache);
$key = key($this->cache);
$this->remove($key);
}
}
public static function isAvailable(): bool {
return true;
}
}

@ -0,0 +1,190 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Cache;
use OC\Files\Filesystem;
use OC\Files\View;
use OCP\ICache;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCP\Server;
use Psr\Log\LoggerInterface;
class File implements ICache {
/** @var View */
protected $storage;
/**
* Returns the cache storage for the logged in user
*
* @return \OC\Files\View cache storage
* @throws \OC\ForbiddenException
* @throws \OC\User\NoUserException
*/
protected function getStorage() {
if ($this->storage !== null) {
return $this->storage;
}
$session = Server::get(IUserSession::class);
if ($session->isLoggedIn()) {
$rootView = new View();
$userId = $session->getUser()->getUID();
Filesystem::initMountPoints($userId);
if (!$rootView->file_exists('/' . $userId . '/cache')) {
$rootView->mkdir('/' . $userId . '/cache');
}
$this->storage = new View('/' . $userId . '/cache');
return $this->storage;
} else {
Server::get(LoggerInterface::class)->error('Can\'t get cache storage, user not logged in', ['app' => 'core']);
throw new \OC\ForbiddenException('Can\t get cache storage, user not logged in');
}
}
/**
* @param string $key
* @return mixed|null
* @throws \OC\ForbiddenException
*/
public function get($key) {
$result = null;
if ($this->hasKey($key)) {
$storage = $this->getStorage();
$result = $storage->file_get_contents($key);
}
return $result;
}
/**
* Returns the size of the stored/cached data
*
* @param string $key
* @return int
*/
public function size($key) {
$result = 0;
if ($this->hasKey($key)) {
$storage = $this->getStorage();
$result = $storage->filesize($key);
}
return $result;
}
/**
* @param string $key
* @param mixed $value
* @param int $ttl
* @return bool|mixed
* @throws \OC\ForbiddenException
*/
public function set($key, $value, $ttl = 0) {
$storage = $this->getStorage();
$result = false;
// unique id to avoid chunk collision, just in case
$uniqueId = Server::get(ISecureRandom::class)->generate(
16,
ISecureRandom::CHAR_ALPHANUMERIC
);
// use part file to prevent hasKey() to find the key
// while it is being written
$keyPart = $key . '.' . $uniqueId . '.part';
if ($storage && $storage->file_put_contents($keyPart, $value)) {
if ($ttl === 0) {
$ttl = 86400; // 60*60*24
}
$result = $storage->touch($keyPart, time() + $ttl);
$result &= $storage->rename($keyPart, $key);
}
return $result;
}
/**
* @param string $key
* @return bool
* @throws \OC\ForbiddenException
*/
public function hasKey($key) {
$storage = $this->getStorage();
if ($storage && $storage->is_file($key) && $storage->isReadable($key)) {
return true;
}
return false;
}
/**
* @param string $key
* @return bool|mixed
* @throws \OC\ForbiddenException
*/
public function remove($key) {
$storage = $this->getStorage();
if (!$storage) {
return false;
}
return $storage->unlink($key);
}
/**
* @param string $prefix
* @return bool
* @throws \OC\ForbiddenException
*/
public function clear($prefix = '') {
$storage = $this->getStorage();
if ($storage && $storage->is_dir('/')) {
$dh = $storage->opendir('/');
if (is_resource($dh)) {
while (($file = readdir($dh)) !== false) {
if ($file !== '.' && $file !== '..' && ($prefix === '' || str_starts_with($file, $prefix))) {
$storage->unlink('/' . $file);
}
}
}
}
return true;
}
/**
* Runs GC
* @throws \OC\ForbiddenException
*/
public function gc() {
$storage = $this->getStorage();
if ($storage) {
// extra hour safety, in case of stray part chunks that take longer to write,
// because touch() is only called after the chunk was finished
$now = time() - 3600;
$dh = $storage->opendir('/');
if (!is_resource($dh)) {
return null;
}
while (($file = readdir($dh)) !== false) {
if ($file !== '.' && $file !== '..') {
try {
$mtime = $storage->filemtime('/' . $file);
if ($mtime < $now) {
$storage->unlink('/' . $file);
}
} catch (\OCP\Lock\LockedException $e) {
// ignore locked chunks
Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
} catch (\OCP\Files\ForbiddenException $e) {
Server::get(LoggerInterface::class)->debug('Could not cleanup forbidden chunk "' . $file . '"', ['app' => 'core']);
} catch (\OCP\Files\LockNotAcquiredException $e) {
Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
}
}
}
}
}
public static function isAvailable(): bool {
return true;
}
}

@ -585,16 +585,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IURLGenerator::class, URLGenerator::class); $this->registerAlias(IURLGenerator::class, URLGenerator::class);
$this->registerService(ICache::class, function ($c) { $this->registerAlias(ICache::class, Cache\File::class);
/** @var LoggerInterface $logger */
$logger = $c->get(LoggerInterface::class);
$logger->debug('The requested service "' . ICache::class . '" is deprecated. Please use "' . ICacheFactory::class . '" instead to create a cache. This service will be removed in a future Nextcloud version.', ['app' => 'serverDI']);
/** @var ICacheFactory $cacheFactory */
$cacheFactory = $c->get(ICacheFactory::class);
return $cacheFactory->isLocalCacheAvailable() ? $cacheFactory->createLocal() : $cacheFactory->createInMemory();
});
$this->registerService(Factory::class, function (Server $c) { $this->registerService(Factory::class, function (Server $c) {
$profiler = $c->get(IProfiler::class); $profiler = $c->get(IProfiler::class);
$logger = $c->get(LoggerInterface::class); $logger = $c->get(LoggerInterface::class);

@ -29,6 +29,7 @@ use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IAvatar; use OCP\IAvatar;
use OCP\IAvatarManager; use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N; use OCP\IL10N;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUser; use OCP\IUser;
@ -54,6 +55,8 @@ class AvatarControllerTest extends \Test\TestCase {
private $avatarFile; private $avatarFile;
/** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */ /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */
private $avatarManager; private $avatarManager;
/** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
private $cache;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
private $l; private $l;
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
@ -71,6 +74,8 @@ class AvatarControllerTest extends \Test\TestCase {
parent::setUp(); parent::setUp();
$this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock(); $this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock();
$this->cache = $this->getMockBuilder('OCP\ICache')
->disableOriginalConstructor()->getMock();
$this->l = $this->getMockBuilder(IL10N::class)->getMock(); $this->l = $this->getMockBuilder(IL10N::class)->getMock();
$this->l->method('t')->willReturnArgument(0); $this->l->method('t')->willReturnArgument(0);
$this->userManager = $this->getMockBuilder(IUserManager::class)->getMock(); $this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
@ -93,6 +98,7 @@ class AvatarControllerTest extends \Test\TestCase {
'core', 'core',
$this->request, $this->request,
$this->avatarManager, $this->avatarManager,
$this->cache,
$this->l, $this->l,
$this->userManager, $this->userManager,
$this->rootFolder, $this->rootFolder,
@ -292,6 +298,25 @@ class AvatarControllerTest extends \Test\TestCase {
$this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar()); $this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar());
} }
/**
* Trying to get a tmp avatar when it is not available. 404
*/
public function testTmpAvatarNoTmp(): void {
$response = $this->avatarController->getTmpAvatar();
$this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
}
/**
* Fetch tmp avatar
*/
public function testTmpAvatarValid(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$response = $this->avatarController->getTmpAvatar();
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
}
/** /**
* When trying to post a new avatar a path or image should be posted. * When trying to post a new avatar a path or image should be posted.
*/ */
@ -310,6 +335,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName); $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName);
$this->assertTrue($copyRes); $this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
//Create request return //Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]]; $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]];
$this->request->method('getUploadedFile')->willReturn($reqRet); $this->request->method('getUploadedFile')->willReturn($reqRet);
@ -345,6 +373,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName); $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName);
$this->assertTrue($copyRes); $this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.gif'));
//Create request return //Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]]; $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]];
$this->request->method('getUploadedFile')->willReturn($reqRet); $this->request->method('getUploadedFile')->willReturn($reqRet);
@ -433,6 +464,93 @@ class AvatarControllerTest extends \Test\TestCase {
$this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg')); $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
} }
/**
* Test what happens if the upload of the avatar fails
*/
public function testPostAvatarException(): void {
$this->cache->expects($this->once())
->method('set')
->willThrowException(new \Exception('foo'));
$file = $this->getMockBuilder('OCP\Files\File')
->disableOriginalConstructor()->getMock();
$file->expects($this->once())
->method('getContent')
->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$file->expects($this->once())
->method('getMimeType')
->willReturn('image/jpeg');
$userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
$this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
$userFolder->method('get')->willReturn($file);
$this->logger->expects($this->once())
->method('error')
->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
$expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_OK);
$this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
}
/**
* Test invalid crop argument
*/
public function testPostCroppedAvatarInvalidCrop(): void {
$response = $this->avatarController->postCroppedAvatar([]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Test no tmp avatar to crop
*/
public function testPostCroppedAvatarNoTmpAvatar(): void {
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Test with non square crop
*/
public function testPostCroppedAvatarNoSquareCrop(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarMock->method('set')->willThrowException(new \OC\NotSquareException);
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Check for proper reply on proper crop argument
*/
public function testPostCroppedAvatarValidCrop(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
$this->assertEquals('success', $response->getData()['status']);
}
/**
* Test what happens if the cropping of the avatar fails
*/
public function testPostCroppedAvatarException(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarMock->method('set')->willThrowException(new \Exception('foo'));
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$this->logger->expects($this->once())
->method('error')
->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
$expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST);
$this->assertEquals($expectedResponse, $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]));
}
/** /**
* Check for proper reply on proper crop argument * Check for proper reply on proper crop argument
*/ */

@ -0,0 +1,65 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Cache;
use OCP\Cache\CappedMemoryCache;
/**
* Class CappedMemoryCacheTest
*
* @package Test\Cache
*/
class CappedMemoryCacheTest extends TestCache {
protected function setUp(): void {
parent::setUp();
$this->instance = new CappedMemoryCache();
}
public function testSetOverCap(): void {
$instance = new CappedMemoryCache(3);
$instance->set('1', 'a');
$instance->set('2', 'b');
$instance->set('3', 'c');
$instance->set('4', 'd');
$instance->set('5', 'e');
$this->assertFalse($instance->hasKey('1'));
$this->assertFalse($instance->hasKey('2'));
$this->assertTrue($instance->hasKey('3'));
$this->assertTrue($instance->hasKey('4'));
$this->assertTrue($instance->hasKey('5'));
}
public function testClear(): void {
$value = 'ipsum lorum';
$this->instance->set('1_value1', $value);
$this->instance->set('1_value2', $value);
$this->instance->set('2_value1', $value);
$this->instance->set('3_value1', $value);
$this->assertTrue($this->instance->clear());
$this->assertFalse($this->instance->hasKey('1_value1'));
$this->assertFalse($this->instance->hasKey('1_value2'));
$this->assertFalse($this->instance->hasKey('2_value1'));
$this->assertFalse($this->instance->hasKey('3_value1'));
}
public function testIndirectSet(): void {
$this->instance->set('array', []);
$this->instance['array'][] = 'foo';
$this->assertEquals(['foo'], $this->instance->get('array'));
$this->instance['array']['bar'] = 'qwerty';
$this->assertEquals(['foo', 'bar' => 'qwerty'], $this->instance->get('array'));
}
}

@ -0,0 +1,160 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Cache;
use OC\Cache\File;
use OC\Files\Filesystem;
use OC\Files\Storage\Local;
use OC\Files\Storage\Storage;
use OC\Files\Storage\Temporary;
use OC\Files\View;
use OCP\Files\LockNotAcquiredException;
use OCP\Files\Mount\IMountManager;
use OCP\ITempManager;
use OCP\Lock\LockedException;
use OCP\Server;
use Test\Traits\UserTrait;
/**
* Class FileCacheTest
*
* @group DB
*
* @package Test\Cache
*/
class FileCacheTest extends TestCache {
use UserTrait;
/**
* @var string
* */
private $user;
/**
* @var string
* */
private $datadir;
/**
* @var Storage
* */
private $storage;
/**
* @var View
* */
private $rootView;
public function skip() {
//$this->skipUnless(OC_User::isLoggedIn());
}
protected function setUp(): void {
parent::setUp();
//login
$this->createUser('test', 'test');
$this->user = \OC_User::getUser();
\OC_User::setUserId('test');
//clear all proxies and hooks so we can do clean testing
\OC_Hook::clear('OC_Filesystem');
/** @var IMountManager $manager */
$manager = Server::get(IMountManager::class);
$manager->removeMount('/test');
$storage = new Temporary([]);
Filesystem::mount($storage, [], '/test/cache');
//set up the users dir
$this->rootView = new View('');
$this->rootView->mkdir('/test');
$this->instance = new File();
// forces creation of cache folder for subsequent tests
$this->instance->set('hack', 'hack');
}
protected function tearDown(): void {
if ($this->instance) {
$this->instance->remove('hack', 'hack');
}
\OC_User::setUserId($this->user);
if ($this->instance) {
$this->instance->clear();
$this->instance = null;
}
parent::tearDown();
}
private function setupMockStorage() {
$mockStorage = $this->getMockBuilder(Local::class)
->onlyMethods(['filemtime', 'unlink'])
->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]])
->getMock();
Filesystem::mount($mockStorage, [], '/test/cache');
return $mockStorage;
}
public function testGarbageCollectOldKeys(): void {
$mockStorage = $this->setupMockStorage();
$mockStorage->expects($this->atLeastOnce())
->method('filemtime')
->willReturn(100);
$mockStorage->expects($this->once())
->method('unlink')
->with('key1')
->willReturn(true);
$this->instance->set('key1', 'value1');
$this->instance->gc();
}
public function testGarbageCollectLeaveRecentKeys(): void {
$mockStorage = $this->setupMockStorage();
$mockStorage->expects($this->atLeastOnce())
->method('filemtime')
->willReturn(time() + 3600);
$mockStorage->expects($this->never())
->method('unlink')
->with('key1');
$this->instance->set('key1', 'value1');
$this->instance->gc();
}
public static function lockExceptionProvider(): array {
return [
[new LockedException('key1')],
[new LockNotAcquiredException('key1', 1)],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('lockExceptionProvider')]
public function testGarbageCollectIgnoreLockedKeys($testException): void {
$mockStorage = $this->setupMockStorage();
$mockStorage->expects($this->atLeastOnce())
->method('filemtime')
->willReturn(100);
$mockStorage->expects($this->atLeastOnce())
->method('unlink')->willReturnOnConsecutiveCalls($this->throwException($testException), $this->returnValue(true));
$this->instance->set('key1', 'value1');
$this->instance->set('key2', 'value2');
$this->instance->gc();
}
}