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

pull/53918/head
John Molakvoæ 3 months ago committed by GitHub
parent cf3ffb3fd1
commit 2b50d9b2c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/dav/appinfo/v1/caldav.php
  2. 2
      apps/dav/appinfo/v1/carddav.php
  3. 2
      apps/dav/appinfo/v1/webdav.php
  4. 17
      apps/dav/lib/Connector/Sabre/Auth.php
  5. 4
      apps/dav/lib/Server.php
  6. 8
      apps/dav/tests/unit/Connector/Sabre/AuthTest.php
  7. 3
      apps/settings/src/components/PersonalInfo/AvatarSection.vue
  8. BIN
      build/integration/data/coloured-pattern-non-square.png
  9. 2
      build/integration/data/coloured-pattern-non-square.png.license
  10. 128
      build/integration/features/avatar.feature
  11. 53
      build/integration/features/bootstrap/Avatar.php
  12. 11
      build/psalm-baseline.xml
  13. 79
      core/Controller/AvatarController.php
  14. 4
      dist/settings-vue-settings-personal-info.js
  15. 2
      dist/settings-vue-settings-personal-info.js.map
  16. 17
      lib/base.php
  17. 2
      lib/composer/composer/autoload_classmap.php
  18. 2
      lib/composer/composer/autoload_static.php
  19. 109
      lib/private/Cache/CappedMemoryCache.php
  20. 190
      lib/private/Cache/File.php
  21. 8
      lib/private/Server.php
  22. 118
      tests/Core/Controller/AvatarControllerTest.php
  23. 65
      tests/lib/Cache/CappedMemoryCacheTest.php
  24. 160
      tests/lib/Cache/FileCacheTest.php

@ -6,7 +6,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Backends
use OC\Files\SetupManager;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarRoot;
@ -41,7 +40,6 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(SetupManager::class),
'principals/'
);
$principalBackend = new Principal(

@ -6,7 +6,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Backends
use OC\Files\SetupManager;
use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
@ -42,7 +41,6 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(SetupManager::class),
'principals/'
);
$principalBackend = new Principal(

@ -6,7 +6,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Files\Filesystem;
use OC\Files\SetupManager;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\BearerAuth;
use OCA\DAV\Connector\Sabre\ServerFactory;
@ -56,7 +55,6 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(SetupManager::class),
'principals/'
);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);

@ -10,7 +10,6 @@ namespace OCA\DAV\Connector\Sabre;
use Exception;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Files\SetupManager;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
@ -38,7 +37,6 @@ class Auth extends AbstractBasic {
private IRequest $request,
private Manager $twoFactorManager,
private IThrottler $throttler,
private SetupManager $setupManager,
string $principalPrefix = 'principals/users/',
) {
$this->principalPrefix = $principalPrefix;
@ -185,13 +183,10 @@ class Auth extends AbstractBasic {
|| ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization')))
|| \OC_User::handleApacheAuth()
) {
$user = $this->userSession->getUser();
$this->setupManager->setupForUser($user);
$uid = $user->getUID();
$this->currentUser = $uid;
$user = $this->userSession->getUser()->getUID();
$this->currentUser = $user;
$this->session->close();
return [true, $this->principalPrefix . $uid];
return [true, $this->principalPrefix . $user];
}
}
@ -206,12 +201,6 @@ class Auth extends AbstractBasic {
$response->setStatus(Http::STATUS_UNAUTHORIZED);
throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
}
$user = $this->userSession->getUser();
if ($user !== null) {
$this->setupManager->setupForUser($user);
}
return $data;
}
}

@ -8,7 +8,6 @@
namespace OCA\DAV;
use OC\Files\Filesystem;
use OC\Files\SetupManager;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\BulkUpload\BulkUploadPlugin;
use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin;
@ -133,8 +132,7 @@ class Server {
\OCP\Server::get(IUserSession::class),
\OCP\Server::get(IRequest::class),
\OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
\OCP\Server::get(IThrottler::class),
\OCP\Server::get(SetupManager::class),
\OCP\Server::get(IThrottler::class)
);
// Set URL explicitly due to reverse-proxy situations

@ -10,7 +10,6 @@ namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Files\SetupManager;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
@ -36,7 +35,6 @@ class AuthTest extends TestCase {
private IRequest&MockObject $request;
private Manager&MockObject $twoFactorManager;
private IThrottler&MockObject $throttler;
private SetupManager&MockObject $setupManager;
private Auth $auth;
protected function setUp(): void {
@ -46,14 +44,12 @@ class AuthTest extends TestCase {
$this->request = $this->createMock(IRequest::class);
$this->twoFactorManager = $this->createMock(Manager::class);
$this->throttler = $this->createMock(IThrottler::class);
$this->setupManager = $this->createMock(SetupManager::class);
$this->auth = new Auth(
$this->session,
$this->userSession,
$this->request,
$this->twoFactorManager,
$this->throttler,
$this->setupManager,
$this->throttler
);
}
@ -583,7 +579,7 @@ class AuthTest extends TestCase {
->method('getUID')
->willReturn('MyTestUser');
$this->userSession
->expects($this->exactly(4))
->expects($this->exactly(3))
->method('getUser')
->willReturn($user);
$response = $this->auth->check($server->httpRequest, $server->httpResponse);

@ -182,7 +182,8 @@ export default {
if (data.status === 'success') {
this.handleAvatarUpdate(false)
} 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
} else {
showError(data.data.message)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later

@ -21,9 +21,32 @@ Feature: avatar
And last avatar is a square of size 512
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
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 The following headers should be set
| Content-Type | image/png |
@ -41,7 +64,7 @@ Feature: avatar
Scenario: set square user avatar from internal path
Given user "user0" uploads file "data/green-square-256.png" to "/internal-green-square-256.png"
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 The following headers should be set
| Content-Type | image/png |
@ -55,21 +78,82 @@ Feature: avatar
And last avatar is a square of size 64
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"
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 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 "#00FF00" color
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
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 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
Then user "user0" gets avatar for user "user0"
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 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
When user "user0" gets avatar for guest "guest0"
Then The following headers should be set

@ -4,6 +4,7 @@
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
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
*/
public function loggedInUserPostsAvatarFromFile(string $source) {
public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
$file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
$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
*/
public function loggedInUserPostsAvatarFromInternalPath(string $path) {
public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
$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
*/

@ -3455,6 +3455,17 @@
<code><![CDATA[$this->providers]]></code>
</UndefinedInterfaceMethod>
</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">
<LessSpecificReturnStatement>
<code><![CDATA[array_merge(

@ -8,6 +8,7 @@
namespace OC\Core\Controller;
use OC\AppFramework\Utility\TimeFactory;
use OC\NotSquareException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
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\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
@ -22,6 +24,7 @@ use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N;
use OCP\Image;
use OCP\IRequest;
@ -38,6 +41,7 @@ class AvatarController extends Controller {
string $appName,
IRequest $request,
protected IAvatarManager $avatarManager,
protected ICache $cache,
protected IL10N $l10n,
protected IUserManager $userManager,
protected IRootFolder $rootFolder,
@ -198,7 +202,8 @@ class AvatarController extends Controller {
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]);
} else {
$phpFileUploadErrors = [
@ -245,6 +250,8 @@ class AvatarController extends Controller {
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (\Throwable $e) {
$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(
['data' => 'notsquare', 'image' => 'data:' . $mimeType . ';base64,' . base64_encode($image->data())],
['data' => 'notsquare'],
Http::STATUS_OK
);
} 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 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

@ -883,6 +883,23 @@ class OC {
$throttler = Server::get(IThrottler::class);
$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,
]);
}
});
}
}

@ -1160,6 +1160,8 @@ return array(
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.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\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',

@ -1201,6 +1201,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.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\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.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,13 +585,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IURLGenerator::class, URLGenerator::class);
$this->registerService(ICache::class, function ($c) {
/** @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->createDistributed();
return new Cache\File();
});
$this->registerService(Factory::class, function (Server $c) {

@ -29,6 +29,7 @@ use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IAvatar;
use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
@ -54,6 +55,8 @@ class AvatarControllerTest extends \Test\TestCase {
private $avatarFile;
/** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */
private $avatarManager;
/** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
private $cache;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
private $l;
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
@ -71,6 +74,8 @@ class AvatarControllerTest extends \Test\TestCase {
parent::setUp();
$this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock();
$this->cache = $this->getMockBuilder('OCP\ICache')
->disableOriginalConstructor()->getMock();
$this->l = $this->getMockBuilder(IL10N::class)->getMock();
$this->l->method('t')->willReturnArgument(0);
$this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
@ -93,6 +98,7 @@ class AvatarControllerTest extends \Test\TestCase {
'core',
$this->request,
$this->avatarManager,
$this->cache,
$this->l,
$this->userManager,
$this->rootFolder,
@ -292,6 +298,25 @@ class AvatarControllerTest extends \Test\TestCase {
$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.
*/
@ -310,6 +335,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName);
$this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
//Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]];
$this->request->method('getUploadedFile')->willReturn($reqRet);
@ -345,6 +373,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName);
$this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.gif'));
//Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]];
$this->request->method('getUploadedFile')->willReturn($reqRet);
@ -433,6 +464,93 @@ class AvatarControllerTest extends \Test\TestCase {
$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
*/

@ -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();
}
}
Loading…
Cancel
Save