diff options
Diffstat (limited to 'lib/private')
247 files changed, 7294 insertions, 3661 deletions
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 9865438161b..3e33e783635 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -37,9 +37,6 @@ namespace OC\Accounts; use Exception; use InvalidArgumentException; -use libphonenumber\NumberParseException; -use libphonenumber\PhoneNumberFormat; -use libphonenumber\PhoneNumberUtil; use OC\Profile\TProfileHelper; use OCP\Accounts\UserUpdatedEvent; use OCP\Cache\CappedMemoryCache; @@ -56,6 +53,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IPhoneNumberUtil; use OCP\IURLGenerator; use OCP\IUser; use OCP\L10N\IFactory; @@ -119,6 +117,7 @@ class AccountManager implements IAccountManager { private IFactory $l10nFactory, private IURLGenerator $urlGenerator, private ICrypto $crypto, + private IPhoneNumberUtil $phoneNumberUtil, ) { $this->internalCache = new CappedMemoryCache(); } @@ -139,13 +138,9 @@ class AccountManager implements IAccountManager { $defaultRegion = 'EN'; } - $phoneUtil = PhoneNumberUtil::getInstance(); - try { - $phoneNumber = $phoneUtil->parse($input, $defaultRegion); - if ($phoneUtil->isValidNumber($phoneNumber)) { - return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164); - } - } catch (NumberParseException $e) { + $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($input, $defaultRegion); + if ($phoneNumber !== null) { + return $phoneNumber; } throw new InvalidArgumentException(self::PROPERTY_PHONE); diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 88044fbf7b6..84bc297143a 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -38,6 +38,7 @@ */ namespace OC\App; +use InvalidArgumentException; use OC\AppConfig; use OC\AppFramework\Bootstrap\Coordinator; use OC\ServerNotAvailableException; @@ -822,16 +823,33 @@ class AppManager implements IAppManager { return $this->defaultEnabled; } - public function getDefaultAppForUser(?IUser $user = null): string { + public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string { // Set fallback to always-enabled files app - $appId = 'files'; - $defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files')); + $appId = $withFallbacks ? 'files' : ''; + $defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', '')); + $defaultApps = array_filter($defaultApps); $user ??= $this->userSession->getUser(); if ($user !== null) { $userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp')); $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps)); + if (empty($defaultApps) && $withFallbacks) { + /* Fallback on user defined apporder */ + $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR); + if (!empty($customOrders)) { + // filter only entries with app key (when added using closures or NavigationManager::add the app is not guranteed to be set) + $customOrders = array_filter($customOrders, fn ($entry) => isset($entry['app'])); + // sort apps by order + usort($customOrders, fn ($a, $b) => $a['order'] - $b['order']); + // set default apps to sorted apps + $defaultApps = array_map(fn ($entry) => $entry['app'], $customOrders); + } + } + } + + if (empty($defaultApps) && $withFallbacks) { + $defaultApps = ['dashboard','files']; } // Find the first app that is enabled for the current user @@ -845,4 +863,19 @@ class AppManager implements IAppManager { return $appId; } + + public function getDefaultApps(): array { + return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files')); + } + + public function setDefaultApps(array $defaultApps): void { + foreach ($defaultApps as $app) { + if (!$this->isInstalled($app)) { + $this->logger->debug('Can not set not installed app as default app', ['missing_app' => $app]); + throw new InvalidArgumentException('App is not installed'); + } + } + + $this->config->setSystemValue('defaultapp', join(',', $defaultApps)); + } } diff --git a/lib/private/App/AppStore/Version/VersionParser.php b/lib/private/App/AppStore/Version/VersionParser.php index 2b88399b9fd..eac9c935517 100644 --- a/lib/private/App/AppStore/Version/VersionParser.php +++ b/lib/private/App/AppStore/Version/VersionParser.php @@ -54,9 +54,9 @@ class VersionParser { // Count the amount of =, if it is one then it's either maximum or minimum // version. If it is two then it is maximum and minimum. $versionElements = explode(' ', $versionSpec); - $firstVersion = isset($versionElements[0]) ? $versionElements[0] : ''; + $firstVersion = $versionElements[0] ?? ''; $firstVersionNumber = substr($firstVersion, 2); - $secondVersion = isset($versionElements[1]) ? $versionElements[1] : ''; + $secondVersion = $versionElements[1] ?? ''; $secondVersionNumber = substr($secondVersion, 2); switch (count($versionElements)) { diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 84f0d5b9e5a..79c650705b2 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -373,7 +373,7 @@ class AppConfig implements IAppConfig { } else { $appIds = $this->getApps(); $values = array_map(function ($appId) use ($key) { - return isset($this->cache[$appId][$key]) ? $this->cache[$appId][$key] : null; + return $this->cache[$appId][$key] ?? null; }, $appIds); $result = array_combine($appIds, $values); diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 5aea2a7a744..5ff2dcd7969 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -55,6 +55,7 @@ use OCP\Http\WellKnown\IHandler; use OCP\Notification\INotifier; use OCP\Profile\ILinkAction; use OCP\Search\IProvider; +use OCP\SetupCheck\ISetupCheck; use OCP\Share\IPublicShareTemplateProvider; use OCP\Support\CrashReport\IReporter; use OCP\UserMigration\IMigrator as IUserMigrator; @@ -137,6 +138,9 @@ class RegistrationContext { /** @var ServiceRegistration<IReferenceProvider>[] */ private array $referenceProviders = []; + /** @var ServiceRegistration<\OCP\TextToImage\IProvider>[] */ + private $textToImageProviders = []; + @@ -146,11 +150,13 @@ class RegistrationContext { /** @var ServiceRegistration<IPublicShareTemplateProvider>[] */ private $publicShareTemplateProviders = []; - /** @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; + + /** @var ServiceRegistration<ISetupCheck>[] */ + private array $setupChecks = []; /** @var PreviewProviderRegistration[] */ - private $previewProviders = []; + private array $previewProviders = []; public function __construct(LoggerInterface $logger) { $this->logger = $logger; @@ -273,6 +279,13 @@ class RegistrationContext { ); } + public function registerTextToImageProvider(string $providerClass): void { + $this->context->registerTextToImageProvider( + $this->appId, + $providerClass + ); + } + public function registerTemplateProvider(string $providerClass): void { $this->context->registerTemplateProvider( $this->appId, @@ -372,6 +385,13 @@ class RegistrationContext { $class ); } + + public function registerSetupCheck(string $setupCheckClass): void { + $this->context->registerSetupCheck( + $this->appId, + $setupCheckClass + ); + } }; } @@ -383,14 +403,14 @@ class RegistrationContext { } /** - * @psalm-param class-string<IReporter> $capability + * @psalm-param class-string<IReporter> $reporterClass */ public function registerCrashReporter(string $appId, string $reporterClass): void { $this->crashReporters[] = new ServiceRegistration($appId, $reporterClass); } /** - * @psalm-param class-string<IWidget> $capability + * @psalm-param class-string<IWidget> $panelClass */ public function registerDashboardPanel(string $appId, string $panelClass): void { $this->dashboardPanels[] = new ServiceRegistration($appId, $panelClass); @@ -443,6 +463,10 @@ class RegistrationContext { $this->textProcessingProviders[] = new ServiceRegistration($appId, $class); } + public function registerTextToImageProvider(string $appId, string $class): void { + $this->textToImageProviders[] = new ServiceRegistration($appId, $class); + } + public function registerTemplateProvider(string $appId, string $class): void { $this->templateProviders[] = new ServiceRegistration($appId, $class); } @@ -524,6 +548,13 @@ class RegistrationContext { } /** + * @psalm-param class-string<ISetupCheck> $setupCheckClass + */ + public function registerSetupCheck(string $appId, string $setupCheckClass): void { + $this->setupChecks[] = new ServiceRegistration($appId, $setupCheckClass); + } + + /** * @param App[] $apps */ public function delegateCapabilityRegistrations(array $apps): void { @@ -565,9 +596,6 @@ class RegistrationContext { } } - /** - * @param App[] $apps - */ public function delegateDashboardPanelRegistrations(IManager $dashboardManager): void { while (($panel = array_shift($this->dashboardPanels)) !== null) { try { @@ -729,6 +757,13 @@ class RegistrationContext { } /** + * @return ServiceRegistration<\OCP\TextToImage\IProvider>[] + */ + public function getTextToImageProviders(): array { + return $this->textToImageProviders; + } + + /** * @return ServiceRegistration<ICustomTemplateProvider>[] */ public function getTemplateProviders(): array { @@ -828,4 +863,11 @@ class RegistrationContext { public function getPublicShareTemplateProviders(): array { return $this->publicShareTemplateProviders; } + + /** + * @return ServiceRegistration<ISetupCheck>[] + */ + public function getSetupChecks(): array { + return $this->setupChecks; + } } diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index a012d1e8ea6..c342ea236e2 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -404,33 +404,6 @@ class DIContainer extends SimpleContainer implements IAppContainer { } /** - * @deprecated use the ILogger instead - * @param string $message - * @param string $level - * @return mixed - */ - public function log($message, $level) { - switch ($level) { - case 'debug': - $level = ILogger::DEBUG; - break; - case 'info': - $level = ILogger::INFO; - break; - case 'warn': - $level = ILogger::WARN; - break; - case 'fatal': - $level = ILogger::FATAL; - break; - default: - $level = ILogger::ERROR; - break; - } - \OCP\Util::writeLog($this->getAppName(), $message, $level); - } - - /** * Register a capability * * @param string $serviceName e.g. 'OCA\Files\Capabilities' diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 408e88583a0..1186753ac73 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -193,9 +193,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ #[\ReturnTypeWillChange] public function offsetGet($offset) { - return isset($this->items['parameters'][$offset]) - ? $this->items['parameters'][$offset] - : null; + return $this->items['parameters'][$offset] ?? null; } /** @@ -255,9 +253,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'cookies': case 'urlParams': case 'method': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + return $this->items[$name] ?? null; case 'parameters': case 'params': if ($this->isPutStreamContent()) { @@ -597,9 +593,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { // only have one default, so we cannot ship an insecure product out of the box ]); - foreach ($forwardedForHeaders as $header) { + // Read the x-forwarded-for headers and values in reverse order as per + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address + foreach (array_reverse($forwardedForHeaders) as $header) { if (isset($this->server[$header])) { - foreach (explode(',', $this->server[$header]) as $IP) { + foreach (array_reverse(explode(',', $this->server[$header])) as $IP) { $IP = trim($IP); // remove brackets from IPv6 addresses @@ -607,6 +605,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { $IP = substr($IP, 1, -1); } + if ($this->isTrustedProxy($trustedProxies, $IP)) { + continue; + } + if (filter_var($IP, FILTER_VALIDATE_IP) !== false) { return $IP; } diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 8bdacf550b6..f0d6ece8a93 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -38,6 +38,7 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; use OCP\IRequest; +use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; use ReflectionMethod; @@ -91,6 +92,10 @@ class CORSMiddleware extends Middleware { if ($this->request->passesCSRFCheck()) { return; } + // Skip CORS check for requests with AppAPI auth. + if ($this->session->getSession() instanceof ISession && $this->session->getSession()->get('app_api') === true) { + return; + } $this->session->logout(); try { if ($user === null || $pass === null || !$this->session->logClientIn($user, $pass, $this->request, $this->throttler)) { diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php index 3232980b7e5..3b2296c145f 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 7aa5cb83926..ecd8485cd7b 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -105,6 +105,11 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { try { return $this->query($resolveName); } catch (QueryException $e2) { + // Pass null if typed and nullable + if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) { + return null; + } + // don't lose the error we got while trying to query by type throw new QueryException($e->getMessage(), (int) $e->getCode(), $e); } diff --git a/lib/private/AppFramework/Utility/TimeFactory.php b/lib/private/AppFramework/Utility/TimeFactory.php index 1e4655dd1cd..2763751132c 100644 --- a/lib/private/AppFramework/Utility/TimeFactory.php +++ b/lib/private/AppFramework/Utility/TimeFactory.php @@ -34,7 +34,7 @@ use OCP\AppFramework\Utility\ITimeFactory; * Use this to get a timestamp or DateTime object in code to remain testable * * @since 8.0.0 - * @since 26.0.0 Extends the \Psr\Clock\ClockInterface interface + * @since 27.0.0 Implements the \Psr\Clock\ClockInterface interface * @ref https://www.php-fig.org/psr/psr-20/#21-clockinterface */ class TimeFactory implements ITimeFactory { diff --git a/lib/private/Authentication/Exceptions/ExpiredTokenException.php b/lib/private/Authentication/Exceptions/ExpiredTokenException.php index 0dc92b45920..15069313712 100644 --- a/lib/private/Authentication/Exceptions/ExpiredTokenException.php +++ b/lib/private/Authentication/Exceptions/ExpiredTokenException.php @@ -27,17 +27,19 @@ namespace OC\Authentication\Exceptions; use OC\Authentication\Token\IToken; -class ExpiredTokenException extends InvalidTokenException { - /** @var IToken */ - private $token; - - public function __construct(IToken $token) { - parent::__construct(); - - $this->token = $token; +/** + * @deprecated 28.0.0 use {@see \OCP\Authentication\Exceptions\ExpiredTokenException} instead + */ +class ExpiredTokenException extends \OCP\Authentication\Exceptions\ExpiredTokenException { + public function __construct( + IToken $token, + ) { + parent::__construct($token); } public function getToken(): IToken { - return $this->token; + $token = parent::getToken(); + /** @var IToken $token We know that we passed OC interface from constructor */ + return $token; } } diff --git a/lib/private/Authentication/Exceptions/InvalidTokenException.php b/lib/private/Authentication/Exceptions/InvalidTokenException.php index acaabff6b88..7de6e1522fa 100644 --- a/lib/private/Authentication/Exceptions/InvalidTokenException.php +++ b/lib/private/Authentication/Exceptions/InvalidTokenException.php @@ -24,7 +24,8 @@ declare(strict_types=1); */ namespace OC\Authentication\Exceptions; -use Exception; - -class InvalidTokenException extends Exception { +/** + * @deprecated 28.0.0 use OCP version instead + */ +class InvalidTokenException extends \OCP\Authentication\Exceptions\InvalidTokenException { } diff --git a/lib/private/Authentication/Exceptions/WipeTokenException.php b/lib/private/Authentication/Exceptions/WipeTokenException.php index 1c60ab9da78..25b7cb74359 100644 --- a/lib/private/Authentication/Exceptions/WipeTokenException.php +++ b/lib/private/Authentication/Exceptions/WipeTokenException.php @@ -27,17 +27,19 @@ namespace OC\Authentication\Exceptions; use OC\Authentication\Token\IToken; -class WipeTokenException extends InvalidTokenException { - /** @var IToken */ - private $token; - - public function __construct(IToken $token) { - parent::__construct(); - - $this->token = $token; +/** + * @deprecated 28.0.0 use {@see \OCP\Authentication\Exceptions\WipeTokenException} instead + */ +class WipeTokenException extends \OCP\Authentication\Exceptions\WipeTokenException { + public function __construct( + IToken $token, + ) { + parent::__construct($token); } public function getToken(): IToken { - return $this->token; + $token = parent::getToken(); + /** @var IToken $token We know that we passed OC interface from constructor */ + return $token; } } diff --git a/lib/private/Authentication/Login/LoginResult.php b/lib/private/Authentication/Login/LoginResult.php index dec012c2fc9..18820d98a47 100644 --- a/lib/private/Authentication/Login/LoginResult.php +++ b/lib/private/Authentication/Login/LoginResult.php @@ -25,6 +25,8 @@ declare(strict_types=1); */ namespace OC\Authentication\Login; +use OC\Core\Controller\LoginController; + class LoginResult { /** @var bool */ private $success; @@ -59,6 +61,9 @@ class LoginResult { return $result; } + /** + * @param LoginController::LOGIN_MSG_*|null $msg + */ public static function failure(LoginData $data, string $msg = null): LoginResult { $result = new static(false, $data); if ($msg !== null) { diff --git a/lib/private/Authentication/Token/IToken.php b/lib/private/Authentication/Token/IToken.php index 5ca4eaea843..eb172f33396 100644 --- a/lib/private/Authentication/Token/IToken.php +++ b/lib/private/Authentication/Token/IToken.php @@ -26,109 +26,10 @@ declare(strict_types=1); */ namespace OC\Authentication\Token; -use JsonSerializable; +use OCP\Authentication\Token\IToken as OCPIToken; -interface IToken extends JsonSerializable { - public const TEMPORARY_TOKEN = 0; - public const PERMANENT_TOKEN = 1; - public const WIPE_TOKEN = 2; - public const DO_NOT_REMEMBER = 0; - public const REMEMBER = 1; - - /** - * Get the token ID - * - * @return int - */ - public function getId(): int; - - /** - * Get the user UID - * - * @return string - */ - public function getUID(): string; - - /** - * Get the login name used when generating the token - * - * @return string - */ - public function getLoginName(): string; - - /** - * Get the (encrypted) login password - * - * @return string|null - */ - public function getPassword(); - - /** - * Get the timestamp of the last password check - * - * @return int - */ - public function getLastCheck(): int; - - /** - * Set the timestamp of the last password check - * - * @param int $time - */ - public function setLastCheck(int $time); - - /** - * Get the authentication scope for this token - * - * @return string - */ - public function getScope(): string; - - /** - * Get the authentication scope for this token - * - * @return array - */ - public function getScopeAsArray(): array; - - /** - * Set the authentication scope for this token - * - * @param array $scope - */ - public function setScope($scope); - - /** - * Get the name of the token - * @return string - */ - public function getName(): string; - - /** - * Get the remember state of the token - * - * @return int - */ - public function getRemember(): int; - - /** - * Set the token - * - * @param string $token - */ - public function setToken(string $token); - - /** - * Set the password - * - * @param string $password - */ - public function setPassword(string $password); - - /** - * Set the expiration time of the token - * - * @param int|null $expires - */ - public function setExpires($expires); +/** + * @deprecated 28.0.0 use {@see \OCP\Authentication\Token\IToken} instead + */ +interface IToken extends OCPIToken { } diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php index 45335e17c31..b77a856589d 100644 --- a/lib/private/Authentication/Token/PublicKeyToken.php +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -137,10 +137,8 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { /** * Get the (encrypted) login password - * - * @return string|null */ - public function getPassword() { + public function getPassword(): ?string { return parent::getPassword(); } @@ -165,10 +163,8 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { /** * Get the timestamp of the last password check - * - * @param int $time */ - public function setLastCheck(int $time) { + public function setLastCheck(int $time): void { parent::setLastCheck($time); } @@ -191,7 +187,7 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { return $scope; } - public function setScope($scope) { + public function setScope(array|string|null $scope): void { if (is_array($scope)) { parent::setScope(json_encode($scope)); } else { @@ -211,15 +207,15 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { return parent::getRemember(); } - public function setToken(string $token) { + public function setToken(string $token): void { parent::setToken($token); } - public function setPassword(string $password = null) { + public function setPassword(string $password = null): void { parent::setPassword($password); } - public function setExpires($expires) { + public function setExpires($expires): void { parent::setExpires($expires); } diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php index 4817c6b8de0..97d6a02b4c4 100644 --- a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php +++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php @@ -25,8 +25,6 @@ declare(strict_types=1); */ namespace OC\Authentication\TwoFactorAuth\Db; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; -use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use function array_map; @@ -70,25 +68,24 @@ class ProviderUserAssignmentDao { * Persist a new/updated (provider_id, uid, enabled) tuple */ public function persist(string $providerId, string $uid, int $enabled): void { - $qb = $this->conn->getQueryBuilder(); - - try { - // Insert a new entry - $insertQuery = $qb->insert(self::TABLE_NAME)->values([ - 'provider_id' => $qb->createNamedParameter($providerId), - 'uid' => $qb->createNamedParameter($uid), - 'enabled' => $qb->createNamedParameter($enabled, IQueryBuilder::PARAM_INT), - ]); - - $insertQuery->execute(); - } catch (UniqueConstraintViolationException $ex) { - // There is already an entry -> update it - $updateQuery = $qb->update(self::TABLE_NAME) - ->set('enabled', $qb->createNamedParameter($enabled)) - ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter($providerId))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); - $updateQuery->execute(); + $conn = $this->conn; + + // Insert a new entry + if ($conn->insertIgnoreConflict(self::TABLE_NAME, [ + 'provider_id' => $providerId, + 'uid' => $uid, + 'enabled' => $enabled, + ])) { + return; } + + // There is already an entry -> update it + $qb = $conn->getQueryBuilder(); + $updateQuery = $qb->update(self::TABLE_NAME) + ->set('enabled', $qb->createNamedParameter($enabled)) + ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter($providerId))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); + $updateQuery->executeStatement(); } /** diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php index 4125c8eb0a8..1e137fa8715 100644 --- a/lib/private/Avatar/AvatarManager.php +++ b/lib/private/Avatar/AvatarManager.php @@ -55,59 +55,26 @@ use Psr\Log\LoggerInterface; * This class implements methods to access Avatar functionality */ class AvatarManager implements IAvatarManager { - /** @var IUserSession */ - private $userSession; - - /** @var Manager */ - private $userManager; - - /** @var IAppData */ - private $appData; - - /** @var IL10N */ - private $l; - - /** @var LoggerInterface */ - private $logger; - - /** @var IConfig */ - private $config; - - /** @var IAccountManager */ - private $accountManager; - - /** @var KnownUserService */ - private $knownUserService; - public function __construct( - IUserSession $userSession, - Manager $userManager, - IAppData $appData, - IL10N $l, - LoggerInterface $logger, - IConfig $config, - IAccountManager $accountManager, - KnownUserService $knownUserService + private IUserSession $userSession, + private Manager $userManager, + private IAppData $appData, + private IL10N $l, + private LoggerInterface $logger, + private IConfig $config, + private IAccountManager $accountManager, + private KnownUserService $knownUserService, ) { - $this->userSession = $userSession; - $this->userManager = $userManager; - $this->appData = $appData; - $this->l = $l; - $this->logger = $logger; - $this->config = $config; - $this->accountManager = $accountManager; - $this->knownUserService = $knownUserService; } /** * return a user specific instance of \OCP\IAvatar * @see \OCP\IAvatar * @param string $userId the ownCloud user id - * @return \OCP\IAvatar * @throws \Exception In case the username is potentially dangerous * @throws NotFoundException In case there is no user folder yet */ - public function getAvatar(string $userId) : IAvatar { + public function getAvatar(string $userId): IAvatar { $user = $this->userManager->get($userId); if ($user === null) { throw new \Exception('user does not exist'); @@ -116,10 +83,7 @@ class AvatarManager implements IAvatarManager { // sanitize userID - fixes casing issue (needed for the filesystem stuff that is done below) $userId = $user->getUID(); - $requestingUser = null; - if ($this->userSession !== null) { - $requestingUser = $this->userSession->getUser(); - } + $requestingUser = $this->userSession->getUser(); try { $folder = $this->appData->getFolder($userId); @@ -157,7 +121,7 @@ class AvatarManager implements IAvatarManager { /** * Clear generated avatars */ - public function clearCachedAvatars() { + public function clearCachedAvatars(): void { $users = $this->config->getUsersForUserValue('avatar', 'generated', 'true'); foreach ($users as $userId) { // This also bumps the avatar version leading to cache invalidation in browsers @@ -183,7 +147,6 @@ class AvatarManager implements IAvatarManager { * Returns a GuestAvatar. * * @param string $name The guest name, e.g. "Albert". - * @return IAvatar */ public function getGuestAvatar(string $name): IAvatar { return new GuestAvatar($name, $this->logger); diff --git a/lib/private/Avatar/GuestAvatar.php b/lib/private/Avatar/GuestAvatar.php index 083deb4108f..26614cf6cfa 100644 --- a/lib/private/Avatar/GuestAvatar.php +++ b/lib/private/Avatar/GuestAvatar.php @@ -35,18 +35,15 @@ use Psr\Log\LoggerInterface; */ class GuestAvatar extends Avatar { /** - * Holds the guest user display name. - */ - private string $userDisplayName; - - /** * GuestAvatar constructor. * * @param string $userDisplayName The guest user display name */ - public function __construct(string $userDisplayName, LoggerInterface $logger) { + public function __construct( + private string $userDisplayName, + LoggerInterface $logger, + ) { parent::__construct($logger); - $this->userDisplayName = $userDisplayName; } /** @@ -68,7 +65,6 @@ class GuestAvatar extends Avatar { * Setting avatars isn't implemented for guests. * * @param \OCP\IImage|resource|string $data - * @return void */ public function set($data): void { // unimplemented for guest user avatars diff --git a/lib/private/Avatar/PlaceholderAvatar.php b/lib/private/Avatar/PlaceholderAvatar.php index e7ca89f4d30..d420ebe574a 100644 --- a/lib/private/Avatar/PlaceholderAvatar.php +++ b/lib/private/Avatar/PlaceholderAvatar.php @@ -32,9 +32,7 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; -use OCP\IConfig; use OCP\IImage; -use OCP\IL10N; use Psr\Log\LoggerInterface; /** @@ -44,26 +42,12 @@ use Psr\Log\LoggerInterface; * for faster retrieval, unlike the GuestAvatar. */ class PlaceholderAvatar extends Avatar { - private ISimpleFolder $folder; - private User $user; - - /** - * UserAvatar constructor. - * - * @param IConfig $config The configuration - * @param ISimpleFolder $folder The avatar files folder - * @param IL10N $l The localization helper - * @param User $user The user this class manages the avatar for - * @param LoggerInterface $logger The logger - */ public function __construct( - ISimpleFolder $folder, - $user, - LoggerInterface $logger) { + private ISimpleFolder $folder, + private User $user, + LoggerInterface $logger, + ) { parent::__construct($logger); - - $this->folder = $folder; - $this->user = $user; } /** @@ -80,7 +64,6 @@ class PlaceholderAvatar extends Avatar { * @throws \Exception if the provided file is not a jpg or png image * @throws \Exception if the provided image is not valid * @throws NotSquareException if the image is not square - * @return void */ public function set($data): void { // unimplemented for placeholder avatars @@ -102,8 +85,6 @@ class PlaceholderAvatar extends Avatar { * * If there is no avatar file yet, one is generated. * - * @param int $size - * @return ISimpleFile * @throws NotFoundException * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException diff --git a/lib/private/Avatar/UserAvatar.php b/lib/private/Avatar/UserAvatar.php index 6d39d5f067d..f96259641f3 100644 --- a/lib/private/Avatar/UserAvatar.php +++ b/lib/private/Avatar/UserAvatar.php @@ -44,31 +44,14 @@ use Psr\Log\LoggerInterface; * This class represents a registered user's avatar. */ class UserAvatar extends Avatar { - private IConfig $config; - private ISimpleFolder $folder; - private IL10N $l; - private User $user; - - /** - * UserAvatar constructor. - * - * @param IConfig $config The configuration - * @param ISimpleFolder $folder The avatar files folder - * @param IL10N $l The localization helper - * @param User $user The user this class manages the avatar for - * @param LoggerInterface $logger The logger - */ public function __construct( - ISimpleFolder $folder, - IL10N $l, - User $user, + private ISimpleFolder $folder, + private IL10N $l, + private User $user, LoggerInterface $logger, - IConfig $config) { + private IConfig $config, + ) { parent::__construct($logger); - $this->folder = $folder; - $this->l = $l; - $this->user = $user; - $this->config = $config; } /** @@ -85,7 +68,6 @@ class UserAvatar extends Avatar { * @throws \Exception if the provided file is not a jpg or png image * @throws \Exception if the provided image is not valid * @throws NotSquareException if the image is not square - * @return void */ public function set($data): void { $img = $this->getAvatarImage($data); @@ -113,7 +95,6 @@ class UserAvatar extends Avatar { * Returns an image from several sources. * * @param IImage|resource|string|\GdImage $data An image object, imagedata or path to the avatar - * @return IImage */ private function getAvatarImage($data): IImage { if ($data instanceof IImage) { @@ -229,8 +210,6 @@ class UserAvatar extends Avatar { * * If there is no avatar file yet, one is generated. * - * @param int $size - * @return ISimpleFile * @throws NotFoundException * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php index 36cccbd4eab..ab7392522b2 100644 --- a/lib/private/BackgroundJob/JobList.php +++ b/lib/private/BackgroundJob/JobList.php @@ -41,6 +41,10 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IConfig; use OCP\IDBConnection; use Psr\Log\LoggerInterface; +use function get_class; +use function json_encode; +use function md5; +use function strlen; class JobList implements IJobList { protected IDBConnection $connection; @@ -55,11 +59,10 @@ class JobList implements IJobList { $this->logger = $logger; } - /** - * @param IJob|class-string<IJob> $job - * @param mixed $argument - */ - public function add($job, $argument = null): void { + public function add($job, $argument = null, int $firstCheck = null): void { + if ($firstCheck === null) { + $firstCheck = $this->timeFactory->getTime(); + } if ($job instanceof IJob) { $class = get_class($job); } else { @@ -79,18 +82,22 @@ class JobList implements IJobList { 'argument' => $query->createNamedParameter($argumentJson), 'argument_hash' => $query->createNamedParameter(md5($argumentJson)), 'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT), + 'last_checked' => $query->createNamedParameter($firstCheck, IQueryBuilder::PARAM_INT), ]); } else { $query->update('jobs') ->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT)) - ->set('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)) + ->set('last_checked', $query->createNamedParameter($firstCheck, IQueryBuilder::PARAM_INT)) ->where($query->expr()->eq('class', $query->createNamedParameter($class))) ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argumentJson)))); } $query->executeStatement(); } + public function scheduleAfter(string $job, int $runAfter, $argument = null): void { + $this->add($job, $argument, $runAfter); + } + /** * @param IJob|string $job * @param mixed $argument @@ -406,7 +413,7 @@ class JobList implements IJobList { $query = $this->connection->getQueryBuilder(); $query->select('*') ->from('jobs') - ->where($query->expr()->neq('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->where($query->expr()->gt('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 6 * 3600, IQueryBuilder::PARAM_INT))) ->setMaxResults(1); if ($className !== null) { diff --git a/lib/private/Collaboration/AutoComplete/Manager.php b/lib/private/Collaboration/AutoComplete/Manager.php index cab15baf535..7b40165d4d8 100644 --- a/lib/private/Collaboration/AutoComplete/Manager.php +++ b/lib/private/Collaboration/AutoComplete/Manager.php @@ -29,47 +29,46 @@ use OCP\IServerContainer; class Manager implements IManager { /** @var string[] */ - protected $sorters = []; + protected array $sorters = []; /** @var ISorter[] */ - protected $sorterInstances = []; - /** @var IServerContainer */ - private $c; + protected array $sorterInstances = []; - public function __construct(IServerContainer $container) { - $this->c = $container; + public function __construct( + private IServerContainer $container, + ) { } - public function runSorters(array $sorters, array &$sortArray, array $context) { + public function runSorters(array $sorters, array &$sortArray, array $context): void { $sorterInstances = $this->getSorters(); while ($sorter = array_shift($sorters)) { if (isset($sorterInstances[$sorter])) { $sorterInstances[$sorter]->sort($sortArray, $context); } else { - $this->c->getLogger()->warning('No sorter for ID "{id}", skipping', [ + $this->container->getLogger()->warning('No sorter for ID "{id}", skipping', [ 'app' => 'core', 'id' => $sorter ]); } } } - public function registerSorter($className) { + public function registerSorter($className): void { $this->sorters[] = $className; } - protected function getSorters() { + protected function getSorters(): array { if (count($this->sorterInstances) === 0) { foreach ($this->sorters as $sorter) { /** @var ISorter $instance */ - $instance = $this->c->resolve($sorter); + $instance = $this->container->resolve($sorter); if (!$instance instanceof ISorter) { - $this->c->getLogger()->notice('Skipping sorter which is not an instance of ISorter. Class name: {class}', + $this->container->getLogger()->notice('Skipping sorter which is not an instance of ISorter. Class name: {class}', ['app' => 'core', 'class' => $sorter]); continue; } $sorterId = trim($instance->getId()); if (trim($sorterId) === '') { - $this->c->getLogger()->notice('Skipping sorter with empty ID. Class name: {class}', + $this->container->getLogger()->notice('Skipping sorter with empty ID. Class name: {class}', ['app' => 'core', 'class' => $sorter]); continue; } diff --git a/lib/private/Collaboration/Collaborators/GroupPlugin.php b/lib/private/Collaboration/Collaborators/GroupPlugin.php index 75e52c19e0b..1c98b904e76 100644 --- a/lib/private/Collaboration/Collaborators/GroupPlugin.php +++ b/lib/private/Collaboration/Collaborators/GroupPlugin.php @@ -37,34 +37,26 @@ use OCP\IUserSession; use OCP\Share\IShare; class GroupPlugin implements ISearchPlugin { - /** @var bool */ - protected $shareeEnumeration; - /** @var bool */ - protected $shareWithGroupOnly; - /** @var bool */ - protected $shareeEnumerationInGroupOnly; - /** @var bool */ - protected $groupSharingDisabled; - - /** @var IGroupManager */ - private $groupManager; - /** @var IConfig */ - private $config; - /** @var IUserSession */ - private $userSession; - - public function __construct(IConfig $config, IGroupManager $groupManager, IUserSession $userSession) { - $this->groupManager = $groupManager; - $this->config = $config; - $this->userSession = $userSession; + protected bool $shareeEnumeration; + protected bool $shareWithGroupOnly; + + protected bool $shareeEnumerationInGroupOnly; + + protected bool $groupSharingDisabled; + + public function __construct( + private IConfig $config, + private IGroupManager $groupManager, + private IUserSession $userSession, + ) { $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $this->groupSharingDisabled = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'no'; } - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { if ($this->groupSharingDisabled) { return false; } diff --git a/lib/private/Collaboration/Collaborators/LookupPlugin.php b/lib/private/Collaboration/Collaborators/LookupPlugin.php index 86ac70ab970..5a03e4f8673 100644 --- a/lib/private/Collaboration/Collaborators/LookupPlugin.php +++ b/lib/private/Collaboration/Collaborators/LookupPlugin.php @@ -38,31 +38,21 @@ use OCP\Share\IShare; use Psr\Log\LoggerInterface; class LookupPlugin implements ISearchPlugin { - /** @var IConfig */ - private $config; - /** @var IClientService */ - private $clientService; /** @var string remote part of the current user's cloud id */ - private $currentUserRemote; - /** @var ICloudIdManager */ - private $cloudIdManager; - /** @var LoggerInterface */ - private $logger; + private string $currentUserRemote; - public function __construct(IConfig $config, - IClientService $clientService, - IUserSession $userSession, - ICloudIdManager $cloudIdManager, - LoggerInterface $logger) { - $this->config = $config; - $this->clientService = $clientService; - $this->cloudIdManager = $cloudIdManager; + public function __construct( + private IConfig $config, + private IClientService $clientService, + IUserSession $userSession, + private ICloudIdManager $cloudIdManager, + private LoggerInterface $logger, + ) { $currentUserCloudId = $userSession->getUser()->getCloudId(); $this->currentUserRemote = $cloudIdManager->resolveCloudId($currentUserCloudId)->getRemote(); - $this->logger = $logger; } - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { $isGlobalScaleEnabled = $this->config->getSystemValueBool('gs.enabled', false); $isLookupServerEnabled = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'yes') === 'yes'; $hasInternetConnection = $this->config->getSystemValueBool('has_internet_connection', true); @@ -103,7 +93,7 @@ class LookupPlugin implements ISearchPlugin { if ($this->currentUserRemote === $remote) { continue; } - $name = isset($lookup['name']['value']) ? $lookup['name']['value'] : ''; + $name = $lookup['name']['value'] ?? ''; $label = empty($name) ? $lookup['federationId'] : $name . ' (' . $lookup['federationId'] . ')'; $result[] = [ 'label' => $label, diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index aa317ec1720..cbdd84efbb3 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -41,50 +41,27 @@ use OCP\Share\IShare; use OCP\Mail\IMailer; class MailPlugin implements ISearchPlugin { - /* @var bool */ - protected $shareWithGroupOnly; - /* @var bool */ - protected $shareeEnumeration; - /* @var bool */ - protected $shareeEnumerationInGroupOnly; - /* @var bool */ - protected $shareeEnumerationPhone; - /* @var bool */ - protected $shareeEnumerationFullMatch; - /* @var bool */ - protected $shareeEnumerationFullMatchEmail; + protected bool $shareWithGroupOnly; - /** @var IManager */ - private $contactsManager; - /** @var ICloudIdManager */ - private $cloudIdManager; - /** @var IConfig */ - private $config; + protected bool $shareeEnumeration; - /** @var IGroupManager */ - private $groupManager; - /** @var KnownUserService */ - private $knownUserService; - /** @var IUserSession */ - private $userSession; - /** @var IMailer */ - private $mailer; + protected bool $shareeEnumerationInGroupOnly; - public function __construct(IManager $contactsManager, - ICloudIdManager $cloudIdManager, - IConfig $config, - IGroupManager $groupManager, - KnownUserService $knownUserService, - IUserSession $userSession, - IMailer $mailer) { - $this->contactsManager = $contactsManager; - $this->cloudIdManager = $cloudIdManager; - $this->config = $config; - $this->groupManager = $groupManager; - $this->knownUserService = $knownUserService; - $this->userSession = $userSession; - $this->mailer = $mailer; + protected bool $shareeEnumerationPhone; + protected bool $shareeEnumerationFullMatch; + + protected bool $shareeEnumerationFullMatchEmail; + + public function __construct( + private IManager $contactsManager, + private ICloudIdManager $cloudIdManager, + private IConfig $config, + private IGroupManager $groupManager, + private KnownUserService $knownUserService, + private IUserSession $userSession, + private IMailer $mailer, + ) { $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; @@ -96,7 +73,7 @@ class MailPlugin implements ISearchPlugin { /** * {@inheritdoc} */ - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { if ($this->shareeEnumerationFullMatch && !$this->shareeEnumerationFullMatchEmail) { return false; } @@ -120,8 +97,8 @@ class MailPlugin implements ISearchPlugin { [ 'limit' => $limit, 'offset' => $offset, - 'enumeration' => (bool) $this->shareeEnumeration, - 'fullmatch' => (bool) $this->shareeEnumerationFullMatch, + 'enumeration' => $this->shareeEnumeration, + 'fullmatch' => $this->shareeEnumerationFullMatch, ] ); $lowerSearch = strtolower($search); @@ -286,6 +263,6 @@ class MailPlugin implements ISearchPlugin { public function isCurrentUser(ICloudId $cloud): bool { $currentUser = $this->userSession->getUser(); - return $currentUser instanceof IUser ? $currentUser->getUID() === $cloud->getUser() : false; + return $currentUser instanceof IUser && $currentUser->getUID() === $cloud->getUser(); } } diff --git a/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php index 413799e52c6..01f25b3d43d 100644 --- a/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php +++ b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php @@ -33,14 +33,12 @@ use OCP\Share; use OCP\Share\IShare; class RemoteGroupPlugin implements ISearchPlugin { - protected $shareeEnumeration; + private bool $enabled = false; - /** @var ICloudIdManager */ - private $cloudIdManager; - /** @var bool */ - private $enabled = false; - - public function __construct(ICloudFederationProviderManager $cloudFederationProviderManager, ICloudIdManager $cloudIdManager) { + public function __construct( + ICloudFederationProviderManager $cloudFederationProviderManager, + private ICloudIdManager $cloudIdManager, + ) { try { $fileSharingProvider = $cloudFederationProviderManager->getCloudFederationProvider('file'); $supportedShareTypes = $fileSharingProvider->getSupportedShareTypes(); @@ -50,10 +48,9 @@ class RemoteGroupPlugin implements ISearchPlugin { } catch (\Exception $e) { // do nothing, just don't enable federated group shares } - $this->cloudIdManager = $cloudIdManager; } - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { $result = ['wide' => [], 'exact' => []]; $resultType = new SearchResultType('remote_groups'); @@ -83,7 +80,7 @@ class RemoteGroupPlugin implements ISearchPlugin { * @return array [user, remoteURL] * @throws \InvalidArgumentException */ - public function splitGroupRemote($address) { + public function splitGroupRemote($address): array { try { $cloudId = $this->cloudIdManager->resolveCloudId($address); return [$cloudId->getUser(), $cloudId->getRemote()]; diff --git a/lib/private/Collaboration/Collaborators/RemotePlugin.php b/lib/private/Collaboration/Collaborators/RemotePlugin.php index 7d7a013a38c..a0868796689 100644 --- a/lib/private/Collaboration/Collaborators/RemotePlugin.php +++ b/lib/private/Collaboration/Collaborators/RemotePlugin.php @@ -37,32 +37,22 @@ use OCP\IUserSession; use OCP\Share\IShare; class RemotePlugin implements ISearchPlugin { - protected $shareeEnumeration; + protected bool $shareeEnumeration; - /** @var IManager */ - private $contactsManager; - /** @var ICloudIdManager */ - private $cloudIdManager; - /** @var IConfig */ - private $config; - /** @var IUserManager */ - private $userManager; - /** @var string */ - private $userId = ''; + private string $userId; - public function __construct(IManager $contactsManager, ICloudIdManager $cloudIdManager, IConfig $config, IUserManager $userManager, IUserSession $userSession) { - $this->contactsManager = $contactsManager; - $this->cloudIdManager = $cloudIdManager; - $this->config = $config; - $this->userManager = $userManager; - $user = $userSession->getUser(); - if ($user !== null) { - $this->userId = $user->getUID(); - } + public function __construct( + private IManager $contactsManager, + private ICloudIdManager $cloudIdManager, + private IConfig $config, + private IUserManager $userManager, + IUserSession $userSession, + ) { + $this->userId = $userSession->getUser()?->getUID() ?? ''; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; } - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { $result = ['wide' => [], 'exact' => []]; $resultType = new SearchResultType('remotes'); @@ -185,7 +175,7 @@ class RemotePlugin implements ISearchPlugin { * @return array [user, remoteURL] * @throws \InvalidArgumentException */ - public function splitUserRemote($address) { + public function splitUserRemote(string $address): array { try { $cloudId = $this->cloudIdManager->resolveCloudId($address); return [$cloudId->getUser(), $cloudId->getRemote()]; diff --git a/lib/private/Collaboration/Collaborators/Search.php b/lib/private/Collaboration/Collaborators/Search.php index 8d99ed42fcd..6b05d7e674b 100644 --- a/lib/private/Collaboration/Collaborators/Search.php +++ b/lib/private/Collaboration/Collaborators/Search.php @@ -35,32 +35,28 @@ use OCP\IContainer; use OCP\Share; class Search implements ISearch { - /** @var IContainer */ - private $c; + protected array $pluginList = []; - protected $pluginList = []; - - public function __construct(IContainer $c) { - $this->c = $c; + public function __construct( + private IContainer $container, + ) { } /** * @param string $search - * @param array $shareTypes * @param bool $lookup * @param int|null $limit * @param int|null $offset - * @return array * @throws \OCP\AppFramework\QueryException */ - public function search($search, array $shareTypes, $lookup, $limit, $offset) { + public function search($search, array $shareTypes, $lookup, $limit, $offset): array { $hasMoreResults = false; // Trim leading and trailing whitespace characters, e.g. when query is copy-pasted $search = trim($search); /** @var ISearchResult $searchResult */ - $searchResult = $this->c->resolve(SearchResult::class); + $searchResult = $this->container->resolve(SearchResult::class); foreach ($shareTypes as $type) { if (!isset($this->pluginList[$type])) { @@ -68,14 +64,14 @@ class Search implements ISearch { } foreach ($this->pluginList[$type] as $plugin) { /** @var ISearchPlugin $searchPlugin */ - $searchPlugin = $this->c->resolve($plugin); + $searchPlugin = $this->container->resolve($plugin); $hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults; } } // Get from lookup server, not a separate share type if ($lookup) { - $searchPlugin = $this->c->resolve(LookupPlugin::class); + $searchPlugin = $this->container->resolve(LookupPlugin::class); $hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults; } @@ -105,7 +101,7 @@ class Search implements ISearch { return [$searchResult->asArray(), $hasMoreResults]; } - public function registerPlugin(array $pluginInfo) { + public function registerPlugin(array $pluginInfo): void { $shareType = constant(Share::class . '::' . $pluginInfo['shareType']); if ($shareType === null) { throw new \InvalidArgumentException('Provided ShareType is invalid'); diff --git a/lib/private/Collaboration/Collaborators/SearchResult.php b/lib/private/Collaboration/Collaborators/SearchResult.php index 76d78c9c231..524ffba4b9e 100644 --- a/lib/private/Collaboration/Collaborators/SearchResult.php +++ b/lib/private/Collaboration/Collaborators/SearchResult.php @@ -28,13 +28,13 @@ use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; class SearchResult implements ISearchResult { - protected $result = [ + protected array $result = [ 'exact' => [], ]; - protected $exactIdMatches = []; + protected array $exactIdMatches = []; - public function addResultSet(SearchResultType $type, array $matches, array $exactMatches = null) { + public function addResultSet(SearchResultType $type, array $matches, array $exactMatches = null): void { $type = $type->getLabel(); if (!isset($this->result[$type])) { $this->result[$type] = []; @@ -47,15 +47,15 @@ class SearchResult implements ISearchResult { } } - public function markExactIdMatch(SearchResultType $type) { + public function markExactIdMatch(SearchResultType $type): void { $this->exactIdMatches[$type->getLabel()] = 1; } - public function hasExactIdMatch(SearchResultType $type) { + public function hasExactIdMatch(SearchResultType $type): bool { return isset($this->exactIdMatches[$type->getLabel()]); } - public function hasResult(SearchResultType $type, $collaboratorId) { + public function hasResult(SearchResultType $type, $collaboratorId): bool { $type = $type->getLabel(); if (!isset($this->result[$type])) { return false; @@ -73,11 +73,11 @@ class SearchResult implements ISearchResult { return false; } - public function asArray() { + public function asArray(): array { return $this->result; } - public function unsetResult(SearchResultType $type) { + public function unsetResult(SearchResultType $type): void { $type = $type->getLabel(); $this->result[$type] = []; if (isset($this->result['exact'][$type])) { diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index 9beecdaa6cb..1bd6762d2e0 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -44,50 +44,30 @@ use OCP\Share\IShare; use OCP\UserStatus\IManager as IUserStatusManager; class UserPlugin implements ISearchPlugin { - /* @var bool */ - protected $shareWithGroupOnly; - /* @var bool */ - protected $shareeEnumeration; - /* @var bool */ - protected $shareeEnumerationInGroupOnly; - /* @var bool */ - protected $shareeEnumerationPhone; - /* @var bool */ - protected $shareeEnumerationFullMatch; - /* @var bool */ - protected $shareeEnumerationFullMatchUserId; - /* @var bool */ - protected $shareeEnumerationFullMatchEmail; - /* @var bool */ - protected $shareeEnumerationFullMatchIgnoreSecondDisplayName; + protected bool $shareWithGroupOnly; - /** @var IConfig */ - private $config; - /** @var IGroupManager */ - private $groupManager; - /** @var IUserSession */ - private $userSession; - /** @var IUserManager */ - private $userManager; - /** @var KnownUserService */ - private $knownUserService; - /** @var IUserStatusManager */ - private $userStatusManager; + protected bool $shareeEnumeration; - public function __construct(IConfig $config, - IUserManager $userManager, - IGroupManager $groupManager, - IUserSession $userSession, - KnownUserService $knownUserService, - IUserStatusManager $userStatusManager) { - $this->config = $config; + protected bool $shareeEnumerationInGroupOnly; - $this->groupManager = $groupManager; - $this->userSession = $userSession; - $this->userManager = $userManager; - $this->knownUserService = $knownUserService; - $this->userStatusManager = $userStatusManager; + protected bool $shareeEnumerationPhone; + protected bool $shareeEnumerationFullMatch; + + protected bool $shareeEnumerationFullMatchUserId; + + protected bool $shareeEnumerationFullMatchEmail; + + protected bool $shareeEnumerationFullMatchIgnoreSecondDisplayName; + + public function __construct( + private IConfig $config, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IUserSession $userSession, + private KnownUserService $knownUserService, + private IUserStatusManager $userStatusManager, + ) { $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; @@ -98,7 +78,7 @@ class UserPlugin implements ISearchPlugin { $this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; } - public function search($search, $limit, $offset, ISearchResult $searchResult) { + public function search($search, $limit, $offset, ISearchResult $searchResult): bool { $result = ['wide' => [], 'exact' => []]; $users = []; $hasMoreResults = false; @@ -282,8 +262,6 @@ class UserPlugin implements ISearchPlugin { } } - - $type = new SearchResultType('users'); $searchResult->addResultSet($type, $result['wide'], $result['exact']); if (count($result['exact'])) { @@ -293,7 +271,7 @@ class UserPlugin implements ISearchPlugin { return $hasMoreResults; } - public function takeOutCurrentUser(array &$users) { + public function takeOutCurrentUser(array &$users): void { $currentUser = $this->userSession->getUser(); if (!is_null($currentUser)) { if (isset($users[$currentUser->getUID()])) { diff --git a/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php index 1dbe8e3bc35..4277b3837d2 100644 --- a/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php +++ b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php @@ -36,10 +36,9 @@ use OCP\Share\Events\ShareDeletedEvent; /** @template-implements IEventListener<Event|NodeDeletedEvent|ShareDeletedEvent|ShareCreatedEvent> */ class FileReferenceEventListener implements IEventListener { - private IReferenceManager $manager; - - public function __construct(IReferenceManager $manager) { - $this->manager = $manager; + public function __construct( + private IReferenceManager $manager, + ) { } public static function register(IEventDispatcher $eventDispatcher): void { diff --git a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php index d423a830495..5f384213976 100644 --- a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php +++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php @@ -41,26 +41,18 @@ use OCP\IUserSession; use OCP\L10N\IFactory; class FileReferenceProvider extends ADiscoverableReferenceProvider { - private IURLGenerator $urlGenerator; - private IRootFolder $rootFolder; private ?string $userId; - private IPreview $previewManager; - private IMimeTypeDetector $mimeTypeDetector; private IL10N $l10n; public function __construct( - IURLGenerator $urlGenerator, - IRootFolder $rootFolder, + private IURLGenerator $urlGenerator, + private IRootFolder $rootFolder, IUserSession $userSession, - IMimeTypeDetector $mimeTypeDetector, - IPreview $previewManager, - IFactory $l10n + private IMimeTypeDetector $mimeTypeDetector, + private IPreview $previewManager, + IFactory $l10n, ) { - $this->urlGenerator = $urlGenerator; - $this->rootFolder = $rootFolder; - $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; - $this->previewManager = $previewManager; - $this->mimeTypeDetector = $mimeTypeDetector; + $this->userId = $userSession->getUser()?->getUID(); $this->l10n = $l10n->get('files'); } diff --git a/lib/private/Collaboration/Reference/LinkReferenceProvider.php b/lib/private/Collaboration/Reference/LinkReferenceProvider.php index dbdab75abcb..df6c6cc9da9 100644 --- a/lib/private/Collaboration/Reference/LinkReferenceProvider.php +++ b/lib/private/Collaboration/Reference/LinkReferenceProvider.php @@ -54,24 +54,16 @@ class LinkReferenceProvider implements IReferenceProvider { 'image/webp' ]; - private IClientService $clientService; - private LoggerInterface $logger; - private SystemConfig $systemConfig; - private IAppDataFactory $appDataFactory; - private IURLGenerator $urlGenerator; - private Limiter $limiter; - private IUserSession $userSession; - private IRequest $request; - - public function __construct(IClientService $clientService, LoggerInterface $logger, SystemConfig $systemConfig, IAppDataFactory $appDataFactory, IURLGenerator $urlGenerator, Limiter $limiter, IUserSession $userSession, IRequest $request) { - $this->clientService = $clientService; - $this->logger = $logger; - $this->systemConfig = $systemConfig; - $this->appDataFactory = $appDataFactory; - $this->urlGenerator = $urlGenerator; - $this->limiter = $limiter; - $this->userSession = $userSession; - $this->request = $request; + public function __construct( + private IClientService $clientService, + private LoggerInterface $logger, + private SystemConfig $systemConfig, + private IAppDataFactory $appDataFactory, + private IURLGenerator $urlGenerator, + private Limiter $limiter, + private IUserSession $userSession, + private IRequest $request, + ) { } public function matchReference(string $referenceText): bool { @@ -119,7 +111,7 @@ class LinkReferenceProvider implements IReferenceProvider { $linkContentType = $headResponse->getHeader('Content-Type'); $expectedContentType = 'text/html'; $suffixedExpectedContentType = $expectedContentType . ';'; - $startsWithSuffixed = substr($linkContentType, 0, strlen($suffixedExpectedContentType)) === $suffixedExpectedContentType; + $startsWithSuffixed = str_starts_with($linkContentType, $suffixedExpectedContentType); // check the header begins with the expected content type if ($linkContentType !== $expectedContentType && !$startsWithSuffixed) { $this->logger->debug('Skip resolving links pointing to content type that is not "text/html"'); diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php index 2897410f5d6..1db87a56494 100644 --- a/lib/private/Collaboration/Reference/ReferenceManager.php +++ b/lib/private/Collaboration/Reference/ReferenceManager.php @@ -46,33 +46,22 @@ class ReferenceManager implements IReferenceManager { /** @var IReferenceProvider[]|null */ private ?array $providers = null; private ICache $cache; - private Coordinator $coordinator; - private ContainerInterface $container; - private LinkReferenceProvider $linkReferenceProvider; - private LoggerInterface $logger; - private IConfig $config; - private IUserSession $userSession; - - public function __construct(LinkReferenceProvider $linkReferenceProvider, - ICacheFactory $cacheFactory, - Coordinator $coordinator, - ContainerInterface $container, - LoggerInterface $logger, - IConfig $config, - IUserSession $userSession) { - $this->linkReferenceProvider = $linkReferenceProvider; + + public function __construct( + private LinkReferenceProvider $linkReferenceProvider, + ICacheFactory $cacheFactory, + private Coordinator $coordinator, + private ContainerInterface $container, + private LoggerInterface $logger, + private IConfig $config, + private IUserSession $userSession, + ) { $this->cache = $cacheFactory->createDistributed('reference'); - $this->coordinator = $coordinator; - $this->container = $container; - $this->logger = $logger; - $this->config = $config; - $this->userSession = $userSession; } /** * Extract a list of URLs from a text * - * @param string $text * @return string[] */ public function extractReferences(string $text): array { @@ -85,9 +74,6 @@ class ReferenceManager implements IReferenceManager { /** * Try to get a cached reference object from a reference string - * - * @param string $referenceId - * @return IReference|null */ public function getReferenceFromCache(string $referenceId): ?IReference { $matchedProvider = $this->getMatchedProvider($referenceId); @@ -102,9 +88,6 @@ class ReferenceManager implements IReferenceManager { /** * Try to get a cached reference object from a full cache key - * - * @param string $cacheKey - * @return IReference|null */ public function getReferenceByCacheKey(string $cacheKey): ?IReference { $cached = $this->cache->get($cacheKey); @@ -118,9 +101,6 @@ class ReferenceManager implements IReferenceManager { /** * Get a reference object from a reference string with a matching provider * Use a cached reference if possible - * - * @param string $referenceId - * @return IReference|null */ public function resolveReference(string $referenceId): ?IReference { $matchedProvider = $this->getMatchedProvider($referenceId); @@ -148,7 +128,6 @@ class ReferenceManager implements IReferenceManager { * Try to match a reference string with all the registered providers * Fallback to the link reference provider (using OpenGraph) * - * @param string $referenceId * @return IReferenceProvider|null the first matching provider */ private function getMatchedProvider(string $referenceId): ?IReferenceProvider { @@ -169,10 +148,6 @@ class ReferenceManager implements IReferenceManager { /** * Get a hashed full cache key from a key and prefix given by a provider - * - * @param IReferenceProvider $provider - * @param string $referenceId - * @return string */ private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string { $cacheKey = $provider->getCacheKey($referenceId); @@ -183,10 +158,6 @@ class ReferenceManager implements IReferenceManager { /** * Remove a specific cache entry from its key+prefix - * - * @param string $cachePrefix - * @param string|null $cacheKey - * @return void */ public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void { if ($cacheKey === null) { diff --git a/lib/private/Collaboration/Reference/RenderReferenceEventListener.php b/lib/private/Collaboration/Reference/RenderReferenceEventListener.php index dc2c5612666..ab9b8fd1b63 100644 --- a/lib/private/Collaboration/Reference/RenderReferenceEventListener.php +++ b/lib/private/Collaboration/Reference/RenderReferenceEventListener.php @@ -34,12 +34,10 @@ use OCP\IInitialStateService; /** @template-implements IEventListener<Event|RenderReferenceEvent> */ class RenderReferenceEventListener implements IEventListener { - private IReferenceManager $manager; - private IInitialStateService $initialStateService; - - public function __construct(IReferenceManager $manager, IInitialStateService $initialStateService) { - $this->manager = $manager; - $this->initialStateService = $initialStateService; + public function __construct( + private IReferenceManager $manager, + private IInitialStateService $initialStateService, + ) { } public static function register(IEventDispatcher $eventDispatcher): void { diff --git a/lib/private/Collaboration/Resources/Collection.php b/lib/private/Collaboration/Resources/Collection.php index e34c38a80cd..3a09fe60051 100644 --- a/lib/private/Collaboration/Resources/Collection.php +++ b/lib/private/Collaboration/Resources/Collection.php @@ -37,46 +37,21 @@ use OCP\IDBConnection; use OCP\IUser; class Collection implements ICollection { - /** @var Manager */ - protected $manager; - - /** @var IDBConnection */ - protected $connection; - - /** @var int */ - protected $id; - - /** @var string */ - protected $name; - - /** @var IUser|null */ - protected $userForAccess; - - /** @var bool|null */ - protected $access; - /** @var IResource[] */ - protected $resources; + protected array $resources = []; public function __construct( - IManager $manager, - IDBConnection $connection, - int $id, - string $name, - ?IUser $userForAccess = null, - ?bool $access = null + /** @var Manager $manager */ + protected IManager $manager, + protected IDBConnection $connection, + protected int $id, + protected string $name, + protected ?IUser $userForAccess = null, + protected ?bool $access = null ) { - $this->manager = $manager; - $this->connection = $connection; - $this->id = $id; - $this->name = $name; - $this->userForAccess = $userForAccess; - $this->access = $access; - $this->resources = []; } /** - * @return int * @since 16.0.0 */ public function getId(): int { @@ -84,7 +59,6 @@ class Collection implements ICollection { } /** - * @return string * @since 16.0.0 */ public function getName(): string { @@ -92,7 +66,6 @@ class Collection implements ICollection { } /** - * @param string $name * @since 16.0.0 */ public function setName(string $name): void { @@ -120,7 +93,6 @@ class Collection implements ICollection { /** * Adds a resource to a collection * - * @param IResource $resource * @throws ResourceException when the resource is already part of the collection * @since 16.0.0 */ @@ -153,7 +125,6 @@ class Collection implements ICollection { /** * Removes a resource from a collection * - * @param IResource $resource * @since 16.0.0 */ public function removeResource(IResource $resource): void { @@ -178,8 +149,6 @@ class Collection implements ICollection { /** * Can a user/guest access the collection * - * @param IUser|null $user - * @return bool * @since 16.0.0 */ public function canAccess(?IUser $user): bool { diff --git a/lib/private/Collaboration/Resources/Manager.php b/lib/private/Collaboration/Resources/Manager.php index fc8804e69b4..0f4dbd7cbb7 100644 --- a/lib/private/Collaboration/Resources/Manager.php +++ b/lib/private/Collaboration/Resources/Manager.php @@ -45,26 +45,17 @@ class Manager implements IManager { public const TABLE_RESOURCES = 'collres_resources'; public const TABLE_ACCESS_CACHE = 'collres_accesscache'; - /** @var IDBConnection */ - protected $connection; - /** @var IProviderManager */ - protected $providerManager; - /** @var LoggerInterface */ - protected $logger; - /** @var string[] */ - protected $providers = []; - + protected array $providers = []; - public function __construct(IDBConnection $connection, IProviderManager $providerManager, LoggerInterface $logger) { - $this->connection = $connection; - $this->providerManager = $providerManager; - $this->logger = $logger; + public function __construct( + protected IDBConnection $connection, + protected IProviderManager $providerManager, + protected LoggerInterface $logger, + ) { } /** - * @param int $id - * @return ICollection * @throws CollectionException when the collection could not be found * @since 16.0.0 */ @@ -85,9 +76,6 @@ class Manager implements IManager { } /** - * @param int $id - * @param IUser|null $user - * @return ICollection * @throws CollectionException when the collection could not be found * @since 16.0.0 */ @@ -122,10 +110,6 @@ class Manager implements IManager { } /** - * @param IUser $user - * @param string $filter - * @param int $limit - * @param int $start * @return ICollection[] * @since 16.0.0 */ @@ -173,8 +157,6 @@ class Manager implements IManager { } /** - * @param string $name - * @return ICollection * @since 16.0.0 */ public function newCollection(string $name): ICollection { @@ -189,9 +171,6 @@ class Manager implements IManager { } /** - * @param string $type - * @param string $id - * @return IResource * @since 16.0.0 */ public function createResource(string $type, string $id): IResource { @@ -199,10 +178,6 @@ class Manager implements IManager { } /** - * @param string $type - * @param string $id - * @param IUser|null $user - * @return IResource * @throws ResourceException * @since 16.0.0 */ @@ -239,8 +214,6 @@ class Manager implements IManager { } /** - * @param ICollection $collection - * @param IUser|null $user * @return IResource[] * @since 16.0.0 */ @@ -274,8 +247,6 @@ class Manager implements IManager { /** * Get the rich object data of a resource * - * @param IResource $resource - * @return array * @since 16.0.0 */ public function getResourceRichObject(IResource $resource): array { @@ -294,9 +265,6 @@ class Manager implements IManager { /** * Can a user/guest access the collection * - * @param IResource $resource - * @param IUser|null $user - * @return bool * @since 16.0.0 */ public function canAccessResource(IResource $resource, ?IUser $user): bool { @@ -325,9 +293,6 @@ class Manager implements IManager { /** * Can a user/guest access the collection * - * @param ICollection $collection - * @param IUser|null $user - * @return bool * @since 16.0.0 */ public function canAccessCollection(ICollection $collection, ?IUser $user): bool { @@ -505,9 +470,6 @@ class Manager implements IManager { $query->execute(); } - /** - * @param string $provider - */ public function registerResourceProvider(string $provider): void { $this->logger->debug('\OC\Collaboration\Resources\Manager::registerResourceProvider is deprecated', ['provider' => $provider]); $this->providerManager->registerResourceProvider($provider); @@ -516,7 +478,6 @@ class Manager implements IManager { /** * Get the resource type of the provider * - * @return string * @since 16.0.0 */ public function getType(): string { diff --git a/lib/private/Collaboration/Resources/ProviderManager.php b/lib/private/Collaboration/Resources/ProviderManager.php index 4f5ed53b162..823d6764f58 100644 --- a/lib/private/Collaboration/Resources/ProviderManager.php +++ b/lib/private/Collaboration/Resources/ProviderManager.php @@ -34,20 +34,15 @@ use Psr\Log\LoggerInterface; class ProviderManager implements IProviderManager { /** @var string[] */ - protected $providers = []; + protected array $providers = []; /** @var IProvider[] */ - protected $providerInstances = []; + protected array $providerInstances = []; - /** @var IServerContainer */ - protected $serverContainer; - - /** @var LoggerInterface */ - protected $logger; - - public function __construct(IServerContainer $serverContainer, LoggerInterface $logger) { - $this->serverContainer = $serverContainer; - $this->logger = $logger; + public function __construct( + protected IServerContainer $serverContainer, + protected LoggerInterface $logger, + ) { } public function getResourceProviders(): array { diff --git a/lib/private/Collaboration/Resources/Resource.php b/lib/private/Collaboration/Resources/Resource.php index b5e0215cb39..059b1802128 100644 --- a/lib/private/Collaboration/Resources/Resource.php +++ b/lib/private/Collaboration/Resources/Resource.php @@ -33,45 +33,19 @@ use OCP\IDBConnection; use OCP\IUser; class Resource implements IResource { - /** @var IManager */ - protected $manager; - - /** @var IDBConnection */ - protected $connection; - - /** @var string */ - protected $type; - - /** @var string */ - protected $id; - - /** @var IUser|null */ - protected $userForAccess; - - /** @var bool|null */ - protected $access; - - /** @var array|null */ - protected $data; + protected ?array $data = null; public function __construct( - IManager $manager, - IDBConnection $connection, - string $type, - string $id, - ?IUser $userForAccess = null, - ?bool $access = null + protected IManager $manager, + protected IDBConnection $connection, + protected string $type, + protected string $id, + protected ?IUser $userForAccess = null, + protected ?bool $access = null ) { - $this->manager = $manager; - $this->connection = $connection; - $this->type = $type; - $this->id = $id; - $this->userForAccess = $userForAccess; - $this->access = $access; } /** - * @return string * @since 16.0.0 */ public function getType(): string { @@ -79,7 +53,6 @@ class Resource implements IResource { } /** - * @return string * @since 16.0.0 */ public function getId(): string { @@ -87,7 +60,6 @@ class Resource implements IResource { } /** - * @return array * @since 16.0.0 */ public function getRichObject(): array { @@ -101,8 +73,6 @@ class Resource implements IResource { /** * Can a user/guest access the resource * - * @param IUser|null $user - * @return bool * @since 16.0.0 */ public function canAccess(?IUser $user): bool { diff --git a/lib/private/Command/ClosureJob.php b/lib/private/Command/ClosureJob.php index 5639852e4db..7216bcc762a 100644 --- a/lib/private/Command/ClosureJob.php +++ b/lib/private/Command/ClosureJob.php @@ -24,11 +24,10 @@ namespace OC\Command; use OC\BackgroundJob\QueuedJob; use Laravel\SerializableClosure\SerializableClosure as LaravelClosure; -use Opis\Closure\SerializableClosure as OpisClosure; class ClosureJob extends QueuedJob { - protected function run($serializedCallable) { - $callable = unserialize($serializedCallable, [LaravelClosure::class, OpisClosure::class]); + protected function run($argument) { + $callable = unserialize($argument, [LaravelClosure::class]); $callable = $callable->getClosure(); if (is_callable($callable)) { $callable(); diff --git a/lib/private/Command/CommandJob.php b/lib/private/Command/CommandJob.php index 5b267162c81..e34ffe9440b 100644 --- a/lib/private/Command/CommandJob.php +++ b/lib/private/Command/CommandJob.php @@ -29,8 +29,8 @@ use OCP\Command\ICommand; * Wrap a command in the background job interface */ class CommandJob extends QueuedJob { - protected function run($serializedCommand) { - $command = unserialize($serializedCommand); + protected function run($argument) { + $command = unserialize($argument); if ($command instanceof ICommand) { $command->handle(); } else { diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index af4fda277d6..725febef85d 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -501,6 +501,22 @@ class Manager implements ICommentsManager { ) ); } + } elseif ($lastKnownCommentId > 0) { + // We didn't find the "$lastKnownComment" but we still use the ID as an offset. + // This is required as a fall-back for expired messages in talk and deleted comments in other apps. + if ($sortDirection === 'desc') { + if ($includeLastKnown) { + $query->andWhere($query->expr()->lte('id', $query->createNamedParameter($lastKnownCommentId))); + } else { + $query->andWhere($query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))); + } + } else { + if ($includeLastKnown) { + $query->andWhere($query->expr()->gte('id', $query->createNamedParameter($lastKnownCommentId))); + } else { + $query->andWhere($query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))); + } + } } $resultStatement = $query->execute(); diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php index 113f0507ef5..a0306c9798c 100644 --- a/lib/private/Console/Application.php +++ b/lib/private/Console/Application.php @@ -47,17 +47,12 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; class Application { - /** @var IConfig */ - private $config; + private IConfig $config; private SymfonyApplication $application; - /** @var IEventDispatcher */ - private $dispatcher; - /** @var IRequest */ - private $request; - /** @var LoggerInterface */ - private $logger; - /** @var MemoryInfo */ - private $memoryInfo; + private IEventDispatcher $dispatcher; + private IRequest $request; + private LoggerInterface $logger; + private MemoryInfo $memoryInfo; public function __construct(IConfig $config, IEventDispatcher $dispatcher, @@ -74,8 +69,6 @@ class Application { } /** - * @param InputInterface $input - * @param ConsoleOutputInterface $output * @throws \Exception */ public function loadCommands( diff --git a/lib/private/Console/TimestampFormatter.php b/lib/private/Console/TimestampFormatter.php index 8d74c28e94f..afb1f67c37f 100644 --- a/lib/private/Console/TimestampFormatter.php +++ b/lib/private/Console/TimestampFormatter.php @@ -27,17 +27,17 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyleInterface; class TimestampFormatter implements OutputFormatterInterface { - /** @var IConfig */ + /** @var ?IConfig */ protected $config; /** @var OutputFormatterInterface */ protected $formatter; /** - * @param IConfig $config + * @param ?IConfig $config * @param OutputFormatterInterface $formatter */ - public function __construct(IConfig $config, OutputFormatterInterface $formatter) { + public function __construct(?IConfig $config, OutputFormatterInterface $formatter) { $this->config = $config; $this->formatter = $formatter; } @@ -104,11 +104,16 @@ class TimestampFormatter implements OutputFormatterInterface { return $this->formatter->format($message); } - $timeZone = $this->config->getSystemValue('logtimezone', 'UTC'); - $timeZone = $timeZone !== null ? new \DateTimeZone($timeZone) : null; + if ($this->config instanceof IConfig) { + $timeZone = $this->config->getSystemValue('logtimezone', 'UTC'); + $timeZone = $timeZone !== null ? new \DateTimeZone($timeZone) : null; - $time = new \DateTime('now', $timeZone); - $timestampInfo = $time->format($this->config->getSystemValue('logdateformat', \DateTimeInterface::ATOM)); + $time = new \DateTime('now', $timeZone); + $timestampInfo = $time->format($this->config->getSystemValue('logdateformat', \DateTimeInterface::ATOM)); + } else { + $time = new \DateTime('now'); + $timestampInfo = $time->format(\DateTimeInterface::ATOM); + } return $timestampInfo . ' ' . $this->formatter->format($message); } diff --git a/lib/private/Contacts/ContactsMenu/ActionProviderStore.php b/lib/private/Contacts/ContactsMenu/ActionProviderStore.php index 7ba5db4bb33..67354a5fb2d 100644 --- a/lib/private/Contacts/ContactsMenu/ActionProviderStore.php +++ b/lib/private/Contacts/ContactsMenu/ActionProviderStore.php @@ -33,6 +33,7 @@ use OC\Contacts\ContactsMenu\Providers\EMailProvider; use OC\Contacts\ContactsMenu\Providers\LocalTimeProvider; use OC\Contacts\ContactsMenu\Providers\ProfileProvider; use OCP\AppFramework\QueryException; +use OCP\Contacts\ContactsMenu\IBulkProvider; use OCP\Contacts\ContactsMenu\IProvider; use OCP\IServerContainer; use OCP\IUser; @@ -47,18 +48,26 @@ class ActionProviderStore { } /** - * @return IProvider[] + * @return list<IProvider|IBulkProvider> * @throws Exception */ public function getProviders(IUser $user): array { $appClasses = $this->getAppProviderClasses($user); $providerClasses = $this->getServerProviderClasses(); $allClasses = array_merge($providerClasses, $appClasses); + /** @var list<IProvider|IBulkProvider> $providers */ $providers = []; foreach ($allClasses as $class) { try { - $providers[] = $this->serverContainer->get($class); + $provider = $this->serverContainer->get($class); + if ($provider instanceof IProvider || $provider instanceof IBulkProvider) { + $providers[] = $provider; + } else { + $this->logger->warning('Ignoring invalid contacts menu provider', [ + 'class' => $class, + ]); + } } catch (QueryException $ex) { $this->logger->error( 'Could not load contacts menu action provider ' . $class, diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index c692b486ae4..eeb6ae56bc1 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -33,6 +33,8 @@ namespace OC\Contacts\ContactsMenu; use OC\KnownUser\KnownUserService; use OC\Profile\ProfileManager; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; use OCP\Contacts\ContactsMenu\IContactsStore; use OCP\Contacts\ContactsMenu\IEntry; use OCP\Contacts\IManager; @@ -42,10 +44,17 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\L10N\IFactory as IL10NFactory; +use function array_column; +use function array_fill_keys; +use function array_filter; +use function array_key_exists; +use function array_merge; +use function count; class ContactsStore implements IContactsStore { public function __construct( private IManager $contactsManager, + private ?StatusService $userStatusService, private IConfig $config, private ProfileManager $profileManager, private IUserManager $userManager, @@ -70,15 +79,75 @@ class ContactsStore implements IContactsStore { if ($offset !== null) { $options['offset'] = $offset; } + // Status integration only works without pagination and filters + if ($offset === null && ($filter === null || $filter === '')) { + $recentStatuses = $this->userStatusService?->findAllRecentStatusChanges($limit, $offset) ?? []; + } else { + $recentStatuses = []; + } - $allContacts = $this->contactsManager->search( - $filter ?? '', - [ - 'FN', - 'EMAIL' - ], - $options - ); + // Search by status if there is no filter and statuses are available + if (!empty($recentStatuses)) { + $allContacts = array_filter(array_map(function (UserStatus $userStatus) use ($options) { + // UID is ambiguous with federation. We have to use the federated cloud ID to an exact match of + // A local user + $user = $this->userManager->get($userStatus->getUserId()); + if ($user === null) { + return null; + } + + $contact = $this->contactsManager->search( + $user->getCloudId(), + [ + 'CLOUD', + ], + array_merge( + $options, + [ + 'limit' => 1, + 'offset' => 0, + ], + ), + )[0] ?? null; + if ($contact !== null) { + $contact[Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP] = $userStatus->getStatusMessageTimestamp(); + } + return $contact; + }, $recentStatuses)); + if ($limit !== null && count($allContacts) < $limit) { + // More contacts were requested + $fromContacts = $this->contactsManager->search( + $filter ?? '', + [ + 'FN', + 'EMAIL' + ], + array_merge( + $options, + [ + 'limit' => $limit - count($allContacts), + ], + ), + ); + + // Create hash map of all status contacts + $existing = array_fill_keys(array_column($allContacts, 'URI'), null); + // Append the ones that are new + $allContacts = array_merge( + $allContacts, + array_filter($fromContacts, fn (array $contact): bool => !array_key_exists($contact['URI'], $existing)) + ); + } + } else { + $allContacts = $this->contactsManager->search( + $filter ?? '', + [ + 'FN', + 'EMAIL' + ], + $options + ); + } $userId = $user->getUID(); $contacts = array_filter($allContacts, function ($contact) use ($userId) { @@ -268,8 +337,10 @@ class ContactsStore implements IContactsStore { if (isset($contact['UID'])) { $uid = $contact['UID']; $entry->setId($uid); + $entry->setProperty('isUser', false); if (isset($contact['isLocalSystemBook'])) { $avatar = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $uid, 'size' => 64]); + $entry->setProperty('isUser', true); } elseif (isset($contact['FN'])) { $avatar = $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $contact['FN'], 'size' => 64]); } else { diff --git a/lib/private/Contacts/ContactsMenu/Entry.php b/lib/private/Contacts/ContactsMenu/Entry.php index f1cb4f9c52f..954f46e1296 100644 --- a/lib/private/Contacts/ContactsMenu/Entry.php +++ b/lib/private/Contacts/ContactsMenu/Entry.php @@ -29,8 +29,11 @@ namespace OC\Contacts\ContactsMenu; use OCP\Contacts\ContactsMenu\IAction; use OCP\Contacts\ContactsMenu\IEntry; +use function array_merge; class Entry implements IEntry { + public const PROPERTY_STATUS_MESSAGE_TIMESTAMP = 'statusMessageTimestamp'; + /** @var string|int|null */ private $id = null; @@ -50,6 +53,11 @@ class Entry implements IEntry { private array $properties = []; + private ?string $status = null; + private ?string $statusMessage = null; + private ?int $statusMessageTimestamp = null; + private ?string $statusIcon = null; + public function setId(string $id): void { $this->id = $id; } @@ -102,6 +110,16 @@ class Entry implements IEntry { $this->sortActions(); } + public function setStatus(string $status, + string $statusMessage = null, + int $statusMessageTimestamp = null, + string $icon = null): void { + $this->status = $status; + $this->statusMessage = $statusMessage; + $this->statusMessageTimestamp = $statusMessageTimestamp; + $this->statusIcon = $icon; + } + /** * @return IAction[] */ @@ -127,11 +145,15 @@ class Entry implements IEntry { }); } + public function setProperty(string $propertyName, mixed $value) { + $this->properties[$propertyName] = $value; + } + /** - * @param array $contact key-value array containing additional properties + * @param array $properties key-value array containing additional properties */ - public function setProperties(array $contact): void { - $this->properties = $contact; + public function setProperties(array $properties): void { + $this->properties = array_merge($this->properties, $properties); } public function getProperty(string $key): mixed { @@ -142,7 +164,7 @@ class Entry implements IEntry { } /** - * @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null} + * @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null, status: string|null, statusMessage: null|string, statusMessageTimestamp: null|int, statusIcon: null|string, isUser: bool, uid: mixed} */ public function jsonSerialize(): array { $topAction = !empty($this->actions) ? $this->actions[0]->jsonSerialize() : null; @@ -160,6 +182,20 @@ class Entry implements IEntry { 'emailAddresses' => $this->getEMailAddresses(), 'profileTitle' => $this->profileTitle, 'profileUrl' => $this->profileUrl, + 'status' => $this->status, + 'statusMessage' => $this->statusMessage, + 'statusMessageTimestamp' => $this->statusMessageTimestamp, + 'statusIcon' => $this->statusIcon, + 'isUser' => $this->getProperty('isUser') === true, + 'uid' => $this->getProperty('UID'), ]; } + + public function getStatusMessage(): ?string { + return $this->statusMessage; + } + + public function getStatusMessageTimestamp(): ?int { + return $this->statusMessageTimestamp; + } } diff --git a/lib/private/Contacts/ContactsMenu/Manager.php b/lib/private/Contacts/ContactsMenu/Manager.php index 490cf602283..5cf9a07c8e3 100644 --- a/lib/private/Contacts/ContactsMenu/Manager.php +++ b/lib/private/Contacts/ContactsMenu/Manager.php @@ -28,7 +28,9 @@ namespace OC\Contacts\ContactsMenu; use Exception; use OCP\App\IAppManager; use OCP\Constants; +use OCP\Contacts\ContactsMenu\IBulkProvider; use OCP\Contacts\ContactsMenu\IEntry; +use OCP\Contacts\ContactsMenu\IProvider; use OCP\IConfig; use OCP\IUser; @@ -80,8 +82,19 @@ class Manager { * @return IEntry[] */ private function sortEntries(array $entries): array { - usort($entries, function (IEntry $entryA, IEntry $entryB) { - return strcasecmp($entryA->getFullName(), $entryB->getFullName()); + usort($entries, function (Entry $entryA, Entry $entryB) { + $aStatusTimestamp = $entryA->getProperty(Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP); + $bStatusTimestamp = $entryB->getProperty(Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP); + if (!$aStatusTimestamp && !$bStatusTimestamp) { + return strcasecmp($entryA->getFullName(), $entryB->getFullName()); + } + if ($aStatusTimestamp === null) { + return 1; + } + if ($bStatusTimestamp === null) { + return -1; + } + return $bStatusTimestamp - $aStatusTimestamp; }); return $entries; } @@ -92,9 +105,14 @@ class Manager { */ private function processEntries(array $entries, IUser $user): void { $providers = $this->actionProviderStore->getProviders($user); - foreach ($entries as $entry) { - foreach ($providers as $provider) { - $provider->process($entry); + + foreach ($providers as $provider) { + if ($provider instanceof IBulkProvider && !($provider instanceof IProvider)) { + $provider->process($entries); + } elseif ($provider instanceof IProvider && !($provider instanceof IBulkProvider)) { + foreach ($entries as $entry) { + $provider->process($entry); + } } } } diff --git a/lib/private/ContactsManager.php b/lib/private/ContactsManager.php index c39f7c715cc..cfbd4305cd8 100644 --- a/lib/private/ContactsManager.php +++ b/lib/private/ContactsManager.php @@ -91,20 +91,20 @@ class ContactsManager implements IManager { * This function can be used to delete the contact identified by the given id * * @param int $id the unique identifier to a contact - * @param string $address_book_key identifier of the address book in which the contact shall be deleted + * @param string $addressBookKey identifier of the address book in which the contact shall be deleted * @return bool successful or not */ - public function delete($id, $address_book_key) { - $addressBook = $this->getAddressBook($address_book_key); + public function delete($id, $addressBookKey) { + $addressBook = $this->getAddressBook($addressBookKey); if (!$addressBook) { - return null; + return false; } if ($addressBook->getPermissions() & Constants::PERMISSION_DELETE) { return $addressBook->delete($id); } - return null; + return false; } /** @@ -112,11 +112,11 @@ class ContactsManager implements IManager { * Otherwise the contact will be updated by replacing the entire data set. * * @param array $properties this array if key-value-pairs defines a contact - * @param string $address_book_key identifier of the address book in which the contact shall be created or updated - * @return array representing the contact just created or updated + * @param string $addressBookKey identifier of the address book in which the contact shall be created or updated + * @return ?array representing the contact just created or updated */ - public function createOrUpdate($properties, $address_book_key) { - $addressBook = $this->getAddressBook($address_book_key); + public function createOrUpdate($properties, $addressBookKey) { + $addressBook = $this->getAddressBook($addressBookKey); if (!$addressBook) { return null; } @@ -133,7 +133,7 @@ class ContactsManager implements IManager { * * @return bool true if enabled, false if not */ - public function isEnabled() { + public function isEnabled(): bool { return !empty($this->addressBooks) || !empty($this->addressBookLoaders); } @@ -192,11 +192,8 @@ class ContactsManager implements IManager { /** * Get (and load when needed) the address book for $key - * - * @param string $addressBookKey - * @return IAddressBook */ - protected function getAddressBook($addressBookKey) { + protected function getAddressBook(string $addressBookKey): ?IAddressBook { $this->loadAddressBooks(); if (!array_key_exists($addressBookKey, $this->addressBooks)) { return null; diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php index acaa529c0e2..ad232aaabd1 100644 --- a/lib/private/DB/Adapter.php +++ b/lib/private/DB/Adapter.php @@ -30,6 +30,7 @@ namespace OC\DB; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OC\DB\Exceptions\DbalException; /** * This handles the way we use to write queries, into something that can be @@ -142,9 +143,12 @@ class Adapter { foreach ($values as $key => $value) { $builder->setValue($key, $builder->createNamedParameter($value)); } - return $builder->execute(); - } catch (UniqueConstraintViolationException $e) { - return 0; + return $builder->executeStatement(); + } catch (DbalException $e) { + if ($e->getReason() === \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + return 0; + } + throw $e; } } } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 85c6a72dfdb..df35e0b5e0d 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -42,7 +42,6 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Schema; @@ -220,7 +219,7 @@ class Connection extends \Doctrine\DBAL\Connection { * @return Statement The prepared statement. * @throws Exception */ - public function prepare($statement, $limit = null, $offset = null): Statement { + public function prepare($sql, $limit = null, $offset = null): Statement { if ($limit === -1 || $limit === null) { $limit = null; } else { @@ -231,9 +230,9 @@ class Connection extends \Doctrine\DBAL\Connection { } if (!is_null($limit)) { $platform = $this->getDatabasePlatform(); - $statement = $platform->modifyLimitQuery($statement, $limit, $offset); + $sql = $platform->modifyLimitQuery($sql, $limit, $offset); } - $statement = $this->replaceTablePrefix($statement); + $statement = $this->replaceTablePrefix($sql); $statement = $this->adapter->fixupStatement($statement); return parent::prepare($statement); @@ -321,14 +320,14 @@ class Connection extends \Doctrine\DBAL\Connection { * * @param string $seqName Name of the sequence object from which the ID should be returned. * - * @return string the last inserted ID. + * @return int the last inserted ID. * @throws Exception */ - public function lastInsertId($seqName = null) { - if ($seqName) { - $seqName = $this->replaceTablePrefix($seqName); + public function lastInsertId($name = null): int { + if ($name) { + $name = $this->replaceTablePrefix($name); } - return $this->adapter->lastInsertId($seqName); + return $this->adapter->lastInsertId($name); } /** @@ -600,10 +599,6 @@ class Connection extends \Doctrine\DBAL\Connection { return new SQLiteMigrator($this, $config, $dispatcher); } elseif ($platform instanceof OraclePlatform) { return new OracleMigrator($this, $config, $dispatcher); - } elseif ($platform instanceof MySQLPlatform) { - return new MySQLMigrator($this, $config, $dispatcher); - } elseif ($platform instanceof PostgreSQL94Platform) { - return new PostgreSqlMigrator($this, $config, $dispatcher); } else { return new Migrator($this, $config, $dispatcher); } diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php index a53c7ecd994..e27c98194fb 100644 --- a/lib/private/DB/ConnectionAdapter.php +++ b/lib/private/DB/ConnectionAdapter.php @@ -27,6 +27,10 @@ namespace OC\DB; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Schema\Schema; use OC\DB\Exceptions\DbalException; use OCP\DB\IPreparedStatement; @@ -87,7 +91,7 @@ class ConnectionAdapter implements IDBConnection { public function lastInsertId(string $table): int { try { - return (int)$this->inner->lastInsertId($table); + return $this->inner->lastInsertId($table); } catch (Exception $e) { throw DbalException::wrap($e); } @@ -242,4 +246,19 @@ class ConnectionAdapter implements IDBConnection { public function getInner(): Connection { return $this->inner; } + + public function getDatabaseProvider(): string { + $platform = $this->inner->getDatabasePlatform(); + if ($platform instanceof MySQLPlatform) { + return IDBConnection::PLATFORM_MYSQL; + } elseif ($platform instanceof OraclePlatform) { + return IDBConnection::PLATFORM_ORACLE; + } elseif ($platform instanceof PostgreSQLPlatform) { + return IDBConnection::PLATFORM_POSTGRES; + } elseif ($platform instanceof SqlitePlatform) { + return IDBConnection::PLATFORM_SQLITE; + } else { + throw new \Exception('Database ' . $platform::class . ' not supported'); + } + } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index 1b0ac436364..4b286ff5442 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -139,7 +139,7 @@ class ConnectionFactory { $additionalConnectionParams = array_merge($additionalConnectionParams, $additionalConnectionParams['driverOptions']); } $host = $additionalConnectionParams['host']; - $port = isset($additionalConnectionParams['port']) ? $additionalConnectionParams['port'] : null; + $port = $additionalConnectionParams['port'] ?? null; $dbName = $additionalConnectionParams['dbname']; // we set the connect string as dbname and unset the host to coerce doctrine into using it as connect string diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php index 71d7b51d149..60f9b65cd5f 100644 --- a/lib/private/DB/MigrationService.php +++ b/lib/private/DB/MigrationService.php @@ -58,7 +58,7 @@ class MigrationService { /** * @throws \Exception */ - public function __construct($appName, Connection $connection, ?IOutput $output = null, ?AppLocator $appLocator = null) { + public function __construct(string $appName, Connection $connection, ?IOutput $output = null, ?AppLocator $appLocator = null) { $this->appName = $appName; $this->connection = $connection; if ($output === null) { @@ -100,18 +100,15 @@ class MigrationService { /** * Returns the name of the app for which this migration is executed - * - * @return string */ - public function getApp() { + public function getApp(): string { return $this->appName; } /** - * @return bool * @codeCoverageIgnore - this will implicitly tested on installation */ - private function createMigrationTable() { + private function createMigrationTable(): bool { if ($this->migrationTableCreated) { return false; } @@ -188,7 +185,7 @@ class MigrationService { ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp()))) ->orderBy('version'); - $result = $qb->execute(); + $result = $qb->executeQuery(); $rows = $result->fetchAll(\PDO::FETCH_COLUMN); $result->closeCursor(); @@ -197,15 +194,17 @@ class MigrationService { /** * Returns all versions which are available in the migration folder - * - * @return array + * @return list<string> */ - public function getAvailableVersions() { + public function getAvailableVersions(): array { $this->ensureMigrationsAreLoaded(); return array_map('strval', array_keys($this->migrations)); } - protected function findMigrations() { + /** + * @return array<string, string> + */ + protected function findMigrations(): array { $directory = realpath($this->migrationsPath); if ($directory === false || !file_exists($directory) || !is_dir($directory)) { return []; @@ -322,10 +321,9 @@ class MigrationService { /** * Return the explicit version for the aliases; current, next, prev, latest * - * @param string $alias * @return mixed|null|string */ - public function getMigration($alias) { + public function getMigration(string $alias) { switch ($alias) { case 'current': return $this->getCurrentVersion(); @@ -342,29 +340,22 @@ class MigrationService { return '0'; } - /** - * @param string $version - * @param int $delta - * @return null|string - */ - private function getRelativeVersion($version, $delta) { + private function getRelativeVersion(string $version, int $delta): ?string { $this->ensureMigrationsAreLoaded(); $versions = $this->getAvailableVersions(); - array_unshift($versions, 0); + array_unshift($versions, '0'); + /** @var int $offset */ $offset = array_search($version, $versions, true); if ($offset === false || !isset($versions[$offset + $delta])) { // Unknown version or delta out of bounds. return null; } - return (string) $versions[$offset + $delta]; + return (string)$versions[$offset + $delta]; } - /** - * @return string - */ - private function getCurrentVersion() { + private function getCurrentVersion(): string { $m = $this->getMigratedVersions(); if (count($m) === 0) { return '0'; @@ -374,11 +365,9 @@ class MigrationService { } /** - * @param string $version - * @return string * @throws \InvalidArgumentException */ - private function getClass($version) { + private function getClass(string $version): string { $this->ensureMigrationsAreLoaded(); if (isset($this->migrations[$version])) { @@ -390,22 +379,18 @@ class MigrationService { /** * Allows to set an IOutput implementation which is used for logging progress and messages - * - * @param IOutput $output */ - public function setOutput(IOutput $output) { + public function setOutput(IOutput $output): void { $this->output = $output; } /** * Applies all not yet applied versions up to $to - * - * @param string $to - * @param bool $schemaOnly * @throws \InvalidArgumentException */ - public function migrate($to = 'latest', $schemaOnly = false) { + public function migrate(string $to = 'latest', bool $schemaOnly = false): void { if ($schemaOnly) { + $this->output->debug('Migrating schema only'); $this->migrateSchemaOnly($to); return; } @@ -425,11 +410,9 @@ class MigrationService { /** * Applies all not yet applied versions up to $to - * - * @param string $to * @throws \InvalidArgumentException */ - public function migrateSchemaOnly($to = 'latest') { + public function migrateSchemaOnly(string $to = 'latest'): void { // read known migrations $toBeExecuted = $this->getMigrationsToExecute($to); @@ -439,6 +422,7 @@ class MigrationService { $toSchema = null; foreach ($toBeExecuted as $version) { + $this->output->debug('- Reading ' . $version); $instance = $this->createInstance($version); $toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper { @@ -447,16 +431,20 @@ class MigrationService { } if ($toSchema instanceof SchemaWrapper) { + $this->output->debug('- Checking target database schema'); $targetSchema = $toSchema->getWrappedSchema(); $this->ensureUniqueNamesConstraints($targetSchema); if ($this->checkOracle) { $beforeSchema = $this->connection->createSchema(); $this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix())); } + + $this->output->debug('- Migrate database schema'); $this->connection->migrateToSchema($targetSchema); $toSchema->performDropTableCalls(); } + $this->output->debug('- Mark migrations as executed'); foreach ($toBeExecuted as $version) { $this->markAsExecuted($version); } diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php index 74e5a285351..1d960e72dc5 100644 --- a/lib/private/DB/Migrator.php +++ b/lib/private/DB/Migrator.php @@ -31,7 +31,6 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\AbstractAsset; -use Doctrine\DBAL\Schema\Comparator; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\SchemaDiff; use Doctrine\DBAL\Types\StringType; @@ -75,7 +74,7 @@ class Migrator { $schemaDiff = $this->getDiff($targetSchema, $this->connection); $script = ''; - $sqls = $schemaDiff->toSql($this->connection->getDatabasePlatform()); + $sqls = $this->connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff); foreach ($sqls as $sql) { $script .= $this->convertStatementToScript($sql); } @@ -95,18 +94,20 @@ class Migrator { } return preg_match($filterExpression, $asset) === 1; }); - return $this->connection->getSchemaManager()->createSchema(); + return $this->connection->createSchemaManager()->introspectSchema(); } /** * @return SchemaDiff */ protected function getDiff(Schema $targetSchema, Connection $connection) { - // adjust varchar columns with a length higher then getVarcharMaxLength to clob + // Adjust STRING columns with a length higher than 4000 to TEXT (clob) + // for consistency between the supported databases and + // old vs. new installations. foreach ($targetSchema->getTables() as $table) { foreach ($table->getColumns() as $column) { if ($column->getType() instanceof StringType) { - if ($column->getLength() > $connection->getDatabasePlatform()->getVarcharMaxLength()) { + if ($column->getLength() > 4000) { $column->setType(Type::getType('text')); $column->setLength(null); } @@ -122,7 +123,7 @@ class Migrator { } return preg_match($filterExpression, $asset) === 1; }); - $sourceSchema = $connection->getSchemaManager()->createSchema(); + $sourceSchema = $connection->createSchemaManager()->introspectSchema(); // remove tables we don't know about foreach ($sourceSchema->getTables() as $table) { @@ -137,9 +138,8 @@ class Migrator { } } - /** @psalm-suppress InternalMethod */ - $comparator = new Comparator(); - return $comparator->compare($sourceSchema, $targetSchema); + $comparator = $connection->createSchemaManager()->createComparator(); + return $comparator->compareSchemas($sourceSchema, $targetSchema); } /** @@ -155,11 +155,11 @@ class Migrator { if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) { $connection->beginTransaction(); } - $sqls = $schemaDiff->toSql($connection->getDatabasePlatform()); + $sqls = $connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff); $step = 0; foreach ($sqls as $sql) { $this->emit($sql, $step++, count($sqls)); - $connection->query($sql); + $connection->executeQuery($sql); } if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) { $connection->commit(); @@ -178,7 +178,7 @@ class Migrator { } protected function getFilterExpression() { - return '/^' . preg_quote($this->config->getSystemValueString('dbtableprefix', 'oc_')) . '/'; + return '/^' . preg_quote($this->config->getSystemValueString('dbtableprefix', 'oc_'), '/') . '/'; } protected function emit(string $sql, int $step, int $max): void { diff --git a/lib/private/DB/MySQLMigrator.php b/lib/private/DB/MySQLMigrator.php deleted file mode 100644 index 0f8cbb309f3..00000000000 --- a/lib/private/DB/MySQLMigrator.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\DB; - -use Doctrine\DBAL\Schema\Schema; - -class MySQLMigrator extends Migrator { - /** - * @param Schema $targetSchema - * @param \Doctrine\DBAL\Connection $connection - * @return \Doctrine\DBAL\Schema\SchemaDiff - */ - protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { - $platform = $connection->getDatabasePlatform(); - $platform->registerDoctrineTypeMapping('enum', 'string'); - $platform->registerDoctrineTypeMapping('bit', 'string'); - - $schemaDiff = parent::getDiff($targetSchema, $connection); - - // identifiers need to be quoted for mysql - foreach ($schemaDiff->changedTables as $tableDiff) { - $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); - foreach ($tableDiff->changedColumns as $column) { - $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); - } - } - - return $schemaDiff; - } -} diff --git a/lib/private/DB/OracleConnection.php b/lib/private/DB/OracleConnection.php index b7e040965ee..1112d5c450a 100644 --- a/lib/private/DB/OracleConnection.php +++ b/lib/private/DB/OracleConnection.php @@ -29,6 +29,8 @@ namespace OC\DB; class OracleConnection extends Connection { /** * Quote the keys of the array + * @param array<string, string> $data + * @return array<string, string> */ private function quoteKeys(array $data) { $return = []; diff --git a/lib/private/DB/OracleMigrator.php b/lib/private/DB/OracleMigrator.php index 18deb97ec26..5abab1a34e2 100644 --- a/lib/private/DB/OracleMigrator.php +++ b/lib/private/DB/OracleMigrator.php @@ -1,5 +1,8 @@ <?php + +declare(strict_types=1); /** + * @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Christoph Wurst <christoph@winzerhof-wurst.at> @@ -29,176 +32,114 @@ namespace OC\DB; use Doctrine\DBAL\Exception; -use Doctrine\DBAL\Schema\Column; -use Doctrine\DBAL\Schema\ColumnDiff; -use Doctrine\DBAL\Schema\ForeignKeyConstraint; -use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Schema; -use Doctrine\DBAL\Schema\Table; class OracleMigrator extends Migrator { /** - * Quote a column's name but changing the name requires recreating - * the column instance and copying over all properties. - * - * @param Column $column old column - * @return Column new column instance with new name - */ - protected function quoteColumn(Column $column) { - $newColumn = new Column( - $this->connection->quoteIdentifier($column->getName()), - $column->getType() - ); - $newColumn->setAutoincrement($column->getAutoincrement()); - $newColumn->setColumnDefinition($column->getColumnDefinition()); - $newColumn->setComment($column->getComment()); - $newColumn->setDefault($column->getDefault()); - $newColumn->setFixed($column->getFixed()); - $newColumn->setLength($column->getLength()); - $newColumn->setNotnull($column->getNotnull()); - $newColumn->setPrecision($column->getPrecision()); - $newColumn->setScale($column->getScale()); - $newColumn->setUnsigned($column->getUnsigned()); - $newColumn->setPlatformOptions($column->getPlatformOptions()); - $newColumn->setCustomSchemaOptions($column->getPlatformOptions()); - return $newColumn; - } - - /** - * Quote an index's name but changing the name requires recreating - * the index instance and copying over all properties. - * - * @param Index $index old index - * @return Index new index instance with new name - */ - protected function quoteIndex($index) { - return new Index( - //TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()), - $index->getName(), - array_map(function ($columnName) { - return $this->connection->quoteIdentifier($columnName); - }, $index->getColumns()), - $index->isUnique(), - $index->isPrimary(), - $index->getFlags(), - $index->getOptions() - ); - } - - /** - * Quote an ForeignKeyConstraint's name but changing the name requires recreating - * the ForeignKeyConstraint instance and copying over all properties. - * - * @param ForeignKeyConstraint $fkc old fkc - * @return ForeignKeyConstraint new fkc instance with new name - */ - protected function quoteForeignKeyConstraint($fkc) { - return new ForeignKeyConstraint( - array_map(function ($columnName) { - return $this->connection->quoteIdentifier($columnName); - }, $fkc->getLocalColumns()), - $this->connection->quoteIdentifier($fkc->getForeignTableName()), - array_map(function ($columnName) { - return $this->connection->quoteIdentifier($columnName); - }, $fkc->getForeignColumns()), - $fkc->getName(), - $fkc->getOptions() - ); - } - - /** * @param Schema $targetSchema * @param \Doctrine\DBAL\Connection $connection * @return \Doctrine\DBAL\Schema\SchemaDiff * @throws Exception */ - protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { - $schemaDiff = parent::getDiff($targetSchema, $connection); - + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection): \Doctrine\DBAL\Schema\SchemaDiff { // oracle forces us to quote the identifiers - $schemaDiff->newTables = array_map(function (Table $table) { - return new Table( + $quotedSchema = new Schema(); + foreach ($targetSchema->getTables() as $table) { + $quotedTable = $quotedSchema->createTable( $this->connection->quoteIdentifier($table->getName()), - array_map(function (Column $column) { - return $this->quoteColumn($column); - }, $table->getColumns()), - array_map(function (Index $index) { - return $this->quoteIndex($index); - }, $table->getIndexes()), - [], - array_map(function (ForeignKeyConstraint $fck) { - return $this->quoteForeignKeyConstraint($fck); - }, $table->getForeignKeys()), - $table->getOptions() ); - }, $schemaDiff->newTables); - $schemaDiff->removedTables = array_map(function (Table $table) { - return new Table( - $this->connection->quoteIdentifier($table->getName()), - $table->getColumns(), - $table->getIndexes(), - [], - $table->getForeignKeys(), - $table->getOptions() - ); - }, $schemaDiff->removedTables); - - foreach ($schemaDiff->changedTables as $tableDiff) { - $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); - - $tableDiff->addedColumns = array_map(function (Column $column) { - return $this->quoteColumn($column); - }, $tableDiff->addedColumns); - - foreach ($tableDiff->changedColumns as $column) { - $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); - // auto increment is not relevant for oracle and can anyhow not be applied on change - $column->changedProperties = array_diff($column->changedProperties, ['autoincrement', 'unsigned']); + foreach ($table->getColumns() as $column) { + $newColumn = $quotedTable->addColumn( + $this->connection->quoteIdentifier($column->getName()), + $column->getType()->getTypeRegistry()->lookupName($column->getType()), + ); + $newColumn->setAutoincrement($column->getAutoincrement()); + $newColumn->setColumnDefinition($column->getColumnDefinition()); + $newColumn->setComment($column->getComment()); + $newColumn->setDefault($column->getDefault()); + $newColumn->setFixed($column->getFixed()); + $newColumn->setLength($column->getLength()); + $newColumn->setNotnull($column->getNotnull()); + $newColumn->setPrecision($column->getPrecision()); + $newColumn->setScale($column->getScale()); + $newColumn->setUnsigned($column->getUnsigned()); + $newColumn->setPlatformOptions($column->getPlatformOptions()); } - // remove columns that no longer have changed (because autoincrement and unsigned are not supported) - $tableDiff->changedColumns = array_filter($tableDiff->changedColumns, function (ColumnDiff $column) { - return count($column->changedProperties) > 0; - }); - - $tableDiff->removedColumns = array_map(function (Column $column) { - return $this->quoteColumn($column); - }, $tableDiff->removedColumns); - - $tableDiff->renamedColumns = array_map(function (Column $column) { - return $this->quoteColumn($column); - }, $tableDiff->renamedColumns); - - $tableDiff->addedIndexes = array_map(function (Index $index) { - return $this->quoteIndex($index); - }, $tableDiff->addedIndexes); - $tableDiff->changedIndexes = array_map(function (Index $index) { - return $this->quoteIndex($index); - }, $tableDiff->changedIndexes); - - $tableDiff->removedIndexes = array_map(function (Index $index) { - return $this->quoteIndex($index); - }, $tableDiff->removedIndexes); + foreach ($table->getIndexes() as $index) { + if ($index->isPrimary()) { + $quotedTable->setPrimaryKey( + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $index->getColumns()), + //TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()), + $index->getName(), + ); + } elseif ($index->isUnique()) { + $quotedTable->addUniqueIndex( + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $index->getColumns()), + //TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()), + $index->getName(), + $index->getOptions(), + ); + } else { + $quotedTable->addIndex( + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $index->getColumns()), + //TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()), + $index->getName(), + $index->getFlags(), + $index->getOptions(), + ); + } + } - $tableDiff->renamedIndexes = array_map(function (Index $index) { - return $this->quoteIndex($index); - }, $tableDiff->renamedIndexes); + foreach ($table->getUniqueConstraints() as $constraint) { + $quotedTable->addUniqueConstraint( + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $constraint->getColumns()), + $this->connection->quoteIdentifier($constraint->getName()), + $constraint->getFlags(), + $constraint->getOptions(), + ); + } - $tableDiff->addedForeignKeys = array_map(function (ForeignKeyConstraint $fkc) { - return $this->quoteForeignKeyConstraint($fkc); - }, $tableDiff->addedForeignKeys); + foreach ($table->getForeignKeys() as $foreignKey) { + $quotedTable->addForeignKeyConstraint( + $this->connection->quoteIdentifier($foreignKey->getForeignTableName()), + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $foreignKey->getLocalColumns()), + array_map(function ($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $foreignKey->getForeignColumns()), + $foreignKey->getOptions(), + $this->connection->quoteIdentifier($foreignKey->getName()), + ); + } - $tableDiff->changedForeignKeys = array_map(function (ForeignKeyConstraint $fkc) { - return $this->quoteForeignKeyConstraint($fkc); - }, $tableDiff->changedForeignKeys); + foreach ($table->getOptions() as $option => $value) { + $quotedTable->addOption( + $option, + $value, + ); + } + } - $tableDiff->removedForeignKeys = array_map(function (ForeignKeyConstraint $fkc) { - return $this->quoteForeignKeyConstraint($fkc); - }, $tableDiff->removedForeignKeys); + foreach ($targetSchema->getSequences() as $sequence) { + $quotedSchema->createSequence( + $sequence->getName(), + $sequence->getAllocationSize(), + $sequence->getInitialValue(), + ); } - return $schemaDiff; + return parent::getDiff($quotedSchema, $connection); } /** @@ -206,7 +147,7 @@ class OracleMigrator extends Migrator { * @return string */ protected function convertStatementToScript($statement) { - if (substr($statement, -1) === ';') { + if (str_ends_with($statement, ';')) { return $statement . PHP_EOL . '/' . PHP_EOL; } $script = $statement . ';'; diff --git a/lib/private/DB/PostgreSqlMigrator.php b/lib/private/DB/PostgreSqlMigrator.php deleted file mode 100644 index 92a0842e1a7..00000000000 --- a/lib/private/DB/PostgreSqlMigrator.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\DB; - -use Doctrine\DBAL\Schema\Schema; - -class PostgreSqlMigrator extends Migrator { - /** - * @param Schema $targetSchema - * @param \Doctrine\DBAL\Connection $connection - * @return \Doctrine\DBAL\Schema\SchemaDiff - */ - protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { - $schemaDiff = parent::getDiff($targetSchema, $connection); - - foreach ($schemaDiff->changedTables as $tableDiff) { - // fix default value in brackets - pg 9.4 is returning a negative default value in () - // see https://github.com/doctrine/dbal/issues/2427 - foreach ($tableDiff->changedColumns as $column) { - $column->changedProperties = array_filter($column->changedProperties, function ($changedProperties) use ($column) { - if ($changedProperties !== 'default') { - return true; - } - $fromDefault = $column->fromColumn->getDefault(); - $toDefault = $column->column->getDefault(); - $fromDefault = trim((string) $fromDefault, '()'); - - // by intention usage of != - return $fromDefault != $toDefault; - }); - } - } - - return $schemaDiff; - } -} diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 2f97b4a146c..c2818911ccf 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -866,7 +866,7 @@ class QueryBuilder implements IQueryBuilder { public function where(...$predicates) { if ($this->getQueryPart('where') !== null && $this->systemConfig->getValue('debug', false)) { // Only logging a warning, not throwing for now. - $e = new QueryException('Using where() on non-empty WHERE part, please verify it is intentional to not call whereAnd() or whereOr() instead. Otherwise consider creating a new query builder object or call resetQueryPart(\'where\') first.'); + $e = new QueryException('Using where() on non-empty WHERE part, please verify it is intentional to not call andWhere() or orWhere() instead. Otherwise consider creating a new query builder object or call resetQueryPart(\'where\') first.'); $this->logger->warning($e->getMessage(), ['exception' => $e]); } @@ -1202,7 +1202,7 @@ class QueryBuilder implements IQueryBuilder { * @link http://www.zetacomponents.org * * @param mixed $value - * @param mixed $type + * @param IQueryBuilder::PARAM_* $type * @param string $placeHolder The name to bind with. The string must start with a colon ':'. * * @return IParameter the placeholder name used. @@ -1229,7 +1229,7 @@ class QueryBuilder implements IQueryBuilder { * </code> * * @param mixed $value - * @param integer $type + * @param IQueryBuilder::PARAM_* $type * * @return IParameter */ diff --git a/lib/private/DB/SQLiteMigrator.php b/lib/private/DB/SQLiteMigrator.php index cbb39070a48..e0102e105b2 100644 --- a/lib/private/DB/SQLiteMigrator.php +++ b/lib/private/DB/SQLiteMigrator.php @@ -24,8 +24,6 @@ namespace OC\DB; use Doctrine\DBAL\Schema\Schema; -use Doctrine\DBAL\Types\BigIntType; -use Doctrine\DBAL\Types\Type; class SQLiteMigrator extends Migrator { /** @@ -34,21 +32,12 @@ class SQLiteMigrator extends Migrator { * @return \Doctrine\DBAL\Schema\SchemaDiff */ protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { - $platform = $connection->getDatabasePlatform(); - $platform->registerDoctrineTypeMapping('tinyint unsigned', 'integer'); - $platform->registerDoctrineTypeMapping('smallint unsigned', 'integer'); - $platform->registerDoctrineTypeMapping('varchar ', 'string'); - foreach ($targetSchema->getTables() as $table) { foreach ($table->getColumns() as $column) { // column comments are not supported on SQLite if ($column->getComment() !== null) { $column->setComment(null); } - // with sqlite autoincrement columns is of type integer - if ($column->getType() instanceof BigIntType && $column->getAutoincrement()) { - $column->setType(Type::getType('integer')); - } } } diff --git a/lib/private/Dashboard/Manager.php b/lib/private/Dashboard/Manager.php index 18a66499167..afe28872e69 100644 --- a/lib/private/Dashboard/Manager.php +++ b/lib/private/Dashboard/Manager.php @@ -40,8 +40,8 @@ class Manager implements IManager { /** @var array */ private $lazyWidgets = []; - /** @var IWidget[] */ - private $widgets = []; + /** @var array<string, IWidget> */ + private array $widgets = []; private ContainerInterface $serverContainer; private ?IAppManager $appManager = null; @@ -134,6 +134,9 @@ class Manager implements IManager { $this->lazyWidgets = []; } + /** + * @return array<string, IWidget> + */ public function getWidgets(): array { $this->loadLazyPanels(); return $this->widgets; diff --git a/lib/private/DateTimeFormatter.php b/lib/private/DateTimeFormatter.php index 1c8b4f6d3ab..57c4833a4e3 100644 --- a/lib/private/DateTimeFormatter.php +++ b/lib/private/DateTimeFormatter.php @@ -125,7 +125,7 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { * @return string Formatted relative date string */ public function formatDateRelativeDay($timestamp, $format = 'long', \DateTimeZone $timeZone = null, \OCP\IL10N $l = null) { - if (substr($format, -1) !== '*' && substr($format, -1) !== '*') { + if (!str_ends_with($format, '^') && !str_ends_with($format, '*')) { $format .= '^'; } @@ -289,7 +289,7 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { * @return string Formatted relative date and time string */ public function formatDateTimeRelativeDay($timestamp, $formatDate = 'long', $formatTime = 'medium', \DateTimeZone $timeZone = null, \OCP\IL10N $l = null) { - if (substr($formatDate, -1) !== '^' && substr($formatDate, -1) !== '*') { + if (!str_ends_with($formatDate, '^') && !str_ends_with($formatDate, '*')) { $formatDate .= '^'; } diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index b11c4060ab4..ea2f0dd7575 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -1,9 +1,13 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org> * * @author Bjoern Schiessle <bjoern@schiessle.org> * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Maxence Lange <maxence@artificial-owl.com> * * @license GNU AGPL version 3 or any later version * @@ -32,6 +36,9 @@ use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudFederationShare; use OCP\Federation\ICloudIdManager; use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMDiscoveryService; use Psr\Log\LoggerInterface; /** @@ -43,40 +50,16 @@ use Psr\Log\LoggerInterface; */ class CloudFederationProviderManager implements ICloudFederationProviderManager { /** @var array list of available cloud federation providers */ - private $cloudFederationProvider; - - /** @var IAppManager */ - private $appManager; - - /** @var IClientService */ - private $httpClientService; - - /** @var ICloudIdManager */ - private $cloudIdManager; - - private LoggerInterface $logger; - - /** @var array cache OCM end-points */ - private $ocmEndPoints = []; - - private $supportedAPIVersion = '1.0-proposal1'; - - /** - * CloudFederationProviderManager constructor. - * - * @param IAppManager $appManager - * @param IClientService $httpClientService - * @param ICloudIdManager $cloudIdManager - */ - public function __construct(IAppManager $appManager, - IClientService $httpClientService, - ICloudIdManager $cloudIdManager, - LoggerInterface $logger) { - $this->cloudFederationProvider = []; - $this->appManager = $appManager; - $this->httpClientService = $httpClientService; - $this->cloudIdManager = $cloudIdManager; - $this->logger = $logger; + private array $cloudFederationProvider = []; + + public function __construct( + private IConfig $config, + private IAppManager $appManager, + private IClientService $httpClientService, + private ICloudIdManager $cloudIdManager, + private IOCMDiscoveryService $discoveryService, + private LoggerInterface $logger + ) { } @@ -130,16 +113,18 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager public function sendShare(ICloudFederationShare $share) { $cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith()); - $ocmEndPoint = $this->getOCMEndPoint($cloudID->getRemote()); - if (empty($ocmEndPoint)) { + try { + $ocmProvider = $this->discoveryService->discover($cloudID->getRemote()); + } catch (OCMProviderException $e) { return false; } $client = $this->httpClientService->newClient(); try { - $response = $client->post($ocmEndPoint . '/shares', [ + $response = $client->post($ocmProvider->getEndPoint() . '/shares', [ 'body' => json_encode($share->getShare()), 'headers' => ['content-type' => 'application/json'], + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), 'timeout' => 10, 'connect_timeout' => 10, ]); @@ -168,17 +153,18 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager * @return array|false */ public function sendNotification($url, ICloudFederationNotification $notification) { - $ocmEndPoint = $this->getOCMEndPoint($url); - - if (empty($ocmEndPoint)) { + try { + $ocmProvider = $this->discoveryService->discover($url); + } catch (OCMProviderException $e) { return false; } $client = $this->httpClientService->newClient(); try { - $response = $client->post($ocmEndPoint . '/notifications', [ + $response = $client->post($ocmProvider->getEndPoint() . '/notifications', [ 'body' => json_encode($notification->getMessage()), 'headers' => ['content-type' => 'application/json'], + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), 'timeout' => 10, 'connect_timeout' => 10, ]); @@ -202,36 +188,4 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager public function isReady() { return $this->appManager->isEnabledForUser('cloud_federation_api'); } - /** - * check if server supports the new OCM api and ask for the correct end-point - * - * @param string $url full base URL of the cloud server - * @return string - */ - protected function getOCMEndPoint($url) { - if (isset($this->ocmEndPoints[$url])) { - return $this->ocmEndPoints[$url]; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get($url . '/ocm-provider/', ['timeout' => 10, 'connect_timeout' => 10]); - } catch (\Exception $e) { - $this->ocmEndPoints[$url] = ''; - return ''; - } - - $result = $response->getBody(); - $result = json_decode($result, true); - - $supportedVersion = isset($result['apiVersion']) && $result['apiVersion'] === $this->supportedAPIVersion; - - if (isset($result['endPoint']) && $supportedVersion) { - $this->ocmEndPoints[$url] = $result['endPoint']; - return $result['endPoint']; - } - - $this->ocmEndPoints[$url] = ''; - return ''; - } } diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 67d01bb6999..27db4dfe809 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -59,6 +59,7 @@ use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchQuery; use OCP\Files\Storage\IStorage; +use OCP\FilesMetadata\IFilesMetadataManager; use OCP\IDBConnection; use OCP\Util; use Psr\Log\LoggerInterface; @@ -132,7 +133,8 @@ class Cache implements ICache { return new CacheQueryBuilder( $this->connection, \OC::$server->getSystemConfig(), - \OC::$server->get(LoggerInterface::class) + \OC::$server->get(LoggerInterface::class), + \OC::$server->get(IFilesMetadataManager::class), ); } @@ -154,6 +156,7 @@ class Cache implements ICache { public function get($file) { $query = $this->getQueryBuilder(); $query->selectFileCache(); + $metadataQuery = $query->selectMetadata(); if (is_string($file) || $file == '') { // normalize file @@ -175,6 +178,7 @@ class Cache implements ICache { } elseif (!$data) { return $data; } else { + $data['metadata'] = $metadataQuery?->extractMetadata($data)->asArray() ?? []; return self::cacheEntryFromData($data, $this->mimetypeLoader); } } @@ -239,11 +243,14 @@ class Cache implements ICache { ->whereParent($fileId) ->orderBy('name', 'ASC'); + $metadataQuery = $query->selectMetadata(); + $result = $query->execute(); $files = $result->fetchAll(); $result->closeCursor(); - return array_map(function (array $data) { + return array_map(function (array $data) use ($metadataQuery) { + $data['metadata'] = $metadataQuery?->extractMetadata($data)->asArray() ?? []; return self::cacheEntryFromData($data, $this->mimetypeLoader); }, $files); } diff --git a/lib/private/Files/Cache/CacheEntry.php b/lib/private/Files/Cache/CacheEntry.php index ce9df2823c8..d1a64552fd1 100644 --- a/lib/private/Files/Cache/CacheEntry.php +++ b/lib/private/Files/Cache/CacheEntry.php @@ -134,7 +134,7 @@ class CacheEntry implements ICacheEntry { } public function getUnencryptedSize(): int { - if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { + if ($this->data['encrypted'] && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { return $this->data['unencrypted_size']; } else { return $this->data['size'] ?? 0; diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php index 34d2177b84e..365d28fc8c5 100644 --- a/lib/private/Files/Cache/CacheQueryBuilder.php +++ b/lib/private/Files/Cache/CacheQueryBuilder.php @@ -5,6 +5,7 @@ declare(strict_types=1); /** * @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> * + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * * @license GNU AGPL version 3 or any later version @@ -28,6 +29,8 @@ namespace OC\Files\Cache; use OC\DB\QueryBuilder\QueryBuilder; use OC\SystemConfig; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\FilesMetadata\IMetadataQuery; use OCP\IDBConnection; use Psr\Log\LoggerInterface; @@ -35,9 +38,14 @@ use Psr\Log\LoggerInterface; * Query builder with commonly used helpers for filecache queries */ class CacheQueryBuilder extends QueryBuilder { - private $alias = null; - - public function __construct(IDBConnection $connection, SystemConfig $systemConfig, LoggerInterface $logger) { + private ?string $alias = null; + + public function __construct( + IDBConnection $connection, + SystemConfig $systemConfig, + LoggerInterface $logger, + private IFilesMetadataManager $filesMetadataManager, + ) { parent::__construct($connection, $systemConfig, $logger); } @@ -63,7 +71,7 @@ class CacheQueryBuilder extends QueryBuilder { public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) { $name = $alias ?: 'filecache'; $this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime', - 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'unencrypted_size') + 'storage_mtime', 'encrypted', 'etag', "$name.permissions", 'checksum', 'unencrypted_size') ->from('filecache', $name); if ($joinExtendedCache) { @@ -126,4 +134,15 @@ class CacheQueryBuilder extends QueryBuilder { return $this; } + + /** + * join metadata to current query builder and returns an helper + * + * @return IMetadataQuery|null NULL if no metadata have never been generated + */ + public function selectMetadata(): ?IMetadataQuery { + $metadataQuery = $this->filesMetadataManager->getMetadataQuery($this, $this->alias, 'fileid'); + $metadataQuery?->retrieveMetadata(); + return $metadataQuery; + } } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 15c089a0f11..d8c5e66e129 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tobias Kaminsky <tobias@kaminsky.me> @@ -37,52 +38,47 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchQuery; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\FilesMetadata\IMetadataQuery; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUser; use Psr\Log\LoggerInterface; class QuerySearchHelper { - /** @var IMimeTypeLoader */ - private $mimetypeLoader; - /** @var IDBConnection */ - private $connection; - /** @var SystemConfig */ - private $systemConfig; - private LoggerInterface $logger; - /** @var SearchBuilder */ - private $searchBuilder; - /** @var QueryOptimizer */ - private $queryOptimizer; - private IGroupManager $groupManager; - public function __construct( - IMimeTypeLoader $mimetypeLoader, - IDBConnection $connection, - SystemConfig $systemConfig, - LoggerInterface $logger, - SearchBuilder $searchBuilder, - QueryOptimizer $queryOptimizer, - IGroupManager $groupManager, + private IMimeTypeLoader $mimetypeLoader, + private IDBConnection $connection, + private SystemConfig $systemConfig, + private LoggerInterface $logger, + private SearchBuilder $searchBuilder, + private QueryOptimizer $queryOptimizer, + private IGroupManager $groupManager, + private IFilesMetadataManager $filesMetadataManager, ) { - $this->mimetypeLoader = $mimetypeLoader; - $this->connection = $connection; - $this->systemConfig = $systemConfig; - $this->logger = $logger; - $this->searchBuilder = $searchBuilder; - $this->queryOptimizer = $queryOptimizer; - $this->groupManager = $groupManager; } protected function getQueryBuilder() { return new CacheQueryBuilder( $this->connection, $this->systemConfig, - $this->logger + $this->logger, + $this->filesMetadataManager, ); } - protected function applySearchConstraints(CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches): void { + /** + * @param CacheQueryBuilder $query + * @param ISearchQuery $searchQuery + * @param array $caches + * @param IMetadataQuery|null $metadataQuery + */ + protected function applySearchConstraints( + CacheQueryBuilder $query, + ISearchQuery $searchQuery, + array $caches, + ?IMetadataQuery $metadataQuery = null + ): void { $storageFilters = array_values(array_map(function (ICache $cache) { return $cache->getQueryFilterForStorage(); }, $caches)); @@ -90,12 +86,12 @@ class QuerySearchHelper { $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]); $this->queryOptimizer->processOperator($filter); - $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter); + $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter, $metadataQuery); if ($searchExpr) { $query->andWhere($searchExpr); } - $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder()); + $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder(), $metadataQuery); if ($searchQuery->getLimit()) { $query->setMaxResults($searchQuery->getLimit()); @@ -144,6 +140,11 @@ class QuerySearchHelper { )); } + + protected function equipQueryForShares(CacheQueryBuilder $query): void { + $query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source')); + } + /** * Perform a file system search in multiple caches * @@ -175,19 +176,31 @@ class QuerySearchHelper { $query = $builder->selectFileCache('file', false); $requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation()); + if (in_array('systemtag', $requestedFields)) { $this->equipQueryForSystemTags($query, $this->requireUser($searchQuery)); } if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) { $this->equipQueryForDavTags($query, $this->requireUser($searchQuery)); } + if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) { + $this->equipQueryForShares($query); + } - $this->applySearchConstraints($query, $searchQuery, $caches); + $metadataQuery = $query->selectMetadata(); + + $this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery); $result = $query->execute(); $files = $result->fetchAll(); - $rawEntries = array_map(function (array $data) { + $rawEntries = array_map(function (array $data) use ($metadataQuery) { + // migrate to null safe ... + if ($metadataQuery === null) { + $data['metadata'] = []; + } else { + $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray(); + } return Cache::cacheEntryFromData($data, $this->mimetypeLoader); }, $files); diff --git a/lib/private/Files/Cache/Scanner.php b/lib/private/Files/Cache/Scanner.php index 52268032409..074e88e7639 100644 --- a/lib/private/Files/Cache/Scanner.php +++ b/lib/private/Files/Cache/Scanner.php @@ -385,10 +385,10 @@ class Scanner extends BasicEmitter implements IScanner { * @param int $reuse a combination of self::REUSE_* * @param int $folderId id for the folder to be scanned * @param bool $lock set to false to disable getting an additional read lock during scanning - * @param int $oldSize the size of the folder before (re)scanning the children + * @param int|float $oldSize the size of the folder before (re)scanning the children * @return int|float the size of the scanned folder or -1 if the size is unknown at this stage */ - protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int $oldSize) { + protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize) { if ($reuse === -1) { $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG; } @@ -418,7 +418,10 @@ class Scanner extends BasicEmitter implements IScanner { return $size; } - private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) { + /** + * @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive + */ + private function handleChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float &$size): array { // we put this in it's own function so it cleans up the memory before we start recursing $existingChildren = $this->getExistingChildren($folderId); $newChildren = iterator_to_array($this->storage->getDirectoryContent($path)); @@ -436,7 +439,7 @@ class Scanner extends BasicEmitter implements IScanner { $childQueue = []; $newChildNames = []; foreach ($newChildren as $fileMeta) { - $permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions']; + $permissions = $fileMeta['scan_permissions'] ?? $fileMeta['permissions']; if ($permissions === 0) { continue; } @@ -453,7 +456,7 @@ class Scanner extends BasicEmitter implements IScanner { $newChildNames[] = $file; $child = $path ? $path . '/' . $file : $file; try { - $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false; + $existingData = $existingChildren[$file] ?? false; $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta); if ($data) { if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) { diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index b9a70bbd39b..c3699cca63d 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tobias Kaminsky <tobias@kaminsky.me> @@ -32,6 +33,7 @@ use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchOrder; +use OCP\FilesMetadata\IMetadataQuery; /** * Tools for transforming search queries into database queries @@ -76,7 +78,7 @@ class SearchBuilder { return array_reduce($operator->getArguments(), function (array $fields, ISearchOperator $operator) { return array_unique(array_merge($fields, $this->extractRequestedFields($operator))); }, []); - } elseif ($operator instanceof ISearchComparison) { + } elseif ($operator instanceof ISearchComparison && !$operator->getExtra()) { return [$operator->getField()]; } return []; @@ -86,13 +88,21 @@ class SearchBuilder { * @param IQueryBuilder $builder * @param ISearchOperator[] $operators */ - public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) { - return array_filter(array_map(function ($operator) use ($builder) { - return $this->searchOperatorToDBExpr($builder, $operator); + public function searchOperatorArrayToDBExprArray( + IQueryBuilder $builder, + array $operators, + ?IMetadataQuery $metadataQuery = null + ) { + return array_filter(array_map(function ($operator) use ($builder, $metadataQuery) { + return $this->searchOperatorToDBExpr($builder, $operator, $metadataQuery); }, $operators)); } - public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) { + public function searchOperatorToDBExpr( + IQueryBuilder $builder, + ISearchOperator $operator, + ?IMetadataQuery $metadataQuery = null + ) { $expr = $builder->expr(); if ($operator instanceof ISearchBinaryOperator) { @@ -104,29 +114,37 @@ class SearchBuilder { case ISearchBinaryOperator::OPERATOR_NOT: $negativeOperator = $operator->getArguments()[0]; if ($negativeOperator instanceof ISearchComparison) { - return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap); + return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap, $metadataQuery); } else { throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); } // no break case ISearchBinaryOperator::OPERATOR_AND: - return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); + return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments(), $metadataQuery)); case ISearchBinaryOperator::OPERATOR_OR: - return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); + return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments(), $metadataQuery)); default: throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); } } elseif ($operator instanceof ISearchComparison) { - return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap); + return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap, $metadataQuery); } else { throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator)); } } - private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { - $this->validateComparison($comparison); + private function searchComparisonToDBExpr( + IQueryBuilder $builder, + ISearchComparison $comparison, + array $operatorMap, + ?IMetadataQuery $metadataQuery = null + ) { + if ($comparison->getExtra()) { + [$field, $value, $type] = $this->getExtraOperatorField($comparison, $metadataQuery); + } else { + [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison); + } - [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison); if (isset($operatorMap[$type])) { $queryOperator = $operatorMap[$type]; return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); @@ -136,9 +154,12 @@ class SearchBuilder { } private function getOperatorFieldAndValue(ISearchComparison $operator) { + $this->validateComparison($operator); + $field = $operator->getField(); $value = $operator->getValue(); $type = $operator->getType(); + if ($field === 'mimetype') { $value = (string)$value; if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { @@ -171,6 +192,8 @@ class SearchBuilder { } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) { $field = 'path_hash'; $value = md5((string)$value); + } elseif ($field === 'owner') { + $field = 'uid_owner'; } return [$field, $value, $type]; } @@ -187,6 +210,9 @@ class SearchBuilder { 'favorite' => 'boolean', 'fileid' => 'integer', 'storage' => 'integer', + 'share_with' => 'string', + 'share_type' => 'integer', + 'owner' => 'string', ]; $comparisons = [ 'mimetype' => ['eq', 'like'], @@ -199,6 +225,9 @@ class SearchBuilder { 'favorite' => ['eq'], 'fileid' => ['eq'], 'storage' => ['eq'], + 'share_with' => ['eq'], + 'share_type' => ['eq'], + 'owner' => ['eq'], ]; if (!isset($types[$operator->getField()])) { @@ -213,6 +242,24 @@ class SearchBuilder { } } + + private function getExtraOperatorField(ISearchComparison $operator, IMetadataQuery $metadataQuery): array { + $field = $operator->getField(); + $value = $operator->getValue(); + $type = $operator->getType(); + + switch($operator->getExtra()) { + case IMetadataQuery::EXTRA: + $metadataQuery->joinIndex($field); // join index table if not joined yet + $field = $metadataQuery->getMetadataValueField($field); + break; + default: + throw new \InvalidArgumentException('Invalid extra type: ' . $operator->getExtra()); + } + + return [$field, $value, $type]; + } + private function getParameterForValue(IQueryBuilder $builder, $value) { if ($value instanceof \DateTime) { $value = $value->getTimestamp(); @@ -228,24 +275,32 @@ class SearchBuilder { /** * @param IQueryBuilder $query * @param ISearchOrder[] $orders + * @param IMetadataQuery|null $metadataQuery */ - public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) { + public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders, ?IMetadataQuery $metadataQuery = null): void { foreach ($orders as $order) { $field = $order->getField(); - if ($field === 'fileid') { - $field = 'file.fileid'; - } + switch ($order->getExtra()) { + case IMetadataQuery::EXTRA: + $metadataQuery->joinIndex($field); // join index table if not joined yet + $field = $metadataQuery->getMetadataValueField($order->getField()); + break; - // Mysql really likes to pick an index for sorting if it can't fully satisfy the where - // filter with an index, since search queries pretty much never are fully filtered by index - // mysql often picks an index for sorting instead of the much more useful index for filtering. - // - // By changing the order by to an expression, mysql isn't smart enough to see that it could still - // use the index, so it instead picks an index for the filtering - if ($field === 'mtime') { - $field = $query->func()->add($field, $query->createNamedParameter(0)); - } + default: + if ($field === 'fileid') { + $field = 'file.fileid'; + } + // Mysql really likes to pick an index for sorting if it can't fully satisfy the where + // filter with an index, since search queries pretty much never are fully filtered by index + // mysql often picks an index for sorting instead of the much more useful index for filtering. + // + // By changing the order by to an expression, mysql isn't smart enough to see that it could still + // use the index, so it instead picks an index for the filtering + if ($field === 'mtime') { + $field = $query->func()->add($field, $query->createNamedParameter(0)); + } + } $query->addOrderBy($field, $order->getDirection()); } } diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index d8cf3eb61d7..73c9a017019 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -52,8 +52,6 @@ class CacheJail extends CacheWrapper { public function __construct($cache, $root) { parent::__construct($cache); $this->root = $root; - $this->connection = \OC::$server->getDatabaseConnection(); - $this->mimetypeLoader = \OC::$server->getMimeTypeLoader(); if ($cache instanceof CacheJail) { $this->unjailedRoot = $cache->getSourcePath($root); diff --git a/lib/private/Files/Config/CachedMountInfo.php b/lib/private/Files/Config/CachedMountInfo.php index 43c9fae63ec..7c97135a565 100644 --- a/lib/private/Files/Config/CachedMountInfo.php +++ b/lib/private/Files/Config/CachedMountInfo.php @@ -35,6 +35,7 @@ class CachedMountInfo implements ICachedMountInfo { protected ?int $mountId; protected string $rootInternalPath; protected string $mountProvider; + protected string $key; /** * CachedMountInfo constructor. @@ -65,6 +66,7 @@ class CachedMountInfo implements ICachedMountInfo { throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters"); } $this->mountProvider = $mountProvider; + $this->key = $rootId . '::' . $mountPoint; } /** @@ -132,4 +134,8 @@ class CachedMountInfo implements ICachedMountInfo { public function getMountProvider(): string { return $this->mountProvider; } + + public function getKey(): string { + return $this->key; + } } diff --git a/lib/private/Files/Config/LazyStorageMountInfo.php b/lib/private/Files/Config/LazyStorageMountInfo.php index 78055a2cdb8..7e4acb2e129 100644 --- a/lib/private/Files/Config/LazyStorageMountInfo.php +++ b/lib/private/Files/Config/LazyStorageMountInfo.php @@ -39,6 +39,7 @@ class LazyStorageMountInfo extends CachedMountInfo { $this->rootId = 0; $this->storageId = 0; $this->mountPoint = ''; + $this->key = ''; } /** @@ -87,4 +88,11 @@ class LazyStorageMountInfo extends CachedMountInfo { public function getMountProvider(): string { return $this->mount->getMountProvider(); } + + public function getKey(): string { + if (!$this->key) { + $this->key = $this->getRootId() . '::' . $this->getMountPoint(); + } + return $this->key; + } } diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index ae6481e45bb..d251199fd43 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -238,6 +238,11 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { $mounts = array_reduce($mounts, function (array $mounts, array $providerMounts) { return array_merge($mounts, $providerMounts); }, []); + + if (count($mounts) === 0) { + throw new \Exception("No root mounts provided by any provider"); + } + return $mounts; } diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 90f94b6598e..2fb7a7d83f4 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -29,13 +29,11 @@ namespace OC\Files\Config; use OCP\Cache\CappedMemoryCache; -use OCA\Files_Sharing\SharedMount; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; use OCP\Files\Config\ICachedMountFileInfo; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IUserMountCache; -use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\IDBConnection; use OCP\IUser; @@ -78,41 +76,27 @@ class UserMountCache implements IUserMountCache { public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) { $this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user'); - // filter out non-proper storages coming from unit tests - $mounts = array_filter($mounts, function (IMountPoint $mount) { - return $mount instanceof SharedMount || ($mount->getStorage() && $mount->getStorage()->getCache()); - }); - /** @var ICachedMountInfo[] $newMounts */ - $newMounts = array_map(function (IMountPoint $mount) use ($user) { + /** @var array<string, ICachedMountInfo> $newMounts */ + $newMounts = []; + foreach ($mounts as $mount) { // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet) - if ($mount->getStorageRootId() === -1) { - return null; - } else { - return new LazyStorageMountInfo($user, $mount); + if ($mount->getStorageRootId() !== -1) { + $mountInfo = new LazyStorageMountInfo($user, $mount); + $newMounts[$mountInfo->getKey()] = $mountInfo; } - }, $mounts); - $newMounts = array_values(array_filter($newMounts)); - $newMountKeys = array_map(function (ICachedMountInfo $mount) { - return $mount->getRootId() . '::' . $mount->getMountPoint(); - }, $newMounts); - $newMounts = array_combine($newMountKeys, $newMounts); + } $cachedMounts = $this->getMountsForUser($user); if (is_array($mountProviderClasses)) { $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) { // for existing mounts that didn't have a mount provider set // we still want the ones that map to new mounts - $mountKey = $mountInfo->getRootId() . '::' . $mountInfo->getMountPoint(); - if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountKey])) { + if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) { return true; } return in_array($mountInfo->getMountProvider(), $mountProviderClasses); }); } - $cachedRootKeys = array_map(function (ICachedMountInfo $mount) { - return $mount->getRootId() . '::' . $mount->getMountPoint(); - }, $cachedMounts); - $cachedMounts = array_combine($cachedRootKeys, $cachedMounts); $addedMounts = []; $removedMounts = []; @@ -131,46 +115,44 @@ class UserMountCache implements IUserMountCache { $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts); - $this->connection->beginTransaction(); - try { - foreach ($addedMounts as $mount) { - $this->addToCache($mount); - /** @psalm-suppress InvalidArgument */ - $this->mountsForUsers[$user->getUID()][] = $mount; - } - foreach ($removedMounts as $mount) { - $this->removeFromCache($mount); - $index = array_search($mount, $this->mountsForUsers[$user->getUID()]); - unset($this->mountsForUsers[$user->getUID()][$index]); - } - foreach ($changedMounts as $mount) { - $this->updateCachedMount($mount); + if ($addedMounts || $removedMounts || $changedMounts) { + $this->connection->beginTransaction(); + $userUID = $user->getUID(); + try { + foreach ($addedMounts as $mount) { + $this->addToCache($mount); + /** @psalm-suppress InvalidArgument */ + $this->mountsForUsers[$userUID][$mount->getKey()] = $mount; + } + foreach ($removedMounts as $mount) { + $this->removeFromCache($mount); + unset($this->mountsForUsers[$userUID][$mount->getKey()]); + } + foreach ($changedMounts as $mount) { + $this->updateCachedMount($mount); + /** @psalm-suppress InvalidArgument */ + $this->mountsForUsers[$userUID][$mount->getKey()] = $mount; + } + $this->connection->commit(); + } catch (\Throwable $e) { + $this->connection->rollBack(); + throw $e; } - $this->connection->commit(); - } catch (\Throwable $e) { - $this->connection->rollBack(); - throw $e; } $this->eventLogger->end('fs:setup:user:register'); } /** - * @param ICachedMountInfo[] $newMounts - * @param ICachedMountInfo[] $cachedMounts + * @param array<string, ICachedMountInfo> $newMounts + * @param array<string, ICachedMountInfo> $cachedMounts * @return ICachedMountInfo[] */ private function findChangedMounts(array $newMounts, array $cachedMounts) { - $new = []; - foreach ($newMounts as $mount) { - $new[$mount->getRootId() . '::' . $mount->getMountPoint()] = $mount; - } $changed = []; - foreach ($cachedMounts as $cachedMount) { - $key = $cachedMount->getRootId() . '::' . $cachedMount->getMountPoint(); - if (isset($new[$key])) { - $newMount = $new[$key]; + foreach ($cachedMounts as $key => $cachedMount) { + if (isset($newMounts[$key])) { + $newMount = $newMounts[$key]; if ( - $newMount->getMountPoint() !== $cachedMount->getMountPoint() || $newMount->getStorageId() !== $cachedMount->getStorageId() || $newMount->getMountId() !== $cachedMount->getMountId() || $newMount->getMountProvider() !== $cachedMount->getMountProvider() @@ -238,7 +220,7 @@ class UserMountCache implements IUserMountCache { $row['mount_point'], $row['mount_provider_class'] ?? '', $mount_id, - isset($row['path']) ? $row['path'] : '', + $row['path'] ?? '', ); } @@ -247,20 +229,28 @@ class UserMountCache implements IUserMountCache { * @return ICachedMountInfo[] */ public function getMountsForUser(IUser $user) { - if (!isset($this->mountsForUsers[$user->getUID()])) { + $userUID = $user->getUID(); + if (!isset($this->mountsForUsers[$userUID])) { $builder = $this->connection->getQueryBuilder(); $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') ->from('mounts', 'm') ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID()))); + ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($userUID))); $result = $query->execute(); $rows = $result->fetchAll(); $result->closeCursor(); - $this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); + $this->mountsForUsers[$userUID] = []; + /** @var array<string, ICachedMountInfo> $mounts */ + foreach ($rows as $row) { + $mount = $this->dbRowToMountInfo($row); + if ($mount !== null) { + $this->mountsForUsers[$userUID][$mount->getKey()] = $mount; + } + } } - return $this->mountsForUsers[$user->getUID()]; + return $this->mountsForUsers[$userUID]; } /** @@ -463,7 +453,7 @@ class UserMountCache implements IUserMountCache { }, $mounts); $mounts = array_combine($mountPoints, $mounts); - $current = $path; + $current = rtrim($path, '/'); // walk up the directory tree until we find a path that has a mountpoint set // the loop will return if a mountpoint is found or break if none are found while (true) { diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index 3937ee16a7c..5ba2f27b78b 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -6,6 +6,7 @@ * @author Joas Schilling <coding@schilljs.com> * @author Julius Härtl <jus@bitgrid.net> * @author Lukas Reschke <lukas@statuscode.ch> + * @author Maxence Lange <maxence@artificial-owl.com> * @author Morris Jobke <hey@morrisjobke.de> * @author Piotr M <mrow4a@yahoo.com> * @author Robin Appelman <robin@icewind.nl> @@ -32,9 +33,10 @@ */ namespace OC\Files; -use OCA\Files_Sharing\ISharedStorage; +use OC\Files\Mount\HomeMountPoint; +use OCA\Files_Sharing\External\Mount; +use OCA\Files_Sharing\ISharedMountPoint; use OCP\Files\Cache\ICacheEntry; -use OCP\Files\IHomeStorage; use OCP\Files\Mount\IMountPoint; use OCP\IUser; @@ -121,21 +123,14 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { */ #[\ReturnTypeWillChange] public function offsetGet($offset) { - if ($offset === 'type') { - return $this->getType(); - } elseif ($offset === 'etag') { - return $this->getEtag(); - } elseif ($offset === 'size') { - return $this->getSize(); - } elseif ($offset === 'mtime') { - return $this->getMTime(); - } elseif ($offset === 'permissions') { - return $this->getPermissions(); - } elseif (isset($this->data[$offset])) { - return $this->data[$offset]; - } else { - return null; - } + return match ($offset) { + 'type' => $this->getType(), + 'etag' => $this->getEtag(), + 'size' => $this->getSize(), + 'mtime' => $this->getMTime(), + 'permissions' => $this->getPermissions(), + default => $this->data[$offset] ?? null, + }; } /** @@ -207,7 +202,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { if ($includeMounts) { $this->updateEntryfromSubMounts(); - if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { + if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { return $this->data['unencrypted_size']; } else { return isset($this->data['size']) ? 0 + $this->data['size'] : 0; @@ -229,11 +224,11 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return bool */ public function isEncrypted() { - return $this->data['encrypted']; + return $this->data['encrypted'] ?? false; } /** - * Return the currently version used for the HMAC in the encryption app + * Return the current version used for the HMAC in the encryption app */ public function getEncryptedVersion(): int { return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1; @@ -243,11 +238,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return int */ public function getPermissions() { - $perms = (int) $this->data['permissions']; - if (\OCP\Util::isSharingDisabledForUser() || ($this->isShared() && !\OC\Share\Share::isResharingAllowed())) { - $perms = $perms & ~\OCP\Constants::PERMISSION_SHARE; - } - return $perms; + return (int) $this->data['permissions']; } /** @@ -315,13 +306,12 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return bool */ public function isShared() { - $storage = $this->getStorage(); - return $storage->instanceOfStorage(ISharedStorage::class); + return $this->mount instanceof ISharedMountPoint; } public function isMounted() { - $storage = $this->getStorage(); - return !($storage->instanceOfStorage(IHomeStorage::class) || $storage->instanceOfStorage(ISharedStorage::class)); + $isHome = $this->mount instanceof HomeMountPoint; + return !$isHome && !$this->isShared(); } /** @@ -416,4 +406,16 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { public function getUploadTime(): int { return (int) $this->data['upload_time']; } + + public function getParentId(): int { + return $this->data['parent'] ?? -1; + } + + /** + * @inheritDoc + * @return array<string, int|string|bool|float|string[]|int[]> + */ + public function getMetadata(): array { + return $this->data['metadata'] ?? []; + } } diff --git a/lib/private/Files/Mount/HomeMountPoint.php b/lib/private/Files/Mount/HomeMountPoint.php new file mode 100644 index 00000000000..0bec12af5c2 --- /dev/null +++ b/lib/private/Files/Mount/HomeMountPoint.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Mount; + +use OCP\Files\Storage\IStorageFactory; +use OCP\IUser; + +class HomeMountPoint extends MountPoint { + private IUser $user; + + public function __construct( + IUser $user, + $storage, + string $mountpoint, + array $arguments = null, + IStorageFactory $loader = null, + array $mountOptions = null, + int $mountId = null, + string $mountProvider = null + ) { + parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId, $mountProvider); + $this->user = $user; + } + + public function getUser(): IUser { + return $this->user; + } +} diff --git a/lib/private/Files/Mount/LocalHomeMountProvider.php b/lib/private/Files/Mount/LocalHomeMountProvider.php index 25a67fc1574..964b607d152 100644 --- a/lib/private/Files/Mount/LocalHomeMountProvider.php +++ b/lib/private/Files/Mount/LocalHomeMountProvider.php @@ -38,6 +38,6 @@ class LocalHomeMountProvider implements IHomeMountProvider { */ public function getHomeMountForUser(IUser $user, IStorageFactory $loader) { $arguments = ['user' => $user]; - return new MountPoint('\OC\Files\Storage\Home', '/' . $user->getUID(), $arguments, $loader, null, null, self::class); + return new HomeMountPoint($user, '\OC\Files\Storage\Home', '/' . $user->getUID(), $arguments, $loader, null, null, self::class); } } diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php index 805cce658a6..94304ff4838 100644 --- a/lib/private/Files/Mount/Manager.php +++ b/lib/private/Files/Mount/Manager.php @@ -10,6 +10,7 @@ declare(strict_types=1); * @author Robin Appelman <robin@icewind.nl> * @author Robin McCorkell <robin@mccorkell.me.uk> * @author Roeland Jago Douma <roeland@famdouma.nl> + * @author Jonas <jonas@freesources.org> * * @license AGPL-3.0 * @@ -33,6 +34,7 @@ use OCP\Cache\CappedMemoryCache; use OC\Files\Filesystem; use OC\Files\SetupManager; use OC\Files\SetupManagerFactory; +use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; @@ -99,6 +101,15 @@ class Manager implements IMountManager { return $this->pathCache[$path]; } + + + if (count($this->mounts) === 0) { + $this->setupManager->setupRoot(); + if (count($this->mounts) === 0) { + throw new \Exception("No mounts even after explicitly setting up the root mounts"); + } + } + $current = $path; while (true) { $mountPoint = $current . '/'; @@ -115,7 +126,7 @@ class Manager implements IMountManager { } } - throw new NotFoundException("No mount for path " . $path . " existing mounts: " . implode(",", array_keys($this->mounts))); + throw new NotFoundException("No mount for path " . $path . " existing mounts (" . count($this->mounts) ."): " . implode(",", array_keys($this->mounts))); } /** @@ -226,4 +237,21 @@ class Manager implements IMountManager { }); } } + + /** + * Return the mount matching a cached mount info (or mount file info) + * + * @param ICachedMountInfo $info + * + * @return IMountPoint|null + */ + public function getMountFromMountInfo(ICachedMountInfo $info): ?IMountPoint { + $this->setupManager->setupForPath($info->getMountPoint()); + foreach ($this->mounts as $mount) { + if ($mount->getMountPoint() === $info->getMountPoint()) { + return $mount; + } + } + return null; + } } diff --git a/lib/private/Files/Mount/MountPoint.php b/lib/private/Files/Mount/MountPoint.php index f526928cc15..fe6358b32f1 100644 --- a/lib/private/Files/Mount/MountPoint.php +++ b/lib/private/Files/Mount/MountPoint.php @@ -272,7 +272,7 @@ class MountPoint implements IMountPoint { * @return mixed */ public function getOption($name, $default) { - return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default; + return $this->mountOptions[$name] ?? $default; } /** diff --git a/lib/private/Files/Mount/ObjectHomeMountProvider.php b/lib/private/Files/Mount/ObjectHomeMountProvider.php index 77912adfd34..3593a95c311 100644 --- a/lib/private/Files/Mount/ObjectHomeMountProvider.php +++ b/lib/private/Files/Mount/ObjectHomeMountProvider.php @@ -65,7 +65,7 @@ class ObjectHomeMountProvider implements IHomeMountProvider { return null; } - return new MountPoint('\OC\Files\ObjectStore\HomeObjectStoreStorage', '/' . $user->getUID(), $config['arguments'], $loader, null, null, self::class); + return new HomeMountPoint($user, '\OC\Files\ObjectStore\HomeObjectStoreStorage', '/' . $user->getUID(), $config['arguments'], $loader, null, null, self::class); } /** @@ -122,7 +122,7 @@ class ObjectHomeMountProvider implements IHomeMountProvider { $config['arguments']['bucket'] = ''; } $mapper = new \OC\Files\ObjectStore\Mapper($user, $this->config); - $numBuckets = isset($config['arguments']['num_buckets']) ? $config['arguments']['num_buckets'] : 64; + $numBuckets = $config['arguments']['num_buckets'] ?? 64; $config['arguments']['bucket'] .= $mapper->getBucket($numBuckets); $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $config['arguments']['bucket']); diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index ccd10da9d0c..c7462572fed 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -177,7 +177,7 @@ class Folder extends Node implements \OCP\Files\Folder { * @throws \OCP\Files\NotPermittedException */ public function newFile($path, $content = null) { - if (empty($path)) { + if ($path === '') { throw new NotPermittedException('Could not create as provided path is empty'); } if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) { diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php index d495d6f4c57..393b3bbeb06 100644 --- a/lib/private/Files/Node/LazyFolder.php +++ b/lib/private/Files/Node/LazyFolder.php @@ -5,6 +5,7 @@ declare(strict_types=1); /** * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> * + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * * @license GNU AGPL version 3 or any later version @@ -26,10 +27,13 @@ declare(strict_types=1); namespace OC\Files\Node; +use OC\Files\Filesystem; use OC\Files\Utils\PathHelper; use OCP\Files\Folder; use OCP\Constants; +use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; +use OCP\Files\NotPermittedException; /** * Class LazyFolder @@ -41,23 +45,33 @@ use OCP\Files\Mount\IMountPoint; */ class LazyFolder implements Folder { /** @var \Closure(): Folder */ - private $folderClosure; - - /** @var LazyFolder | null */ - protected $folder = null; - + private \Closure $folderClosure; + protected ?Folder $folder = null; + protected IRootFolder $rootFolder; protected array $data; /** - * LazyFolder constructor. - * + * @param IRootFolder $rootFolder * @param \Closure(): Folder $folderClosure + * @param array $data */ - public function __construct(\Closure $folderClosure, array $data = []) { + public function __construct(IRootFolder $rootFolder, \Closure $folderClosure, array $data = []) { + $this->rootFolder = $rootFolder; $this->folderClosure = $folderClosure; $this->data = $data; } + protected function getRootFolder(): IRootFolder { + return $this->rootFolder; + } + + protected function getRealFolder(): Folder { + if ($this->folder === null) { + $this->folder = call_user_func($this->folderClosure); + } + return $this->folder; + } + /** * Magic method to first get the real rootFolder and then * call $method with $args on it @@ -67,11 +81,7 @@ class LazyFolder implements Folder { * @return mixed */ public function __call($method, $args) { - if ($this->folder === null) { - $this->folder = call_user_func($this->folderClosure); - } - - return call_user_func_array([$this->folder, $method], $args); + return call_user_func_array([$this->getRealFolder(), $method], $args); } /** @@ -148,7 +158,7 @@ class LazyFolder implements Folder { * @inheritDoc */ public function get($path) { - return $this->__call(__FUNCTION__, func_get_args()); + return $this->getRootFolder()->get($this->getFullPath($path)); } /** @@ -207,6 +217,9 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getId() { + if (isset($this->data['fileid'])) { + return $this->data['fileid']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -221,6 +234,9 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getMTime() { + if (isset($this->data['mtime'])) { + return $this->data['mtime']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -228,6 +244,9 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getSize($includeMounts = true): int|float { + if (isset($this->data['size'])) { + return $this->data['size']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -235,6 +254,9 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getEtag() { + if (isset($this->data['etag'])) { + return $this->data['etag']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -299,6 +321,12 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getName() { + if (isset($this->data['path'])) { + return basename($this->data['path']); + } + if (isset($this->data['name'])) { + return $this->data['name']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -390,6 +418,13 @@ class LazyFolder implements Folder { * @inheritDoc */ public function getFullPath($path) { + if (isset($this->data['path'])) { + $path = PathHelper::normalizePath($path); + if (!Filesystem::isValidPath($path)) { + throw new NotPermittedException('Invalid path "' . $path . '"'); + } + return $this->data['path'] . $path; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -533,4 +568,19 @@ class LazyFolder implements Folder { public function getRelativePath($path) { return PathHelper::getRelativePath($this->getPath(), $path); } + + public function getParentId(): int { + if (isset($this->data['parent'])) { + return $this->data['parent']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + * @return array<string, int|string|bool|float|string[]|int[]> + */ + public function getMetadata(): array { + return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args()); + } } diff --git a/lib/private/Files/Node/LazyRoot.php b/lib/private/Files/Node/LazyRoot.php index c01b9fdbb83..680e80cb45e 100644 --- a/lib/private/Files/Node/LazyRoot.php +++ b/lib/private/Files/Node/LazyRoot.php @@ -22,7 +22,10 @@ */ namespace OC\Files\Node; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node as INode; /** * Class LazyRoot @@ -33,9 +36,18 @@ use OCP\Files\IRootFolder; * @package OC\Files\Node */ class LazyRoot extends LazyFolder implements IRootFolder { - /** - * @inheritDoc - */ + public function __construct(\Closure $folderClosure, array $data = []) { + parent::__construct($this, $folderClosure, $data); + } + + protected function getRootFolder(): IRootFolder { + $folder = $this->getRealFolder(); + if (!$folder instanceof IRootFolder) { + throw new \Exception('Lazy root folder closure didn\'t return a root folder'); + } + return $folder; + } + public function getUserFolder($userId) { return $this->__call(__FUNCTION__, func_get_args()); } @@ -43,4 +55,8 @@ class LazyRoot extends LazyFolder implements IRootFolder { public function getByIdInPath(int $id, string $path) { return $this->__call(__FUNCTION__, func_get_args()); } + + public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode { + return $this->getRootFolder()->getNodeFromCacheEntryAndMount($cacheEntry, $mountPoint); + } } diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php index 8fbdec4b49d..503b0af8921 100644 --- a/lib/private/Files/Node/LazyUserFolder.php +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -34,19 +34,17 @@ use OCP\IUser; use Psr\Log\LoggerInterface; class LazyUserFolder extends LazyFolder { - private IRootFolder $root; private IUser $user; private string $path; private IMountManager $mountManager; public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager $mountManager) { - $this->root = $rootFolder; $this->user = $user; $this->mountManager = $mountManager; $this->path = '/' . $user->getUID() . '/files'; - parent::__construct(function () use ($user): Folder { + parent::__construct($rootFolder, function () use ($user): Folder { try { - $node = $this->root->get($this->path); + $node = $this->getRootFolder()->get($this->path); if ($node instanceof File) { $e = new \RuntimeException(); \OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [ @@ -56,21 +54,22 @@ class LazyUserFolder extends LazyFolder { } return $node; } catch (NotFoundException $e) { - if (!$this->root->nodeExists('/' . $user->getUID())) { - $this->root->newFolder('/' . $user->getUID()); + if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) { + $this->getRootFolder()->newFolder('/' . $user->getUID()); } - return $this->root->newFolder($this->path); + return $this->getRootFolder()->newFolder($this->path); } }, [ 'path' => $this->path, - 'permissions' => Constants::PERMISSION_ALL, + // Sharing user root folder is not allowed + 'permissions' => Constants::PERMISSION_ALL ^ Constants::PERMISSION_SHARE, 'type' => FileInfo::TYPE_FOLDER, 'mimetype' => FileInfo::MIMETYPE_FOLDER, ]); } public function get($path) { - return $this->root->get('/' . $this->user->getUID() . '/files/' . ltrim($path, '/')); + return $this->getRootFolder()->get('/' . $this->user->getUID() . '/files/' . ltrim($path, '/')); } /** @@ -78,7 +77,7 @@ class LazyUserFolder extends LazyFolder { * @return \OCP\Files\Node[] */ public function getById($id) { - return $this->root->getByIdInPath((int)$id, $this->getPath()); + return $this->getRootFolder()->getByIdInPath((int)$id, $this->getPath()); } public function getMountPoint() { diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php index 61ae762638f..acd91c56d3f 100644 --- a/lib/private/Files/Node/Node.php +++ b/lib/private/Files/Node/Node.php @@ -7,6 +7,7 @@ * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author Joas Schilling <coding@schilljs.com> * @author Julius Härtl <jus@bitgrid.net> + * @author Maxence Lange <maxence@artificial-owl.com> * @author Morris Jobke <hey@morrisjobke.de> * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> @@ -43,7 +44,7 @@ use OCP\Files\NotPermittedException; use OCP\Lock\LockedException; use OCP\PreConditionNotMetException; -// FIXME: this class really should be abstract +// FIXME: this class really should be abstract (+1) class Node implements INode { /** * @var \OC\Files\View $view @@ -59,10 +60,7 @@ class Node implements INode { protected ?FileInfo $fileInfo; - /** - * @var Node|null - */ - protected $parent; + protected ?INode $parent; private bool $infoHasSubMountsIncluded; @@ -72,7 +70,7 @@ class Node implements INode { * @param string $path * @param FileInfo $fileInfo */ - public function __construct(IRootFolder $root, $view, $path, $fileInfo = null, ?Node $parent = null, bool $infoHasSubMountsIncluded = true) { + public function __construct(IRootFolder $root, $view, $path, $fileInfo = null, ?INode $parent = null, bool $infoHasSubMountsIncluded = true) { if (Filesystem::normalizePath($view->getRoot()) !== '/') { throw new PreConditionNotMetException('The view passed to the node should not have any fake root set'); } @@ -134,7 +132,14 @@ class Node implements INode { if (method_exists($this->root, 'emit')) { $this->root->emit('\OC\Files', $hook, $args); } - $dispatcher->dispatch('\OCP\Files::' . $hook, new GenericEvent($args)); + + if (in_array($hook, ['preWrite', 'postWrite', 'preCreate', 'postCreate', 'preTouch', 'postTouch', 'preDelete', 'postDelete'], true)) { + $event = new GenericEvent($args[0]); + } else { + $event = new GenericEvent($args); + } + + $dispatcher->dispatch('\OCP\Files::' . $hook, $event); } } @@ -300,7 +305,25 @@ class Node implements INode { return $this->root; } - $this->parent = $this->root->get($newPath); + // Manually fetch the parent if the current node doesn't have a file info yet + try { + $fileInfo = $this->getFileInfo(); + } catch (NotFoundException) { + $this->parent = $this->root->get($newPath); + /** @var \OCP\Files\Folder $this->parent */ + return $this->parent; + } + + // gather the metadata we already know about our parent + $parentData = [ + 'path' => $newPath, + 'fileid' => $fileInfo->getParentId(), + ]; + + // and create lazy folder with it instead of always querying + $this->parent = new LazyFolder($this->root, function () use ($newPath) { + return $this->root->get($newPath); + }, $parentData); } return $this->parent; @@ -328,13 +351,7 @@ class Node implements INode { * @return bool */ public function isValidPath($path) { - if (!$path || $path[0] !== '/') { - $path = '/' . $path; - } - if (strstr($path, '/../') || strrchr($path, '/') === '/..') { - return false; - } - return true; + return Filesystem::isValidPath($path); } public function isMounted() { @@ -477,4 +494,16 @@ class Node implements INode { public function getUploadTime(): int { return $this->getFileInfo()->getUploadTime(); } + + public function getParentId(): int { + return $this->fileInfo->getParentId(); + } + + /** + * @inheritDoc + * @return array<string, int|string|bool|float|string[]|int[]> + */ + public function getMetadata(): array { + return $this->fileInfo->getMetadata(); + } } diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 7bd88981ff2..1195b644083 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -41,6 +41,7 @@ use OC\Files\View; use OC\Hooks\PublicEmitter; use OC\User\NoUserException; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Config\IUserMountCache; use OCP\Files\Events\Node\FilesystemTornDownEvent; use OCP\Files\IRootFolder; @@ -487,4 +488,29 @@ class Root extends Folder implements IRootFolder { }); return $folders; } + + public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode { + $path = $cacheEntry->getPath(); + $fullPath = $mountPoint->getMountPoint() . $path; + // todo: LazyNode? + $info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint); + $parentPath = dirname($fullPath); + $parent = new LazyFolder($this, function () use ($parentPath) { + $parent = $this->get($parentPath); + if ($parent instanceof \OCP\Files\Folder) { + return $parent; + } else { + throw new \Exception("parent $parentPath is not a folder"); + } + }, [ + 'path' => $parentPath, + ]); + $isDir = $info->getType() === FileInfo::TYPE_FOLDER; + $view = new View(''); + if ($isDir) { + return new Folder($this, $view, $path, $info, $parent); + } else { + return new File($this, $view, $path, $info, $parent); + } + } } diff --git a/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php b/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php index 824adcc1d0e..b361249ff47 100644 --- a/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php @@ -26,6 +26,7 @@ namespace OC\Files\ObjectStore; use OC\User\User; +use OCP\IUser; class HomeObjectStoreStorage extends ObjectStoreStorage implements \OCP\Files\IHomeStorage { /** @@ -61,7 +62,7 @@ class HomeObjectStoreStorage extends ObjectStoreStorage implements \OCP\Files\IH * @param string $path, optional * @return \OC\User\User */ - public function getUser($path = null) { + public function getUser($path = null): IUser { return $this->user; } } diff --git a/lib/private/Files/ObjectStore/ObjectStoreScanner.php b/lib/private/Files/ObjectStore/ObjectStoreScanner.php index f001f90fdaa..d827662ae0b 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreScanner.php +++ b/lib/private/Files/ObjectStore/ObjectStoreScanner.php @@ -39,7 +39,7 @@ class ObjectStoreScanner extends Scanner { return []; } - protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int $oldSize) { + protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize) { return 0; } diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index d918bd98729..4dceee9a58b 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -559,6 +559,8 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil } if ($exists) { + // Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways + $stat['unencrypted_size'] = $stat['size']; $this->getCache()->update($fileId, $stat); } else { if (!$this->validateWrites || $this->objectStore->objectExists($urn)) { diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index 49942b385bc..044c3cdc900 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -128,7 +128,7 @@ trait S3ConnectionTrait { ); $options = [ - 'version' => isset($this->params['version']) ? $this->params['version'] : 'latest', + 'version' => $this->params['version'] ?? 'latest', 'credentials' => $provider, 'endpoint' => $base_url, 'region' => $this->params['region'], diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index e0d0f2ce9c7..217e1a1a2ff 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -27,6 +27,7 @@ namespace OC\Files\ObjectStore; use Aws\S3\Exception\S3MultipartUploadException; +use Aws\S3\MultipartCopy; use Aws\S3\MultipartUploader; use Aws\S3\S3Client; use GuzzleHttp\Psr7; @@ -189,9 +190,22 @@ trait S3ObjectTrait { return $this->getConnection()->doesObjectExist($this->bucket, $urn, $this->getSSECParameters()); } - public function copyObject($from, $to) { - $this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', [ - 'params' => $this->getSSECParameters() + $this->getSSECParameters(true) - ]); + public function copyObject($from, $to, array $options = []) { + $sourceMetadata = $this->getConnection()->headObject([ + 'Bucket' => $this->getBucket(), + 'Key' => $from, + ] + $this->getSSECParameters()); + + $copy = new MultipartCopy($this->getConnection(), [ + "source_bucket" => $this->getBucket(), + "source_key" => $from + ], array_merge([ + "bucket" => $this->getBucket(), + "key" => $to, + "acl" => "private", + "params" => $this->getSSECParameters() + $this->getSSECParameters(true), + "source_metadata" => $sourceMetadata + ], $options)); + $copy->copy(); } } diff --git a/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php b/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php index 0caa9b12a02..664402f1238 100644 --- a/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php +++ b/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php @@ -4,6 +4,9 @@ declare(strict_types=1); /** * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> * + * @author Maxence Lange <maxence@artificial-owl.com> + * @author Robin Appelman <robin@icewind.nl> + * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify @@ -48,7 +51,7 @@ class PathPrefixOptimizer extends QueryOptimizerStep { } public function processOperator(ISearchOperator &$operator) { - if (!$this->useHashEq && $operator instanceof ISearchComparison && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) { + if (!$this->useHashEq && $operator instanceof ISearchComparison && !$operator->getExtra() && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) { $operator->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false); } @@ -69,7 +72,7 @@ class PathPrefixOptimizer extends QueryOptimizerStep { private function operatorPairIsPathPrefix(ISearchOperator $like, ISearchOperator $equal): bool { return ( $like instanceof ISearchComparison && $equal instanceof ISearchComparison && - $like->getField() === 'path' && $equal->getField() === 'path' && + !$like->getExtra() && !$equal->getExtra() && $like->getField() === 'path' && $equal->getField() === 'path' && $like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL && $like->getValue() === SearchComparison::escapeLikeParameter($equal->getValue()) . '/%' ); diff --git a/lib/private/Files/Search/SearchComparison.php b/lib/private/Files/Search/SearchComparison.php index 122a1f730b4..d94b3e9dfab 100644 --- a/lib/private/Files/Search/SearchComparison.php +++ b/lib/private/Files/Search/SearchComparison.php @@ -1,7 +1,10 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * * @license GNU AGPL version 3 or any later version @@ -25,48 +28,45 @@ namespace OC\Files\Search; use OCP\Files\Search\ISearchComparison; class SearchComparison implements ISearchComparison { - /** @var string */ - private $type; - /** @var string */ - private $field; - /** @var string|integer|\DateTime */ - private $value; - private $hints = []; + private array $hints = []; - /** - * SearchComparison constructor. - * - * @param string $type - * @param string $field - * @param \DateTime|int|string $value - */ - public function __construct($type, $field, $value) { - $this->type = $type; - $this->field = $field; - $this->value = $value; + public function __construct( + private string $type, + private string $field, + private \DateTime|int|string|bool $value, + private string $extra = '' + ) { } /** * @return string */ - public function getType() { + public function getType(): string { return $this->type; } /** * @return string */ - public function getField() { + public function getField(): string { return $this->field; } /** - * @return \DateTime|int|string + * @return \DateTime|int|string|bool */ - public function getValue() { + public function getValue(): string|int|bool|\DateTime { return $this->value; } + /** + * @return string + * @since 28.0.0 + */ + public function getExtra(): string { + return $this->extra; + } + public function getQueryHint(string $name, $default) { return $this->hints[$name] ?? $default; } diff --git a/lib/private/Files/Search/SearchOrder.php b/lib/private/Files/Search/SearchOrder.php index 1395a87ac72..de514262bf5 100644 --- a/lib/private/Files/Search/SearchOrder.php +++ b/lib/private/Files/Search/SearchOrder.php @@ -2,6 +2,7 @@ /** * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * * @license GNU AGPL version 3 or any later version @@ -26,34 +27,33 @@ use OCP\Files\FileInfo; use OCP\Files\Search\ISearchOrder; class SearchOrder implements ISearchOrder { - /** @var string */ - private $direction; - /** @var string */ - private $field; + public function __construct( + private string $direction, + private string $field, + private string $extra = '' + ) { + } /** - * SearchOrder constructor. - * - * @param string $direction - * @param string $field + * @return string */ - public function __construct($direction, $field) { - $this->direction = $direction; - $this->field = $field; + public function getDirection(): string { + return $this->direction; } /** * @return string */ - public function getDirection() { - return $this->direction; + public function getField(): string { + return $this->field; } /** * @return string + * @since 28.0.0 */ - public function getField() { - return $this->field; + public function getExtra(): string { + return $this->extra; } public function sortFileInfo(FileInfo $a, FileInfo $b): int { diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index b44ead003a8..511e80bd7d9 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -24,8 +24,8 @@ declare(strict_types=1); namespace OC\Files; use OC\Files\Config\MountProviderCollection; +use OC\Files\Mount\HomeMountPoint; use OC\Files\Mount\MountPoint; -use OC\Files\ObjectStore\HomeObjectStoreStorage; use OC\Files\Storage\Common; use OC\Files\Storage\Home; use OC\Files\Storage\Storage; @@ -34,9 +34,15 @@ use OC\Files\Storage\Wrapper\Encoding; use OC\Files\Storage\Wrapper\PermissionsMask; use OC\Files\Storage\Wrapper\Quota; use OC\Lockdown\Filesystem\NullStorage; +use OC\Share\Share; +use OC\Share20\ShareDisableChecker; use OC_App; use OC_Hook; use OC_Util; +use OCA\Files_External\Config\ConfigAdapter; +use OCA\Files_Sharing\External\Mount; +use OCA\Files_Sharing\ISharedMountPoint; +use OCA\Files_Sharing\SharedMount; use OCP\Constants; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; @@ -64,52 +70,33 @@ use Psr\Log\LoggerInterface; class SetupManager { private bool $rootSetup = false; - private IEventLogger $eventLogger; - private MountProviderCollection $mountProviderCollection; - private IMountManager $mountManager; - private IUserManager $userManager; // List of users for which at least one mount is setup private array $setupUsers = []; // List of users for which all mounts are setup private array $setupUsersComplete = []; /** @var array<string, string[]> */ private array $setupUserMountProviders = []; - private IEventDispatcher $eventDispatcher; - private IUserMountCache $userMountCache; - private ILockdownManager $lockdownManager; - private IUserSession $userSession; private ICache $cache; - private LoggerInterface $logger; - private IConfig $config; private bool $listeningForProviders; private array $fullSetupRequired = []; private bool $setupBuiltinWrappersDone = false; public function __construct( - IEventLogger $eventLogger, - MountProviderCollection $mountProviderCollection, - IMountManager $mountManager, - IUserManager $userManager, - IEventDispatcher $eventDispatcher, - IUserMountCache $userMountCache, - ILockdownManager $lockdownManager, - IUserSession $userSession, + private IEventLogger $eventLogger, + private MountProviderCollection $mountProviderCollection, + private IMountManager $mountManager, + private IUserManager $userManager, + private IEventDispatcher $eventDispatcher, + private IUserMountCache $userMountCache, + private ILockdownManager $lockdownManager, + private IUserSession $userSession, ICacheFactory $cacheFactory, - LoggerInterface $logger, - IConfig $config + private LoggerInterface $logger, + private IConfig $config, + private ShareDisableChecker $shareDisableChecker, ) { - $this->eventLogger = $eventLogger; - $this->mountProviderCollection = $mountProviderCollection; - $this->mountManager = $mountManager; - $this->userManager = $userManager; - $this->eventDispatcher = $eventDispatcher; - $this->userMountCache = $userMountCache; - $this->lockdownManager = $lockdownManager; - $this->logger = $logger; - $this->userSession = $userSession; $this->cache = $cacheFactory->createDistributed('setupmanager::'); $this->listeningForProviders = false; - $this->config = $config; $this->setupListeners(); } @@ -133,52 +120,55 @@ class SetupManager { $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - if ($storage->instanceOfStorage(Common::class)) { + if ($mount->getOptions() && $storage->instanceOfStorage(Common::class)) { $storage->setMountOptions($mount->getOptions()); } return $storage; }); - Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - if (!$mount->getOption('enable_sharing', true)) { - return new PermissionsMask([ - 'storage' => $storage, - 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, - ]); + $reSharingEnabled = Share::isResharingAllowed(); + $user = $this->userSession->getUser(); + $sharingEnabledForUser = $user ? !$this->shareDisableChecker->sharingDisabledForUser($user->getUID()) : true; + Filesystem::addStorageWrapper( + 'sharing_mask', + function ($mountPoint, IStorage $storage, IMountPoint $mount) use ($reSharingEnabled, $sharingEnabledForUser) { + $sharingEnabledForMount = $mount->getOption('enable_sharing', true); + $isShared = $mount instanceof ISharedMountPoint; + if (!$sharingEnabledForMount || !$sharingEnabledForUser || (!$reSharingEnabled && $isShared)) { + return new PermissionsMask([ + 'storage' => $storage, + 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, + ]); + } + return $storage; } - return $storage; - }); + ); // install storage availability wrapper, before most other wrappers - Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage) { - if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$storage->isLocal()) { + Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage, IMountPoint $mount) { + $externalMount = $mount instanceof ConfigAdapter || $mount instanceof Mount; + if ($externalMount && !$storage->isLocal()) { return new Availability(['storage' => $storage]); } return $storage; }); Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - if ($mount->getOption('encoding_compatibility', false) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { + if ($mount->getOption('encoding_compatibility', false) && !$mount instanceof SharedMount) { return new Encoding(['storage' => $storage]); } return $storage; }); $quotaIncludeExternal = $this->config->getSystemValue('quota_include_external_storage', false); - Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) use ($quotaIncludeExternal) { + Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage, IMountPoint $mount) use ($quotaIncludeExternal) { // set up quota for home storages, even for other users // which can happen when using sharing - - /** - * @var Storage $storage - */ - if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) { - if (is_object($storage->getUser())) { - $user = $storage->getUser(); - return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) { - return OC_Util::getUserQuota($user); - }, 'root' => 'files', 'include_external_storage' => $quotaIncludeExternal]); - } + if ($mount instanceof HomeMountPoint) { + $user = $mount->getUser(); + return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) { + return OC_Util::getUserQuota($user); + }, 'root' => 'files', 'include_external_storage' => $quotaIncludeExternal]); } return $storage; @@ -345,12 +335,13 @@ class SetupManager { if ($this->rootSetup) { return; } + + $this->setupBuiltinWrappers(); + $this->rootSetup = true; $this->eventLogger->start('fs:setup:root', 'Setup root filesystem'); - $this->setupBuiltinWrappers(); - $rootMounts = $this->mountProviderCollection->getRootMounts(); foreach ($rootMounts as $rootMountProvider) { $this->mountManager->addMount($rootMountProvider); diff --git a/lib/private/Files/SetupManagerFactory.php b/lib/private/Files/SetupManagerFactory.php index 1d9efbd411f..8589cbdea42 100644 --- a/lib/private/Files/SetupManagerFactory.php +++ b/lib/private/Files/SetupManagerFactory.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace OC\Files; +use OC\Share20\ShareDisableChecker; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProviderCollection; @@ -36,40 +37,21 @@ use OCP\Lockdown\ILockdownManager; use Psr\Log\LoggerInterface; class SetupManagerFactory { - private IEventLogger $eventLogger; - private IMountProviderCollection $mountProviderCollection; - private IUserManager $userManager; - private IEventDispatcher $eventDispatcher; - private IUserMountCache $userMountCache; - private ILockdownManager $lockdownManager; - private IUserSession $userSession; private ?SetupManager $setupManager; - private ICacheFactory $cacheFactory; - private LoggerInterface $logger; - private IConfig $config; public function __construct( - IEventLogger $eventLogger, - IMountProviderCollection $mountProviderCollection, - IUserManager $userManager, - IEventDispatcher $eventDispatcher, - IUserMountCache $userMountCache, - ILockdownManager $lockdownManager, - IUserSession $userSession, - ICacheFactory $cacheFactory, - LoggerInterface $logger, - IConfig $config + private IEventLogger $eventLogger, + private IMountProviderCollection $mountProviderCollection, + private IUserManager $userManager, + private IEventDispatcher $eventDispatcher, + private IUserMountCache $userMountCache, + private ILockdownManager $lockdownManager, + private IUserSession $userSession, + private ICacheFactory $cacheFactory, + private LoggerInterface $logger, + private IConfig $config, + private ShareDisableChecker $shareDisableChecker, ) { - $this->eventLogger = $eventLogger; - $this->mountProviderCollection = $mountProviderCollection; - $this->userManager = $userManager; - $this->eventDispatcher = $eventDispatcher; - $this->userMountCache = $userMountCache; - $this->lockdownManager = $lockdownManager; - $this->userSession = $userSession; - $this->cacheFactory = $cacheFactory; - $this->logger = $logger; - $this->config = $config; $this->setupManager = null; } @@ -86,7 +68,8 @@ class SetupManagerFactory { $this->userSession, $this->cacheFactory, $this->logger, - $this->config + $this->config, + $this->shareDisableChecker, ); } return $this->setupManager; diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 5ab411434d0..3d5a2f098b2 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -601,7 +601,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { * @return mixed */ public function getMountOption($name, $default = null) { - return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default; + return $this->mountOptions[$name] ?? $default; } /** diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 70f22a17034..2d2bb52635b 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -120,9 +120,9 @@ class DAV extends Common { if (isset($params['host']) && isset($params['user']) && isset($params['password'])) { $host = $params['host']; //remove leading http[s], will be generated in createBaseUri() - if (substr($host, 0, 8) == "https://") { + if (str_starts_with($host, "https://")) { $host = substr($host, 8); - } elseif (substr($host, 0, 7) == "http://") { + } elseif (str_starts_with($host, "http://")) { $host = substr($host, 7); } $this->host = $host; diff --git a/lib/private/Files/Storage/Home.php b/lib/private/Files/Storage/Home.php index 5427bc425c2..5100b15215b 100644 --- a/lib/private/Files/Storage/Home.php +++ b/lib/private/Files/Storage/Home.php @@ -26,6 +26,7 @@ namespace OC\Files\Storage; use OC\Files\Cache\HomePropagator; +use OCP\IUser; /** * Specialized version of Local storage for home directory usage @@ -94,7 +95,7 @@ class Home extends Local implements \OCP\Files\IHomeStorage { * * @return \OC\User\User owner of this home storage */ - public function getUser() { + public function getUser(): IUser { return $this->user; } diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 02708ed4f7d..0fca853da59 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -51,6 +51,7 @@ use OCP\Files\ForbiddenException; use OCP\Files\GenericFileException; use OCP\Files\IMimeTypeDetector; use OCP\Files\Storage\IStorage; +use OCP\Files\StorageNotAvailableException; use OCP\IConfig; use OCP\Util; use Psr\Log\LoggerInterface; @@ -73,6 +74,8 @@ class Local extends \OC\Files\Storage\Common { protected bool $unlinkOnTruncate; + protected bool $caseInsensitive = false; + public function __construct($arguments) { if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) { throw new \InvalidArgumentException('No data directory set for local storage'); @@ -85,16 +88,23 @@ class Local extends \OC\Files\Storage\Common { $realPath = realpath($this->datadir) ?: $this->datadir; $this->realDataDir = rtrim($realPath, '/') . '/'; } - if (substr($this->datadir, -1) !== '/') { + if (!str_ends_with($this->datadir, '/')) { $this->datadir .= '/'; } $this->dataDirLength = strlen($this->realDataDir); $this->config = \OC::$server->get(IConfig::class); $this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class); $this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022); + $this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false); // support Write-Once-Read-Many file systems $this->unlinkOnTruncate = $this->config->getSystemValueBool('localstorage.unlink_on_truncate', false); + + if (isset($arguments['isExternal']) && $arguments['isExternal'] && !$this->stat('')) { + // data dir not accessible or available, can happen when using an external storage of type Local + // on an unmounted system mount point + throw new StorageNotAvailableException('Local storage path does not exist "' . $this->getSourcePath('') . '"'); + } } public function __destruct() { @@ -155,13 +165,19 @@ class Local extends \OC\Files\Storage\Common { } public function is_dir($path) { - if (substr($path, -1) == '/') { + if ($this->caseInsensitive && !$this->file_exists($path)) { + return false; + } + if (str_ends_with($path, '/')) { $path = substr($path, 0, -1); } return is_dir($this->getSourcePath($path)); } public function is_file($path) { + if ($this->caseInsensitive && !$this->file_exists($path)) { + return false; + } return is_file($this->getSourcePath($path)); } @@ -264,7 +280,13 @@ class Local extends \OC\Files\Storage\Common { } public function file_exists($path) { - return file_exists($this->getSourcePath($path)); + if ($this->caseInsensitive) { + $fullPath = $this->getSourcePath($path); + $content = scandir(dirname($fullPath), SCANDIR_SORT_NONE); + return is_array($content) && array_search(basename($fullPath), $content) !== false; + } else { + return file_exists($this->getSourcePath($path)); + } } public function filemtime($path) { @@ -365,6 +387,11 @@ class Local extends \OC\Files\Storage\Common { } if (@rename($this->getSourcePath($source), $this->getSourcePath($target))) { + if ($this->caseInsensitive) { + if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) { + return false; + } + } return true; } @@ -381,6 +408,11 @@ class Local extends \OC\Files\Storage\Common { } $result = copy($this->getSourcePath($source), $this->getSourcePath($target)); umask($oldMask); + if ($this->caseInsensitive) { + if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) { + return false; + } + } return $result; } } diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index d559454fcb7..7ce4338256f 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -1071,7 +1071,7 @@ class Encryption extends Wrapper { // object store, stores the size after write and doesn't update this during scan // manually store the unencrypted size - if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) { + if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class) && $this->shouldEncrypt($path)) { $this->getCache()->put($path, ['unencrypted_size' => $count]); } diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index 1921ac27848..592acd418ec 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -396,10 +396,7 @@ class Jail extends Wrapper { * @return \OC\Files\Cache\Cache */ public function getCache($path = '', $storage = null) { - if (!$storage) { - $storage = $this->getWrapperStorage(); - } - $sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path), $storage); + $sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path)); return new CacheJail($sourceCache, $this->rootPath); } diff --git a/lib/private/Files/Storage/Wrapper/KnownMtime.php b/lib/private/Files/Storage/Wrapper/KnownMtime.php new file mode 100644 index 00000000000..dde209c44ab --- /dev/null +++ b/lib/private/Files/Storage/Wrapper/KnownMtime.php @@ -0,0 +1,142 @@ +<?php + +namespace OC\Files\Storage\Wrapper; + +use OCP\Cache\CappedMemoryCache; +use OCP\Files\Storage\IStorage; +use Psr\Clock\ClockInterface; + +/** + * Wrapper that overwrites the mtime return by stat/getMetaData if the returned value + * is lower than when we last modified the file. + * + * This is useful because some storage servers can return an outdated mtime right after writes + */ +class KnownMtime extends Wrapper { + private CappedMemoryCache $knowMtimes; + private ClockInterface $clock; + + public function __construct($arguments) { + parent::__construct($arguments); + $this->knowMtimes = new CappedMemoryCache(); + $this->clock = $arguments['clock']; + } + + public function file_put_contents($path, $data) { + $result = parent::file_put_contents($path, $data); + if ($result) { + $now = $this->clock->now()->getTimestamp(); + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function stat($path) { + $stat = parent::stat($path); + if ($stat) { + $this->applyKnownMtime($path, $stat); + } + return $stat; + } + + public function getMetaData($path) { + $stat = parent::getMetaData($path); + if ($stat) { + $this->applyKnownMtime($path, $stat); + } + return $stat; + } + + private function applyKnownMtime(string $path, array &$stat) { + if (isset($stat['mtime'])) { + $knownMtime = $this->knowMtimes->get($path) ?? 0; + $stat['mtime'] = max($stat['mtime'], $knownMtime); + } + } + + public function filemtime($path) { + $knownMtime = $this->knowMtimes->get($path) ?? 0; + return max(parent::filemtime($path), $knownMtime); + } + + public function mkdir($path) { + $result = parent::mkdir($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function rmdir($path) { + $result = parent::rmdir($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function unlink($path) { + $result = parent::unlink($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function rename($source, $target) { + $result = parent::rename($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); + $this->knowMtimes->set($source, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function copy($source, $target) { + $result = parent::copy($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function fopen($path, $mode) { + $result = parent::fopen($path, $mode); + if ($result && $mode === 'w') { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function touch($path, $mtime = null) { + $result = parent::touch($path, $mtime); + if ($result) { + $this->knowMtimes->set($path, $mtime ?? $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function writeStream(string $path, $stream, int $size = null): int { + $result = parent::writeStream($path, $stream, $size); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } +} diff --git a/lib/private/Files/Storage/Wrapper/PermissionsMask.php b/lib/private/Files/Storage/Wrapper/PermissionsMask.php index 0d140e0a39d..a79eaad0569 100644 --- a/lib/private/Files/Storage/Wrapper/PermissionsMask.php +++ b/lib/private/Files/Storage/Wrapper/PermissionsMask.php @@ -140,7 +140,7 @@ class PermissionsMask extends Wrapper { $data = parent::getMetaData($path); if ($data && isset($data['permissions'])) { - $data['scan_permissions'] = isset($data['scan_permissions']) ? $data['scan_permissions'] : $data['permissions']; + $data['scan_permissions'] = $data['scan_permissions'] ?? $data['permissions']; $data['permissions'] &= $this->mask; } return $data; @@ -155,7 +155,7 @@ class PermissionsMask extends Wrapper { public function getDirectoryContent($directory): \Traversable { foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) { - $data['scan_permissions'] = isset($data['scan_permissions']) ? $data['scan_permissions'] : $data['permissions']; + $data['scan_permissions'] = $data['scan_permissions'] ?? $data['permissions']; $data['permissions'] &= $this->mask; yield $data; diff --git a/lib/private/Files/Template/TemplateManager.php b/lib/private/Files/Template/TemplateManager.php index bf72e9e23e8..878680caa4e 100644 --- a/lib/private/Files/Template/TemplateManager.php +++ b/lib/private/Files/Template/TemplateManager.php @@ -240,7 +240,8 @@ class TemplateManager implements ITemplateManager { 'mime' => $file->getMimetype(), 'size' => $file->getSize(), 'type' => $file->getType(), - 'hasPreview' => $this->previewManager->isAvailable($file) + 'hasPreview' => $this->previewManager->isAvailable($file), + 'permissions' => $file->getPermissions(), ]; } diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php index 20c298f21b3..7032e619385 100644 --- a/lib/private/Files/Type/Loader.php +++ b/lib/private/Files/Type/Loader.php @@ -116,8 +116,8 @@ class Loader implements IMimeTypeLoader { * @return int inserted ID */ protected function store($mimetype) { - $mimetypeId = $this->atomic(function () use ($mimetype) { - try { + try { + $mimetypeId = $this->atomic(function () use ($mimetype) { $insert = $this->dbConnection->getQueryBuilder(); $insert->insert('mimetypes') ->values([ @@ -125,26 +125,24 @@ class Loader implements IMimeTypeLoader { ]) ->executeStatement(); return $insert->getLastInsertId(); - } catch (DbalException $e) { - if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { - throw $e; - } - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('id') - ->from('mimetypes') - ->where($qb->expr()->eq('mimetype', $qb->createNamedParameter($mimetype))); - $result = $qb->executeQuery(); - $id = $result->fetchOne(); - $result->closeCursor(); - if ($id !== false) { - return (int) $id; - } + }, $this->dbConnection); + } catch (DbalException $e) { + if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id') + ->from('mimetypes') + ->where($qb->expr()->eq('mimetype', $qb->createNamedParameter($mimetype))); + $result = $qb->executeQuery(); + $id = $result->fetchOne(); + $result->closeCursor(); + if ($id === false) { throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype"); } - }, $this->dbConnection); - if (!$mimetypeId) { - throw new \Exception("Failed to get mimetype id for $mimetype after trying to store it"); + $mimetypeId = (int) $id; } $this->mimetypes[$mimetypeId] = $mimetype; diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 71815939310..ec0d037af06 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -56,6 +56,7 @@ use OC\User\Manager as UserManager; use OCA\Files_Sharing\SharedMount; use OCP\Constants; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\ConnectionLostException; use OCP\Files\EmptyFileNameException; use OCP\Files\FileNameTooLongException; use OCP\Files\InvalidCharacterInPathException; @@ -397,10 +398,11 @@ class View { } $handle = $this->fopen($path, 'rb'); if ($handle) { - $chunkSize = 524288; // 512 kB chunks + $chunkSize = 524288; // 512 kiB chunks while (!feof($handle)) { echo fread($handle, $chunkSize); flush(); + $this->checkConnectionStatus(); } fclose($handle); return $this->filesize($path); @@ -423,7 +425,7 @@ class View { } $handle = $this->fopen($path, 'rb'); if ($handle) { - $chunkSize = 524288; // 512 kB chunks + $chunkSize = 524288; // 512 kiB chunks $startReading = true; if ($from !== 0 && $from !== '0' && fseek($handle, $from) !== 0) { @@ -453,6 +455,7 @@ class View { } echo fread($handle, $len); flush(); + $this->checkConnectionStatus(); } return ftell($handle) - $from; } @@ -462,6 +465,13 @@ class View { return false; } + private function checkConnectionStatus(): void { + $connectionStatus = \connection_status(); + if ($connectionStatus !== CONNECTION_NORMAL) { + throw new ConnectionLostException("Connection lost. Status: $connectionStatus"); + } + } + /** * @param string $path * @return mixed @@ -1515,7 +1525,7 @@ class View { $rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/ // if sharing was disabled for the user we remove the share permissions - if (\OCP\Util::isSharingDisabledForUser()) { + if ($sharingDisabled) { $rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE; } diff --git a/lib/private/FilesMetadata/FilesMetadataManager.php b/lib/private/FilesMetadata/FilesMetadataManager.php new file mode 100644 index 00000000000..28498af4ab0 --- /dev/null +++ b/lib/private/FilesMetadata/FilesMetadataManager.php @@ -0,0 +1,310 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata; + +use JsonException; +use OC\FilesMetadata\Job\UpdateSingleMetadata; +use OC\FilesMetadata\Listener\MetadataDelete; +use OC\FilesMetadata\Listener\MetadataUpdate; +use OC\FilesMetadata\Model\FilesMetadata; +use OC\FilesMetadata\Service\IndexRequestService; +use OC\FilesMetadata\Service\MetadataRequestService; +use OCP\BackgroundJob\IJobList; +use OCP\DB\Exception; +use OCP\DB\Exception as DBException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Files\InvalidPathException; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\FilesMetadata\Event\MetadataBackgroundEvent; +use OCP\FilesMetadata\Event\MetadataLiveEvent; +use OCP\FilesMetadata\Event\MetadataNamedEvent; +use OCP\FilesMetadata\Exceptions\FilesMetadataException; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\FilesMetadata\IMetadataQuery; +use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; +use OCP\IConfig; +use Psr\Log\LoggerInterface; + +/** + * @inheritDoc + * @since 28.0.0 + */ +class FilesMetadataManager implements IFilesMetadataManager { + public const CONFIG_KEY = 'files_metadata'; + private const JSON_MAXSIZE = 100000; + + private ?IFilesMetadata $all = null; + + public function __construct( + private IEventDispatcher $eventDispatcher, + private IJobList $jobList, + private IConfig $config, + private LoggerInterface $logger, + private MetadataRequestService $metadataRequestService, + private IndexRequestService $indexRequestService, + ) { + } + + /** + * @inheritDoc + * + * @param Node $node related node + * @param int $process type of process + * + * @return IFilesMetadata + * @throws FilesMetadataException if metadata are invalid + * @throws InvalidPathException if path to file is not valid + * @throws NotFoundException if file cannot be found + * @see self::PROCESS_BACKGROUND + * @see self::PROCESS_LIVE + * @since 28.0.0 + */ + public function refreshMetadata( + Node $node, + int $process = self::PROCESS_LIVE, + string $namedEvent = '' + ): IFilesMetadata { + try { + $metadata = $this->metadataRequestService->getMetadataFromFileId($node->getId()); + } catch (FilesMetadataNotFoundException) { + $metadata = new FilesMetadata($node->getId()); + } + + // if $process is LIVE, we enforce LIVE + // if $process is NAMED, we go NAMED + // else BACKGROUND + if ((self::PROCESS_LIVE & $process) !== 0) { + $event = new MetadataLiveEvent($node, $metadata); + } elseif ((self::PROCESS_NAMED & $process) !== 0) { + $event = new MetadataNamedEvent($node, $metadata, $namedEvent); + } else { + $event = new MetadataBackgroundEvent($node, $metadata); + } + + $this->eventDispatcher->dispatchTyped($event); + $this->saveMetadata($event->getMetadata()); + + // if requested, we add a new job for next cron to refresh metadata out of main thread + // if $process was set to LIVE+BACKGROUND, we run background process directly + if ($event instanceof MetadataLiveEvent && $event->isRunAsBackgroundJobRequested()) { + if ((self::PROCESS_BACKGROUND & $process) !== 0) { + return $this->refreshMetadata($node, self::PROCESS_BACKGROUND); + } + + $this->jobList->add(UpdateSingleMetadata::class, [$node->getOwner()->getUID(), $node->getId()]); + } + + return $metadata; + } + + /** + * @param int $fileId file id + * @param boolean $generate Generate if metadata does not exists + * + * @inheritDoc + * @return IFilesMetadata + * @throws FilesMetadataNotFoundException if not found + * @since 28.0.0 + */ + public function getMetadata(int $fileId, bool $generate = false): IFilesMetadata { + try { + return $this->metadataRequestService->getMetadataFromFileId($fileId); + } catch (FilesMetadataNotFoundException $ex) { + if ($generate) { + return new FilesMetadata($fileId); + } + + throw $ex; + } + } + + /** + * @param IFilesMetadata $filesMetadata metadata + * + * @inheritDoc + * @throws FilesMetadataException if metadata seems malformed + * @since 28.0.0 + */ + public function saveMetadata(IFilesMetadata $filesMetadata): void { + if ($filesMetadata->getFileId() === 0 || !$filesMetadata->updated()) { + return; + } + + $json = json_encode($filesMetadata->jsonSerialize()); + if (strlen($json) > self::JSON_MAXSIZE) { + throw new FilesMetadataException('json cannot exceed ' . self::JSON_MAXSIZE . ' characters long'); + } + + try { + if ($filesMetadata->getSyncToken() === '') { + $this->metadataRequestService->store($filesMetadata); + } else { + $this->metadataRequestService->updateMetadata($filesMetadata); + } + } catch (DBException $e) { + // most of the logged exception are the result of race condition + // between 2 simultaneous process trying to create/update metadata + $this->logger->warning('issue while saveMetadata', ['exception' => $e, 'metadata' => $filesMetadata]); + + return; + } + + // update indexes + foreach ($filesMetadata->getIndexes() as $index) { + try { + $this->indexRequestService->updateIndex($filesMetadata, $index); + } catch (DBException $e) { + $this->logger->warning('issue while updateIndex', ['exception' => $e]); + } + } + + // update metadata types list + $current = $this->getKnownMetadata(); + $current->import($filesMetadata->jsonSerialize(true)); + $this->config->setAppValue('core', self::CONFIG_KEY, json_encode($current)); + } + + /** + * @param int $fileId file id + * + * @inheritDoc + * @since 28.0.0 + */ + public function deleteMetadata(int $fileId): void { + try { + $this->metadataRequestService->dropMetadata($fileId); + } catch (Exception $e) { + $this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileId' => $fileId]); + } + + try { + $this->indexRequestService->dropIndex($fileId); + } catch (Exception $e) { + $this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileId' => $fileId]); + } + } + + /** + * @param IQueryBuilder $qb + * @param string $fileTableAlias alias of the table that contains data about files + * @param string $fileIdField alias of the field that contains file ids + * + * @inheritDoc + * @return IMetadataQuery|null + * @see IMetadataQuery + * @since 28.0.0 + */ + public function getMetadataQuery( + IQueryBuilder $qb, + string $fileTableAlias, + string $fileIdField + ): ?IMetadataQuery { + // we don't want to join metadata table if never filled + if ($this->config->getAppValue('core', self::CONFIG_KEY, '') === '') { + return null; + } + return new MetadataQuery($qb, $this->getKnownMetadata(), $fileTableAlias, $fileIdField); + } + + /** + * @inheritDoc + * @return IFilesMetadata + * @since 28.0.0 + */ + public function getKnownMetadata(): IFilesMetadata { + if (null !== $this->all) { + return $this->all; + } + $this->all = new FilesMetadata(); + + try { + $data = json_decode($this->config->getAppValue('core', self::CONFIG_KEY, '[]'), true, 127, JSON_THROW_ON_ERROR); + $this->all->import($data); + } catch (JsonException) { + $this->logger->warning('issue while reading stored list of metadata. Advised to run ./occ files:scan --all --generate-metadata'); + } + + return $this->all; + } + + /** + * @param string $key metadata key + * @param string $type metadata type + * @param bool $indexed TRUE if metadata can be search + * @param int $editPermission remote edit permission via Webdav PROPPATCH + * + * @inheritDoc + * @since 28.0.0 + * @see IMetadataValueWrapper::TYPE_INT + * @see IMetadataValueWrapper::TYPE_FLOAT + * @see IMetadataValueWrapper::TYPE_BOOL + * @see IMetadataValueWrapper::TYPE_ARRAY + * @see IMetadataValueWrapper::TYPE_STRING_LIST + * @see IMetadataValueWrapper::TYPE_INT_LIST + * @see IMetadataValueWrapper::TYPE_STRING + * @see IMetadataValueWrapper::EDIT_FORBIDDEN + * @see IMetadataValueWrapper::EDIT_REQ_OWNERSHIP + * @see IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION + * @see IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION + */ + public function initMetadata( + string $key, + string $type, + bool $indexed = false, + int $editPermission = IMetadataValueWrapper::EDIT_FORBIDDEN + ): void { + $current = $this->getKnownMetadata(); + try { + if ($current->getType($key) === $type + && $indexed === $current->isIndex($key) + && $editPermission === $current->getEditPermission($key)) { + return; // if key exists, with same type and indexed, we do nothing. + } + } catch (FilesMetadataNotFoundException) { + // if value does not exist, we keep on the writing of course + } + + $current->import([$key => ['type' => $type, 'indexed' => $indexed, 'editPermission' => $editPermission]]); + $this->config->setAppValue('core', self::CONFIG_KEY, json_encode($current)); + $this->all = $current; + } + + /** + * load listeners + * + * @param IEventDispatcher $eventDispatcher + */ + public static function loadListeners(IEventDispatcher $eventDispatcher): void { + $eventDispatcher->addServiceListener(NodeWrittenEvent::class, MetadataUpdate::class); + $eventDispatcher->addServiceListener(CacheEntryRemovedEvent::class, MetadataDelete::class); + } +} diff --git a/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php b/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php new file mode 100644 index 00000000000..d18c8aa3680 --- /dev/null +++ b/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Job; + +use OC\FilesMetadata\FilesMetadataManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\Files\IRootFolder; +use OCP\FilesMetadata\Event\MetadataLiveEvent; +use OCP\FilesMetadata\IFilesMetadataManager; +use Psr\Log\LoggerInterface; + +/** + * Simple background job, created when requested by an app during the + * dispatch of MetadataLiveEvent. + * This background job will re-run the event to refresh metadata on a non-live thread. + * + * @see MetadataLiveEvent::requestBackgroundJob() + * @since 28.0.0 + */ +class UpdateSingleMetadata extends QueuedJob { + public function __construct( + ITimeFactory $time, + private IRootFolder $rootFolder, + private FilesMetadataManager $filesMetadataManager, + private LoggerInterface $logger + ) { + parent::__construct($time); + } + + protected function run($argument) { + [$userId, $fileId] = $argument; + + try { + $node = $this->rootFolder->getUserFolder($userId)->getById($fileId); + if (count($node) > 0) { + $file = array_shift($node); + $this->filesMetadataManager->refreshMetadata($file, IFilesMetadataManager::PROCESS_BACKGROUND); + } + } catch (\Exception $e) { + $this->logger->warning('issue while running UpdateSingleMetadata', ['exception' => $e, 'userId' => $userId, 'fileId' => $fileId]); + } + } +} diff --git a/lib/private/FilesMetadata/Listener/MetadataDelete.php b/lib/private/FilesMetadata/Listener/MetadataDelete.php new file mode 100644 index 00000000000..d950c2cea5f --- /dev/null +++ b/lib/private/FilesMetadata/Listener/MetadataDelete.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Listener; + +use Exception; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\FilesMetadata\IFilesMetadataManager; +use Psr\Log\LoggerInterface; + +/** + * Handle file deletion event and remove stored metadata related to the deleted file + * + * @template-implements IEventListener<CacheEntryRemovedEvent> + */ +class MetadataDelete implements IEventListener { + public function __construct( + private IFilesMetadataManager $filesMetadataManager, + private LoggerInterface $logger + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof CacheEntryRemovedEvent)) { + return; + } + + try { + $nodeId = $event->getFileId(); + if ($nodeId > 0) { + $this->filesMetadataManager->deleteMetadata($nodeId); + } + } catch (Exception $e) { + $this->logger->warning('issue while running MetadataDelete', ['exception' => $e]); + } + } +} diff --git a/lib/private/FilesMetadata/Listener/MetadataUpdate.php b/lib/private/FilesMetadata/Listener/MetadataUpdate.php new file mode 100644 index 00000000000..9848f079882 --- /dev/null +++ b/lib/private/FilesMetadata/Listener/MetadataUpdate.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Listener; + +use Exception; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\NodeCreatedEvent; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\FilesMetadata\IFilesMetadataManager; +use Psr\Log\LoggerInterface; + +/** + * Handle file creation/modification events and initiate a new event related to the created/edited file. + * The generated new event is broadcast in order to obtain file related metadata from other apps. + * metadata will be stored in database. + * + * @template-implements IEventListener<NodeCreatedEvent|NodeWrittenEvent> + */ +class MetadataUpdate implements IEventListener { + public function __construct( + private IFilesMetadataManager $filesMetadataManager, + private LoggerInterface $logger + ) { + } + + /** + * @param Event $event + */ + public function handle(Event $event): void { + if (!($event instanceof NodeWrittenEvent)) { + return; + } + + try { + $this->filesMetadataManager->refreshMetadata($event->getNode()); + } catch (Exception $e) { + $this->logger->warning('issue while running MetadataUpdate', ['exception' => $e]); + } + } +} diff --git a/lib/private/FilesMetadata/MetadataQuery.php b/lib/private/FilesMetadata/MetadataQuery.php new file mode 100644 index 00000000000..9c3e63e5e22 --- /dev/null +++ b/lib/private/FilesMetadata/MetadataQuery.php @@ -0,0 +1,166 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata; + +use OC\FilesMetadata\Model\FilesMetadata; +use OC\FilesMetadata\Service\IndexRequestService; +use OC\FilesMetadata\Service\MetadataRequestService; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; +use OCP\FilesMetadata\IMetadataQuery; +use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; + +/** + * @inheritDoc + * @since 28.0.0 + */ +class MetadataQuery implements IMetadataQuery { + private array $knownJoinedIndex = []; + public function __construct( + private IQueryBuilder $queryBuilder, + private IFilesMetadata $knownMetadata, + private string $fileTableAlias = 'fc', + private string $fileIdField = 'fileid', + private string $alias = 'meta', + private string $aliasIndexPrefix = 'meta_index' + ) { + } + + /** + * @inheritDoc + * @see self::extractMetadata() + * @since 28.0.0 + */ + public function retrieveMetadata(): void { + $this->queryBuilder->selectAlias($this->alias . '.json', 'meta_json'); + $this->queryBuilder->leftJoin( + $this->fileTableAlias, MetadataRequestService::TABLE_METADATA, $this->alias, + $this->queryBuilder->expr()->eq($this->fileTableAlias . '.' . $this->fileIdField, $this->alias . '.file_id') + ); + } + + /** + * @param array $row result row + * + * @inheritDoc + * @return IFilesMetadata metadata + * @see self::retrieveMetadata() + * @since 28.0.0 + */ + public function extractMetadata(array $row): IFilesMetadata { + $fileId = (array_key_exists($this->fileIdField, $row)) ? $row[$this->fileIdField] : 0; + $metadata = new FilesMetadata((int)$fileId); + try { + $metadata->importFromDatabase($row, $this->alias . '_'); + } catch (FilesMetadataNotFoundException) { + // can be ignored as files' metadata are optional and might not exist in database + } + + return $metadata; + } + + /** + * @param string $metadataKey metadata key + * @param bool $enforce limit the request only to existing metadata + * + * @inheritDoc + * @since 28.0.0 + */ + public function joinIndex(string $metadataKey, bool $enforce = false): string { + if (array_key_exists($metadataKey, $this->knownJoinedIndex)) { + return $this->knownJoinedIndex[$metadataKey]; + } + + $aliasIndex = $this->aliasIndexPrefix . '_' . count($this->knownJoinedIndex); + $this->knownJoinedIndex[$metadataKey] = $aliasIndex; + + $expr = $this->queryBuilder->expr(); + $andX = $expr->andX($expr->eq($aliasIndex . '.file_id', $this->fileTableAlias . '.' . $this->fileIdField)); + $andX->add($expr->eq($this->getMetadataKeyField($metadataKey), $this->queryBuilder->createNamedParameter($metadataKey))); + + if ($enforce) { + $this->queryBuilder->innerJoin( + $this->fileTableAlias, + IndexRequestService::TABLE_METADATA_INDEX, + $aliasIndex, + $andX + ); + } else { + $this->queryBuilder->leftJoin( + $this->fileTableAlias, + IndexRequestService::TABLE_METADATA_INDEX, + $aliasIndex, + $andX + ); + } + + return $aliasIndex; + } + + /** + * @throws FilesMetadataNotFoundException + */ + private function joinedTableAlias(string $metadataKey): string { + if (!array_key_exists($metadataKey, $this->knownJoinedIndex)) { + throw new FilesMetadataNotFoundException('table related to ' . $metadataKey . ' not initiated, you need to use leftJoin() first.'); + } + + return $this->knownJoinedIndex[$metadataKey]; + } + + /** + * @inheritDoc + * + * @param string $metadataKey metadata key + * + * @return string table field + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function getMetadataKeyField(string $metadataKey): string { + return $this->joinedTableAlias($metadataKey) . '.meta_key'; + } + + /** + * @inheritDoc + * + * @param string $metadataKey metadata key + * + * @return string table field + * @throws FilesMetadataNotFoundException if metadataKey is not known + * @throws FilesMetadataTypeException is metadataKey is not set as indexed + * @since 28.0.0 + */ + public function getMetadataValueField(string $metadataKey): string { + return match ($this->knownMetadata->getType($metadataKey)) { + IMetadataValueWrapper::TYPE_STRING => $this->joinedTableAlias($metadataKey) . '.meta_value_string', + IMetadataValueWrapper::TYPE_INT, IMetadataValueWrapper::TYPE_BOOL => $this->joinedTableAlias($metadataKey) . '.meta_value_int', + default => throw new FilesMetadataTypeException('metadata is not set as indexed'), + }; + } +} diff --git a/lib/private/FilesMetadata/Model/FilesMetadata.php b/lib/private/FilesMetadata/Model/FilesMetadata.php new file mode 100644 index 00000000000..629b537dabe --- /dev/null +++ b/lib/private/FilesMetadata/Model/FilesMetadata.php @@ -0,0 +1,621 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Model; + +use JsonException; +use OCP\FilesMetadata\Exceptions\FilesMetadataKeyFormatException; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; +use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; + +/** + * Model that represent metadata linked to a specific file. + * + * @inheritDoc + * @since 28.0.0 + */ +class FilesMetadata implements IFilesMetadata { + /** @var array<string, MetadataValueWrapper> */ + private array $metadata = []; + private bool $updated = false; + private int $lastUpdate = 0; + private string $syncToken = ''; + + public function __construct( + private int $fileId = 0 + ) { + } + + /** + * @inheritDoc + * @return int related file id + * @since 28.0.0 + */ + public function getFileId(): int { + return $this->fileId; + } + + /** + * @inheritDoc + * @return int timestamp + * @since 28.0.0 + */ + public function lastUpdateTimestamp(): int { + return $this->lastUpdate; + } + + /** + * @inheritDoc + * @return string token + * @since 28.0.0 + */ + public function getSyncToken(): string { + return $this->syncToken; + } + + /** + * @inheritDoc + * @return string[] list of keys + * @since 28.0.0 + */ + public function getKeys(): array { + return array_keys($this->metadata); + } + + /** + * @param string $needle metadata key to search + * + * @inheritDoc + * @return bool TRUE if key exist + * @since 28.0.0 + */ + public function hasKey(string $needle): bool { + return (in_array($needle, $this->getKeys())); + } + + /** + * @inheritDoc + * @return string[] list of indexes + * @since 28.0.0 + */ + public function getIndexes(): array { + $indexes = []; + foreach ($this->getKeys() as $key) { + if ($this->metadata[$key]->isIndexed()) { + $indexes[] = $key; + } + } + + return $indexes; + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return bool TRUE if key exists and is set as indexed + * @since 28.0.0 + */ + public function isIndex(string $key): bool { + return $this->metadata[$key]?->isIndexed() ?? false; + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return int edit permission + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function getEditPermission(string $key): int { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getEditPermission(); + } + + /** + * @param string $key metadata key + * @param int $permission edit permission + * + * @inheritDoc + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function setEditPermission(string $key, int $permission): void { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + $this->metadata[$key]->setEditPermission($permission); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return string metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getString(string $key): string { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueString(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return int metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getInt(string $key): int { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueInt(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return float metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getFloat(string $key): float { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueFloat(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return bool metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getBool(string $key): bool { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueBool(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return array metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getArray(string $key): array { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueArray(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return string[] metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getStringList(string $key): array { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueStringList(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return int[] metadata value + * @throws FilesMetadataNotFoundException + * @throws FilesMetadataTypeException + * @since 28.0.0 + */ + public function getIntList(string $key): array { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getValueIntList(); + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return string value type + * @throws FilesMetadataNotFoundException + * @see IMetadataValueWrapper::TYPE_STRING + * @see IMetadataValueWrapper::TYPE_INT + * @see IMetadataValueWrapper::TYPE_FLOAT + * @see IMetadataValueWrapper::TYPE_BOOL + * @see IMetadataValueWrapper::TYPE_ARRAY + * @see IMetadataValueWrapper::TYPE_STRING_LIST + * @see IMetadataValueWrapper::TYPE_INT_LIST + * @since 28.0.0 + */ + public function getType(string $key): string { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getType(); + } + + /** + * @param string $key metadata key + * @param string $value metadata value + * @param bool $index set TRUE if value must be indexed + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setString(string $key, string $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getString($key) === $value && $index === $this->isIndex($key)) { + return $this; // we ignore if value and index have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING); + $this->updated = true; + $this->metadata[$key] = $meta->setValueString($value)->setIndexed($index); + + return $this; + } + + /** + * @param string $key metadata key + * @param int $value metadata value + * @param bool $index set TRUE if value must be indexed + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setInt(string $key, int $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getInt($key) === $value && $index === $this->isIndex($key)) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_INT); + $this->metadata[$key] = $meta->setValueInt($value)->setIndexed($index); + $this->updated = true; + + return $this; + } + + /** + * @param string $key metadata key + * @param float $value metadata value + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setFloat(string $key, float $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getFloat($key) === $value && $index === $this->isIndex($key)) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_FLOAT); + $this->metadata[$key] = $meta->setValueFloat($value)->setIndexed($index); + $this->updated = true; + + return $this; + } + + + /** + * @param string $key metadata key + * @param bool $value metadata value + * @param bool $index set TRUE if value must be indexed + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setBool(string $key, bool $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getBool($key) === $value && $index === $this->isIndex($key)) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_BOOL); + $this->metadata[$key] = $meta->setValueBool($value)->setIndexed($index); + $this->updated = true; + + return $this; + } + + + /** + * @param string $key metadata key + * @param array $value metadata value + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setArray(string $key, array $value): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getArray($key) === $value) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_ARRAY); + $this->metadata[$key] = $meta->setValueArray($value); + $this->updated = true; + + return $this; + } + + /** + * @param string $key metadata key + * @param string[] $value metadata value + * @param bool $index set TRUE if each values from the list must be indexed + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setStringList(string $key, array $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getStringList($key) === $value) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST); + $this->metadata[$key] = $meta->setValueStringList($value)->setIndexed($index); + $this->updated = true; + + return $this; + } + + /** + * @param string $key metadata key + * @param int[] $value metadata value + * @param bool $index set TRUE if each values from the list must be indexed + * + * @inheritDoc + * @return self + * @throws FilesMetadataKeyFormatException + * @since 28.0.0 + */ + public function setIntList(string $key, array $value, bool $index = false): IFilesMetadata { + $this->confirmKeyFormat($key); + try { + if ($this->getIntList($key) === $value) { + return $this; // we ignore if value have not changed + } + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) { + // if value does not exist, or type has changed, we keep on the writing + } + + $valueWrapper = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST); + $this->metadata[$key] = $valueWrapper->setValueIntList($value)->setIndexed($index); + $this->updated = true; + + return $this; + } + + /** + * @param string $key metadata key + * + * @inheritDoc + * @return self + * @since 28.0.0 + */ + public function unset(string $key): IFilesMetadata { + if (!array_key_exists($key, $this->metadata)) { + return $this; + } + + unset($this->metadata[$key]); + $this->updated = true; + + return $this; + } + + /** + * @param string $keyPrefix metadata key prefix + * + * @inheritDoc + * @return self + * @since 28.0.0 + */ + public function removeStartsWith(string $keyPrefix): IFilesMetadata { + if ($keyPrefix === '') { + return $this; + } + + foreach ($this->getKeys() as $key) { + if (str_starts_with($key, $keyPrefix)) { + $this->unset($key); + } + } + + return $this; + } + + /** + * @param string $key + * + * @return void + * @throws FilesMetadataKeyFormatException + */ + private function confirmKeyFormat(string $key): void { + $acceptedChars = ['-', '_']; + if (ctype_alnum(str_replace($acceptedChars, '', $key))) { + return; + } + + throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-, _)'); + } + + /** + * @inheritDoc + * @return bool TRUE if metadata have been modified + * @since 28.0.0 + */ + public function updated(): bool { + return $this->updated; + } + + public function jsonSerialize(bool $emptyValues = false): array { + $data = []; + foreach ($this->metadata as $metaKey => $metaValueWrapper) { + $data[$metaKey] = $metaValueWrapper->jsonSerialize($emptyValues); + } + + return $data; + } + + /** + * @return array<string, string|int|bool|float|string[]|int[]> + */ + public function asArray(): array { + $data = []; + foreach ($this->metadata as $metaKey => $metaValueWrapper) { + try { + $data[$metaKey] = $metaValueWrapper->getValueAny(); + } catch (FilesMetadataNotFoundException $e) { + // ignore exception + } + } + + return $data; + } + + /** + * @param array $data + * + * @inheritDoc + * @return IFilesMetadata + * @since 28.0.0 + */ + public function import(array $data): IFilesMetadata { + foreach ($data as $k => $v) { + $valueWrapper = new MetadataValueWrapper(); + $this->metadata[$k] = $valueWrapper->import($v); + } + $this->updated = false; + + return $this; + } + + /** + * import data from database to configure this model + * + * @param array $data + * @param string $prefix + * + * @return IFilesMetadata + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function importFromDatabase(array $data, string $prefix = ''): IFilesMetadata { + try { + $this->syncToken = $data[$prefix . 'sync_token'] ?? ''; + + return $this->import( + json_decode( + $data[$prefix . 'json'] ?? '[]', + true, + 512, + JSON_THROW_ON_ERROR + ) + ); + } catch (JsonException) { + throw new FilesMetadataNotFoundException(); + } + } +} diff --git a/lib/private/FilesMetadata/Model/MetadataValueWrapper.php b/lib/private/FilesMetadata/Model/MetadataValueWrapper.php new file mode 100644 index 00000000000..90f1554180d --- /dev/null +++ b/lib/private/FilesMetadata/Model/MetadataValueWrapper.php @@ -0,0 +1,421 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Model; + +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; + +/** + * @inheritDoc + * @see IFilesMetadata + * @since 28.0.0 + */ +class MetadataValueWrapper implements IMetadataValueWrapper { + private string $type; + /** @var string|int|float|bool|array|string[]|int[] */ + private mixed $value = null; + private bool $indexed = false; + private int $editPermission = self::EDIT_FORBIDDEN; + + /** + * @param string $type value type + * + * @inheritDoc + * @see self::TYPE_INT + * @see self::TYPE_FLOAT + * @see self::TYPE_BOOL + * @see self::TYPE_ARRAY + * @see self::TYPE_STRING_LIST + * @see self::TYPE_INT_LIST + * @see self::TYPE_STRING + * @since 28.0.0 + */ + public function __construct(string $type = '') { + $this->type = $type; + } + + /** + * @inheritDoc + * @return string value type + * @see self::TYPE_INT + * @see self::TYPE_FLOAT + * @see self::TYPE_BOOL + * @see self::TYPE_ARRAY + * @see self::TYPE_STRING_LIST + * @see self::TYPE_INT_LIST + * @see self::TYPE_STRING + * @since 28.0.0 + */ + public function getType(): string { + return $this->type; + } + + /** + * @param string $type value type + * + * @inheritDoc + * @return bool + * @see self::TYPE_INT + * @see self::TYPE_FLOAT + * @see self::TYPE_BOOL + * @see self::TYPE_ARRAY + * @see self::TYPE_STRING_LIST + * @see self::TYPE_INT_LIST + * @see self::TYPE_STRING + * @since 28.0.0 + */ + public function isType(string $type): bool { + return (strtolower($type) === strtolower($this->type)); + } + + /** + * @param string $type value type + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if type cannot be confirmed + * @see self::TYPE_INT + * @see self::TYPE_BOOL + * @see self::TYPE_ARRAY + * @see self::TYPE_STRING_LIST + * @see self::TYPE_INT_LIST + * @see self::TYPE_STRING + * @see self::TYPE_FLOAT + * @since 28.0.0 + */ + public function assertType(string $type): self { + if (!$this->isType($type)) { + throw new FilesMetadataTypeException('type is \'' . $this->getType() . '\', expecting \'' . $type . '\''); + } + + return $this; + } + + /** + * @param string $value string to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store a string + * @since 28.0.0 + */ + public function setValueString(string $value): self { + $this->assertType(self::TYPE_STRING); + $this->value = $value; + + return $this; + } + + /** + * @param int $value int to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store an int + * @since 28.0.0 + */ + public function setValueInt(int $value): self { + $this->assertType(self::TYPE_INT); + $this->value = $value; + + return $this; + } + + /** + * @param float $value float to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store a float + * @since 28.0.0 + */ + public function setValueFloat(float $value): self { + $this->assertType(self::TYPE_FLOAT); + $this->value = $value; + + return $this; + } + + /** + * @param bool $value bool to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store a bool + * @since 28.0.0 + */ + public function setValueBool(bool $value): self { + $this->assertType(self::TYPE_BOOL); + $this->value = $value; + + + return $this; + } + + /** + * @param array $value array to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store an array + * @since 28.0.0 + */ + public function setValueArray(array $value): self { + $this->assertType(self::TYPE_ARRAY); + $this->value = $value; + + return $this; + } + + /** + * @param string[] $value string list to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store a string list + * @since 28.0.0 + */ + public function setValueStringList(array $value): self { + $this->assertType(self::TYPE_STRING_LIST); + // TODO confirm value is an array or string ? + $this->value = $value; + + return $this; + } + + /** + * @param int[] $value int list to be set as value + * + * @inheritDoc + * @return self + * @throws FilesMetadataTypeException if wrapper was not set to store an int list + * @since 28.0.0 + */ + public function setValueIntList(array $value): self { + $this->assertType(self::TYPE_INT_LIST); + // TODO confirm value is an array of int ? + $this->value = $value; + + return $this; + } + + + /** + * @inheritDoc + * @return string set value + * @throws FilesMetadataTypeException if wrapper was not set to store a string + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueString(): string { + $this->assertType(self::TYPE_STRING); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (string)$this->value; + } + + /** + * @inheritDoc + * @return int set value + * @throws FilesMetadataTypeException if wrapper was not set to store an int + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueInt(): int { + $this->assertType(self::TYPE_INT); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (int)$this->value; + } + + /** + * @inheritDoc + * @return float set value + * @throws FilesMetadataTypeException if wrapper was not set to store a float + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueFloat(): float { + $this->assertType(self::TYPE_FLOAT); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (float)$this->value; + } + + /** + * @inheritDoc + * @return bool set value + * @throws FilesMetadataTypeException if wrapper was not set to store a bool + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueBool(): bool { + $this->assertType(self::TYPE_BOOL); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (bool)$this->value; + } + + /** + * @inheritDoc + * @return array set value + * @throws FilesMetadataTypeException if wrapper was not set to store an array + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueArray(): array { + $this->assertType(self::TYPE_ARRAY); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (array)$this->value; + } + + /** + * @inheritDoc + * @return string[] set value + * @throws FilesMetadataTypeException if wrapper was not set to store a string list + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueStringList(): array { + $this->assertType(self::TYPE_STRING_LIST); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (array)$this->value; + } + + /** + * @inheritDoc + * @return int[] set value + * @throws FilesMetadataTypeException if wrapper was not set to store an int list + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueIntList(): array { + $this->assertType(self::TYPE_INT_LIST); + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return (array)$this->value; + } + + /** + * @inheritDoc + * @return string|int|float|bool|array|string[]|int[] set value + * @throws FilesMetadataNotFoundException if value is not set + * @since 28.0.0 + */ + public function getValueAny(): mixed { + if (null === $this->value) { + throw new FilesMetadataNotFoundException('value is not set'); + } + + return $this->value; + } + + /** + * @param bool $indexed TRUE to set the stored value as an indexed value + * + * @inheritDoc + * @return self + * @since 28.0.0 + */ + public function setIndexed(bool $indexed): self { + $this->indexed = $indexed; + + return $this; + } + + /** + * @inheritDoc + * @return bool TRUE if value is an indexed value + * @since 28.0.0 + */ + public function isIndexed(): bool { + return $this->indexed; + } + + /** + * @param int $permission edit permission + * + * @inheritDoc + * @return self + * @since 28.0.0 + */ + public function setEditPermission(int $permission): self { + $this->editPermission = $permission; + + return $this; + } + + /** + * @inheritDoc + * @return int edit permission + * @since 28.0.0 + */ + public function getEditPermission(): int { + return $this->editPermission; + } + + /** + * @param array $data serialized version of the object + * + * @inheritDoc + * @return self + * @see jsonSerialize + * @since 28.0.0 + */ + public function import(array $data): self { + $this->value = $data['value'] ?? null; + $this->type = $data['type'] ?? ''; + $this->setIndexed($data['indexed'] ?? false); + $this->setEditPermission($data['editPermission'] ?? self::EDIT_FORBIDDEN); + return $this; + } + + public function jsonSerialize(bool $emptyValues = false): array { + return [ + 'value' => ($emptyValues) ? null : $this->value, + 'type' => $this->getType(), + 'indexed' => $this->isIndexed(), + 'editPermission' => $this->getEditPermission() + ]; + } +} diff --git a/lib/private/FilesMetadata/Service/IndexRequestService.php b/lib/private/FilesMetadata/Service/IndexRequestService.php new file mode 100644 index 00000000000..2a23e2c9a67 --- /dev/null +++ b/lib/private/FilesMetadata/Service/IndexRequestService.php @@ -0,0 +1,195 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Service; + +use OCP\DB\Exception as DbException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; +use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +/** + * manage sql request to the metadata_index table + */ +class IndexRequestService { + public const TABLE_METADATA_INDEX = 'files_metadata_index'; + + public function __construct( + private IDBConnection $dbConnection, + private LoggerInterface $logger + ) { + } + + /** + * update the index for a specific metadata key + * + * @param IFilesMetadata $filesMetadata metadata + * @param string $key metadata key to update + * + * @throws DbException + */ + public function updateIndex(IFilesMetadata $filesMetadata, string $key): void { + $fileId = $filesMetadata->getFileId(); + try { + $metadataType = $filesMetadata->getType($key); + } catch (FilesMetadataNotFoundException $e) { + return; + } + + /** + * might look harsh, but a lot simpler than comparing current indexed data, as we can expect + * conflict with a change of types. + * We assume that each time one random metadata were modified we can drop all index for this + * key and recreate them. + * To make it slightly cleaner, we'll use transaction + */ + $this->dbConnection->beginTransaction(); + try { + $this->dropIndex($fileId, $key); + match ($metadataType) { + IMetadataValueWrapper::TYPE_STRING => $this->insertIndexString($fileId, $key, $filesMetadata->getString($key)), + IMetadataValueWrapper::TYPE_INT => $this->insertIndexInt($fileId, $key, $filesMetadata->getInt($key)), + IMetadataValueWrapper::TYPE_BOOL => $this->insertIndexBool($fileId, $key, $filesMetadata->getBool($key)), + IMetadataValueWrapper::TYPE_STRING_LIST => $this->insertIndexStringList($fileId, $key, $filesMetadata->getStringList($key)), + IMetadataValueWrapper::TYPE_INT_LIST => $this->insertIndexIntList($fileId, $key, $filesMetadata->getIntList($key)) + }; + } catch (FilesMetadataNotFoundException|FilesMetadataTypeException|DbException $e) { + $this->dbConnection->rollBack(); + $this->logger->warning('issue while updateIndex', ['exception' => $e, 'fileId' => $fileId, 'key' => $key]); + } + + $this->dbConnection->commit(); + } + + /** + * insert a new entry in the metadata_index table for a string value + * + * @param int $fileId file id + * @param string $key metadata key + * @param string $value metadata value + * + * @throws DbException + */ + private function insertIndexString(int $fileId, string $key, string $value): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert(self::TABLE_METADATA_INDEX) + ->setValue('meta_key', $qb->createNamedParameter($key)) + ->setValue('meta_value_string', $qb->createNamedParameter($value)) + ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); + $qb->executeStatement(); + } + + /** + * insert a new entry in the metadata_index table for an int value + * + * @param int $fileId file id + * @param string $key metadata key + * @param int $value metadata value + * + * @throws DbException + */ + public function insertIndexInt(int $fileId, string $key, int $value): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert(self::TABLE_METADATA_INDEX) + ->setValue('meta_key', $qb->createNamedParameter($key)) + ->setValue('meta_value_int', $qb->createNamedParameter($value, IQueryBuilder::PARAM_INT)) + ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); + $qb->executeStatement(); + } + + /** + * insert a new entry in the metadata_index table for a bool value + * + * @param int $fileId file id + * @param string $key metadata key + * @param bool $value metadata value + * + * @throws DbException + */ + public function insertIndexBool(int $fileId, string $key, bool $value): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert(self::TABLE_METADATA_INDEX) + ->setValue('meta_key', $qb->createNamedParameter($key)) + ->setValue('meta_value_int', $qb->createNamedParameter(($value) ? '1' : '0', IQueryBuilder::PARAM_INT)) + ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); + $qb->executeStatement(); + } + + /** + * insert entries in the metadata_index table for list of string + * + * @param int $fileId file id + * @param string $key metadata key + * @param string[] $values metadata values + * + * @throws DbException + */ + public function insertIndexStringList(int $fileId, string $key, array $values): void { + foreach ($values as $value) { + $this->insertIndexString($fileId, $key, $value); + } + } + + /** + * insert entries in the metadata_index table for list of int + * + * @param int $fileId file id + * @param string $key metadata key + * @param int[] $values metadata values + * + * @throws DbException + */ + public function insertIndexIntList(int $fileId, string $key, array $values): void { + foreach ($values as $value) { + $this->insertIndexInt($fileId, $key, $value); + } + } + + /** + * drop indexes related to a file id + * if a key is specified, only drop entries related to it + * + * @param int $fileId file id + * @param string $key metadata key + * + * @throws DbException + */ + public function dropIndex(int $fileId, string $key = ''): void { + $qb = $this->dbConnection->getQueryBuilder(); + $expr = $qb->expr(); + $qb->delete(self::TABLE_METADATA_INDEX) + ->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + + if ($key !== '') { + $qb->andWhere($expr->eq('meta_key', $qb->createNamedParameter($key))); + } + + $qb->executeStatement(); + } +} diff --git a/lib/private/FilesMetadata/Service/MetadataRequestService.php b/lib/private/FilesMetadata/Service/MetadataRequestService.php new file mode 100644 index 00000000000..85874e92d4a --- /dev/null +++ b/lib/private/FilesMetadata/Service/MetadataRequestService.php @@ -0,0 +1,160 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\FilesMetadata\Service; + +use OC\FilesMetadata\Model\FilesMetadata; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +/** + * manage sql request to the metadata table + */ +class MetadataRequestService { + public const TABLE_METADATA = 'files_metadata'; + + public function __construct( + private IDBConnection $dbConnection, + private LoggerInterface $logger + ) { + } + + /** + * store metadata into database + * + * @param IFilesMetadata $filesMetadata + * + * @throws Exception + */ + public function store(IFilesMetadata $filesMetadata): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert(self::TABLE_METADATA) + ->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)) + ->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize()))) + ->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken())) + ->setValue('last_update', (string) $qb->createFunction('NOW()')); + $qb->executeStatement(); + } + + /** + * returns metadata for a file id + * + * @param int $fileId file id + * + * @return IFilesMetadata + * @throws FilesMetadataNotFoundException if no metadata are found in database + */ + public function getMetadataFromFileId(int $fileId): IFilesMetadata { + try { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('json', 'sync_token')->from(self::TABLE_METADATA); + $qb->where( + $qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)) + ); + $result = $qb->executeQuery(); + $data = $result->fetch(); + $result->closeCursor(); + } catch (Exception $e) { + $this->logger->warning( + 'exception while getMetadataFromDatabase()', ['exception' => $e, 'fileId' => $fileId] + ); + throw new FilesMetadataNotFoundException(); + } + + if ($data === false) { + throw new FilesMetadataNotFoundException(); + } + + $metadata = new FilesMetadata($fileId); + $metadata->importFromDatabase($data); + + return $metadata; + } + + /** + * drop metadata related to a file id + * + * @param int $fileId file id + * + * @return void + * @throws Exception + */ + public function dropMetadata(int $fileId): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete(self::TABLE_METADATA) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + $qb->executeStatement(); + } + + /** + * update metadata in the database + * + * @param IFilesMetadata $filesMetadata metadata + * + * @return int number of affected rows + * @throws Exception + */ + public function updateMetadata(IFilesMetadata $filesMetadata): int { + $qb = $this->dbConnection->getQueryBuilder(); + $expr = $qb->expr(); + + $qb->update(self::TABLE_METADATA) + ->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize()))) + ->set('sync_token', $qb->createNamedParameter($this->generateSyncToken())) + ->set('last_update', $qb->createFunction('NOW()')) + ->where( + $expr->andX( + $expr->eq('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)), + $expr->eq('sync_token', $qb->createNamedParameter($filesMetadata->getSyncToken())) + ) + ); + + return $qb->executeStatement(); + } + + /** + * generate a random token + * @return string + */ + private function generateSyncToken(): string { + $chars = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890'; + + $str = ''; + $max = strlen($chars); + for ($i = 0; $i < 7; $i++) { + try { + $str .= $chars[random_int(0, $max - 2)]; + } catch (\Exception $e) { + $this->logger->warning('exception during generateSyncToken', ['exception' => $e]); + } + } + + return $str; + } +} diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php index ef5641d8137..55792ce1dff 100644 --- a/lib/private/Group/Database.php +++ b/lib/private/Group/Database.php @@ -33,6 +33,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Group\Backend\ABackend; use OCP\Group\Backend\IAddToGroupBackend; +use OCP\Group\Backend\IBatchMethodsBackend; use OCP\Group\Backend\ICountDisabledInGroup; use OCP\Group\Backend\ICountUsersBackend; use OCP\Group\Backend\ICreateGroupBackend; @@ -61,12 +62,11 @@ class Database extends ABackend implements IRemoveFromGroupBackend, ISetDisplayNameBackend, ISearchableGroupBackend, + IBatchMethodsBackend, INamedBackend { - /** @var string[] */ + /** @var array<string, array{gid: string, displayname: string}> */ private $groupCache = []; - - /** @var IDBConnection */ - private $dbConn; + private ?IDBConnection $dbConn; /** * \OC\Group\Database constructor. @@ -270,7 +270,7 @@ class Database extends ABackend implements $this->fixDI(); $query = $this->dbConn->getQueryBuilder(); - $query->select('gid') + $query->select('gid', 'displayname') ->from('groups') ->orderBy('gid', 'ASC'); @@ -293,6 +293,10 @@ class Database extends ABackend implements $groups = []; while ($row = $result->fetch()) { + $this->groupCache[$row['gid']] = [ + 'displayname' => $row['displayname'], + 'gid' => $row['gid'], + ]; $groups[] = $row['gid']; } $result->closeCursor(); @@ -332,6 +336,43 @@ class Database extends ABackend implements } /** + * {@inheritdoc} + */ + public function groupsExists(array $gids): array { + $notFoundGids = []; + $existingGroups = []; + + // In case the data is already locally accessible, not need to do SQL query + // or do a SQL query but with a smaller in clause + foreach ($gids as $gid) { + if (isset($this->groupCache[$gid])) { + $existingGroups[] = $gid; + } else { + $notFoundGids[] = $gid; + } + } + + $qb = $this->dbConn->getQueryBuilder(); + $qb->select('gid', 'displayname') + ->from('groups') + ->where($qb->expr()->in('gid', $qb->createParameter('ids'))); + foreach (array_chunk($notFoundGids, 1000) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY); + $result = $qb->executeQuery(); + while ($row = $result->fetch()) { + $this->groupCache[(string)$row['gid']] = [ + 'displayname' => (string)$row['displayname'], + 'gid' => (string)$row['gid'], + ]; + $existingGroups[] = (string)$row['gid']; + } + $result->closeCursor(); + } + + return $existingGroups; + } + + /** * Get a list of all users in a group * @param string $gid * @param string $search @@ -488,6 +529,43 @@ class Database extends ABackend implements return []; } + /** + * {@inheritdoc} + */ + public function getGroupsDetails(array $gids): array { + $notFoundGids = []; + $details = []; + + // In case the data is already locally accessible, not need to do SQL query + // or do a SQL query but with a smaller in clause + foreach ($gids as $gid) { + if (isset($this->groupCache[$gid])) { + $details[$gid] = ['displayName' => $this->groupCache[$gid]['displayname']]; + } else { + $notFoundGids[] = $gid; + } + } + + foreach (array_chunk($notFoundGids, 1000) as $chunk) { + $query = $this->dbConn->getQueryBuilder(); + $query->select('gid', 'displayname') + ->from('groups') + ->where($query->expr()->in('gid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))); + + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + $details[(string)$row['gid']] = ['displayName' => (string)$row['displayname']]; + $this->groupCache[(string)$row['gid']] = [ + 'displayname' => (string)$row['displayname'], + 'gid' => (string)$row['gid'], + ]; + } + $result->closeCursor(); + } + + return $details; + } + public function setDisplayName(string $gid, string $displayName): bool { if (!$this->groupExists($gid)) { return false; diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 441ee64604d..97521d54ba6 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -85,11 +85,11 @@ class Group implements IGroup { $this->displayName = $displayName; } - public function getGID() { + public function getGID(): string { return $this->gid; } - public function getDisplayName() { + public function getDisplayName(): string { if (is_null($this->displayName)) { foreach ($this->backends as $backend) { if ($backend instanceof IGetDisplayNameBackend) { @@ -126,7 +126,7 @@ class Group implements IGroup { * * @return \OC\User\User[] */ - public function getUsers() { + public function getUsers(): array { if ($this->usersLoaded) { return $this->users; } @@ -153,7 +153,7 @@ class Group implements IGroup { * @param IUser $user * @return bool */ - public function inGroup(IUser $user) { + public function inGroup(IUser $user): bool { if (isset($this->users[$user->getUID()])) { return true; } @@ -171,7 +171,7 @@ class Group implements IGroup { * * @param IUser $user */ - public function addUser(IUser $user) { + public function addUser(IUser $user): void { if ($this->inGroup($user)) { return; } @@ -200,10 +200,8 @@ class Group implements IGroup { /** * remove a user from the group - * - * @param \OC\User\User $user */ - public function removeUser($user) { + public function removeUser(IUser $user): void { $result = false; $this->dispatcher->dispatchTyped(new BeforeUserRemovedEvent($this, $user)); if ($this->emitter) { @@ -262,7 +260,7 @@ class Group implements IGroup { * @param string $search * @return int|bool */ - public function count($search = '') { + public function count($search = ''): int|bool { $users = false; foreach ($this->backends as $backend) { if ($backend->implementsActions(\OC\Group\Backend::COUNT_USERS)) { @@ -282,7 +280,7 @@ class Group implements IGroup { * * @return int|bool */ - public function countDisabled() { + public function countDisabled(): int|bool { $users = false; foreach ($this->backends as $backend) { if ($backend instanceof ICountDisabledInGroup) { @@ -306,7 +304,7 @@ class Group implements IGroup { * @return IUser[] * @deprecated 27.0.0 Use searchUsers instead (same implementation) */ - public function searchDisplayName($search, $limit = null, $offset = null) { + public function searchDisplayName(string $search, int $limit = null, int $offset = null): array { return $this->searchUsers($search, $limit, $offset); } @@ -315,7 +313,7 @@ class Group implements IGroup { * * @return string[] */ - public function getBackendNames() { + public function getBackendNames(): array { $backends = []; foreach ($this->backends as $backend) { if ($backend instanceof INamedBackend) { @@ -329,11 +327,11 @@ class Group implements IGroup { } /** - * delete the group + * Delete the group * * @return bool */ - public function delete() { + public function delete(): bool { // Prevent users from deleting group admin if ($this->getGID() === 'admin') { return false; @@ -378,7 +376,7 @@ class Group implements IGroup { * @return bool * @since 14.0.0 */ - public function canRemoveUser() { + public function canRemoveUser(): bool { foreach ($this->backends as $backend) { if ($backend->implementsActions(GroupInterface::REMOVE_FROM_GOUP)) { return true; @@ -391,7 +389,7 @@ class Group implements IGroup { * @return bool * @since 14.0.0 */ - public function canAddUser() { + public function canAddUser(): bool { foreach ($this->backends as $backend) { if ($backend->implementsActions(GroupInterface::ADD_TO_GROUP)) { return true; diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php index c43b5165a79..47475121ea0 100644 --- a/lib/private/Group/Manager.php +++ b/lib/private/Group/Manager.php @@ -21,6 +21,7 @@ * @author Vincent Petry <vincent@nextcloud.com> * @author Vinicius Cubas Brand <vinicius@eita.org.br> * @author voxsim "Simon Vocella" + * @author Carl Schwan <carl@carlschwan.eu> * * @license AGPL-3.0 * @@ -41,6 +42,8 @@ namespace OC\Group; use OC\Hooks\PublicEmitter; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\Backend\IBatchMethodsBackend; +use OCP\Group\Backend\IGroupDetailsBackend; use OCP\Group\Events\BeforeGroupCreatedEvent; use OCP\Group\Events\GroupCreatedEvent; use OCP\GroupInterface; @@ -74,10 +77,10 @@ class Manager extends PublicEmitter implements IGroupManager { private IEventDispatcher $dispatcher; private LoggerInterface $logger; - /** @var \OC\Group\Group[] */ + /** @var array<string, IGroup> */ private $cachedGroups = []; - /** @var (string[])[] */ + /** @var array<string, list<string>> */ private $cachedUserGroups = []; /** @var \OC\SubAdmin */ @@ -185,7 +188,7 @@ class Manager extends PublicEmitter implements IGroupManager { if ($backend->implementsActions(Backend::GROUP_DETAILS)) { $groupData = $backend->getGroupDetails($gid); if (is_array($groupData) && !empty($groupData)) { - // take the display name from the first backend that has a non-null one + // take the display name from the last backend that has a non-null one if (is_null($displayName) && isset($groupData['displayName'])) { $displayName = $groupData['displayName']; } @@ -198,11 +201,69 @@ class Manager extends PublicEmitter implements IGroupManager { if (count($backends) === 0) { return null; } + /** @var GroupInterface[] $backends */ $this->cachedGroups[$gid] = new Group($gid, $backends, $this->dispatcher, $this->userManager, $this, $displayName); return $this->cachedGroups[$gid]; } /** + * @brief Batch method to create group objects + * + * @param list<string> $gids List of groupIds for which we want to create a IGroup object + * @param array<string, string> $displayNames Array containing already know display name for a groupId + * @return array<string, IGroup> + */ + protected function getGroupsObjects(array $gids, array $displayNames = []): array { + $backends = []; + $groups = []; + foreach ($gids as $gid) { + $backends[$gid] = []; + if (!isset($displayNames[$gid])) { + $displayNames[$gid] = null; + } + } + foreach ($this->backends as $backend) { + if ($backend instanceof IGroupDetailsBackend || $backend->implementsActions(GroupInterface::GROUP_DETAILS)) { + /** @var IGroupDetailsBackend $backend */ + if ($backend instanceof IBatchMethodsBackend) { + $groupDatas = $backend->getGroupsDetails($gids); + } else { + $groupDatas = []; + foreach ($gids as $gid) { + $groupDatas[$gid] = $backend->getGroupDetails($gid); + } + } + foreach ($groupDatas as $gid => $groupData) { + if (!empty($groupData)) { + // take the display name from the last backend that has a non-null one + if (isset($groupData['displayName'])) { + $displayNames[$gid] = $groupData['displayName']; + } + $backends[$gid][] = $backend; + } + } + } else { + if ($backend instanceof IBatchMethodsBackend) { + $existingGroups = $backend->groupsExists($gids); + } else { + $existingGroups = array_filter($gids, fn (string $gid): bool => $backend->groupExists($gid)); + } + foreach ($existingGroups as $group) { + $backends[$group][] = $backend; + } + } + } + foreach ($gids as $gid) { + if (count($backends[$gid]) === 0) { + continue; + } + $this->cachedGroups[$gid] = new Group($gid, $backends[$gid], $this->dispatcher, $this->userManager, $this, $displayNames[$gid]); + $groups[$gid] = $this->cachedGroups[$gid]; + } + return $groups; + } + + /** * @param string $gid * @return bool */ @@ -246,13 +307,9 @@ class Manager extends PublicEmitter implements IGroupManager { $groups = []; foreach ($this->backends as $backend) { $groupIds = $backend->getGroups($search, $limit ?? -1, $offset ?? 0); - foreach ($groupIds as $groupId) { - $aGroup = $this->get($groupId); - if ($aGroup instanceof IGroup) { - $groups[$groupId] = $aGroup; - } else { - $this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']); - } + $newGroups = $this->getGroupsObjects($groupIds); + foreach ($newGroups as $groupId => $group) { + $groups[$groupId] = $group; } if (!is_null($limit) and $limit <= 0) { return array_values($groups); diff --git a/lib/private/Http/Client/DnsPinMiddleware.php b/lib/private/Http/Client/DnsPinMiddleware.php index c6a58972fdd..aecccc6ce97 100644 --- a/lib/private/Http/Client/DnsPinMiddleware.php +++ b/lib/private/Http/Client/DnsPinMiddleware.php @@ -55,7 +55,7 @@ class DnsPinMiddleware { $second = array_pop($labels); $hostname = $second . '.' . $top; - $responses = dns_get_record($hostname, DNS_SOA); + $responses = $this->dnsGetRecord($hostname, DNS_SOA); if ($responses === false || count($responses) === 0) { return null; @@ -81,7 +81,7 @@ class DnsPinMiddleware { continue; } - $dnsResponses = dns_get_record($target, $dnsType); + $dnsResponses = $this->dnsGetRecord($target, $dnsType); $canHaveCnameRecord = true; if ($dnsResponses !== false && count($dnsResponses) > 0) { foreach ($dnsResponses as $dnsResponse) { @@ -104,6 +104,13 @@ class DnsPinMiddleware { return $targetIps; } + /** + * Wrapper for dns_get_record + */ + protected function dnsGetRecord(string $hostname, int $type): array|false { + return \dns_get_record($hostname, $type); + } + public function addDnsPinning() { return function (callable $handler) { return function ( @@ -128,6 +135,10 @@ class DnsPinMiddleware { $targetIps = $this->dnsResolve(idn_to_utf8($hostName), 0); + if (empty($targetIps)) { + throw new LocalServerException('No DNS record found for ' . $hostName); + } + $curlResolves = []; foreach ($ports as $port) { diff --git a/lib/private/Installer.php b/lib/private/Installer.php index dc81135b644..dd4f1f790e3 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -53,6 +53,7 @@ use OCP\HintException; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\ITempManager; +use OCP\Migration\IOutput; use phpseclib\File\X509; use Psr\Log\LoggerInterface; @@ -536,7 +537,10 @@ class Installer { * working ownCloud at the end instead of an aborted update. * @return array Array of error messages (appid => Exception) */ - public static function installShippedApps($softErrors = false) { + public static function installShippedApps($softErrors = false, ?IOutput $output = null) { + if ($output instanceof IOutput) { + $output->debug('Installing shipped apps'); + } $appManager = \OC::$server->getAppManager(); $config = \OC::$server->getConfig(); $errors = []; @@ -551,7 +555,7 @@ class Installer { && $config->getAppValue($filename, 'enabled') !== 'no') { if ($softErrors) { try { - Installer::installShippedApp($filename); + Installer::installShippedApp($filename, $output); } catch (HintException $e) { if ($e->getPrevious() instanceof TableExistsException) { $errors[$filename] = $e; @@ -560,7 +564,7 @@ class Installer { throw $e; } } else { - Installer::installShippedApp($filename); + Installer::installShippedApp($filename, $output); } $config->setAppValue($filename, 'enabled', 'yes'); } @@ -578,9 +582,12 @@ class Installer { /** * install an app already placed in the app folder * @param string $app id of the app to install - * @return integer + * @return string */ - public static function installShippedApp($app) { + public static function installShippedApp($app, ?IOutput $output = null) { + if ($output instanceof IOutput) { + $output->debug('Installing ' . $app); + } //install the database $appPath = OC_App::getAppPath($app); \OC_App::registerAutoloading($app, $appPath); @@ -588,6 +595,9 @@ class Installer { $config = \OC::$server->getConfig(); $ms = new MigrationService($app, \OC::$server->get(Connection::class)); + if ($output instanceof IOutput) { + $ms->setOutput($output); + } $previousVersion = $config->getAppValue($app, 'installed_version', false); $ms->migrate('latest', !$previousVersion); @@ -598,6 +608,9 @@ class Installer { if (is_null($info)) { return false; } + if ($output instanceof IOutput) { + $output->debug('Registering tasks of ' . $app); + } \OC_App::setupBackgroundJobs($info['background-jobs']); OC_App::executeRepairSteps($app, $info['repair-steps']['install']); diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 778124c4c38..6de620e7ec7 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -358,7 +358,7 @@ class Factory implements IFactory { $files = scandir($dir); if ($files !== false) { foreach ($files as $file) { - if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') { + if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) { $available[] = substr($file, 0, -5); } } @@ -374,7 +374,7 @@ class Factory implements IFactory { $files = scandir($themeDir); if ($files !== false) { foreach ($files as $file) { - if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') { + if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) { $available[] = substr($file, 0, -5); } } @@ -490,10 +490,14 @@ class Factory implements IFactory { [$preferred_language] = explode(';', $preference); $preferred_language = str_replace('-', '_', $preferred_language); + $preferred_language_parts = explode('_', $preferred_language); foreach ($available as $available_language) { if ($preferred_language === strtolower($available_language)) { return $this->respectDefaultLanguage($app, $available_language); } + if ($preferred_language_parts[0].'_'.end($preferred_language_parts) === strtolower($available_language)) { + return $available_language; + } } // Fallback from de_De to de diff --git a/lib/private/Lock/AbstractLockingProvider.php b/lib/private/Lock/AbstractLockingProvider.php index 6e8289db12e..604d098fa65 100644 --- a/lib/private/Lock/AbstractLockingProvider.php +++ b/lib/private/Lock/AbstractLockingProvider.php @@ -33,14 +33,18 @@ use OCP\Lock\ILockingProvider; * to release any leftover locks at the end of the request */ abstract class AbstractLockingProvider implements ILockingProvider { - /** how long until we clear stray locks in seconds */ - protected int $ttl; - - protected $acquiredLocks = [ + protected array $acquiredLocks = [ 'shared' => [], 'exclusive' => [] ]; + /** + * + * @param int $ttl how long until we clear stray locks in seconds + */ + public function __construct(protected int $ttl) { + } + /** @inheritDoc */ protected function hasAcquiredLock(string $path, int $type): bool { if ($type === self::LOCK_SHARED) { diff --git a/lib/private/Lock/DBLockingProvider.php b/lib/private/Lock/DBLockingProvider.php index fb8af8ac55b..087b1287754 100644 --- a/lib/private/Lock/DBLockingProvider.php +++ b/lib/private/Lock/DBLockingProvider.php @@ -39,21 +39,15 @@ use OCP\Lock\LockedException; * Locking provider that stores the locks in the database */ class DBLockingProvider extends AbstractLockingProvider { - private IDBConnection $connection; - private ITimeFactory $timeFactory; private array $sharedLocks = []; - private bool $cacheSharedLocks; public function __construct( - IDBConnection $connection, - ITimeFactory $timeFactory, + private IDBConnection $connection, + private ITimeFactory $timeFactory, int $ttl = 3600, - bool $cacheSharedLocks = true + private bool $cacheSharedLocks = true ) { - $this->connection = $connection; - $this->timeFactory = $timeFactory; - $this->ttl = $ttl; - $this->cacheSharedLocks = $cacheSharedLocks; + parent::__construct($ttl); } /** diff --git a/lib/private/Lock/MemcacheLockingProvider.php b/lib/private/Lock/MemcacheLockingProvider.php index d4eebd7c302..8ad25576084 100644 --- a/lib/private/Lock/MemcacheLockingProvider.php +++ b/lib/private/Lock/MemcacheLockingProvider.php @@ -32,11 +32,11 @@ use OCP\IMemcacheTTL; use OCP\Lock\LockedException; class MemcacheLockingProvider extends AbstractLockingProvider { - private IMemcache $memcache; - - public function __construct(IMemcache $memcache, int $ttl = 3600) { - $this->memcache = $memcache; - $this->ttl = $ttl; + public function __construct( + private IMemcache $memcache, + int $ttl = 3600, + ) { + parent::__construct($ttl); } private function setTTL(string $path): void { diff --git a/lib/private/Log.php b/lib/private/Log.php index d6750491d92..1784114911f 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -344,7 +344,7 @@ class Log implements ILogger, IDataLogger { unset($data['app']); unset($data['level']); $data = array_merge($serializer->serializeException($exception), $data); - $data = $this->interpolateMessage($data, $context['message'] ?? '--', 'CustomMessage'); + $data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage'); array_walk($context, [$this->normalizer, 'format']); diff --git a/lib/private/Log/ErrorHandler.php b/lib/private/Log/ErrorHandler.php index c4b9631e75a..e5e04182cd0 100644 --- a/lib/private/Log/ErrorHandler.php +++ b/lib/private/Log/ErrorHandler.php @@ -36,10 +36,9 @@ use Psr\Log\LoggerInterface; use Throwable; class ErrorHandler { - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } /** @@ -94,20 +93,11 @@ class ErrorHandler { } private static function errnoToLogLevel(int $errno): int { - switch ($errno) { - case E_USER_WARNING: - return ILogger::WARN; - - case E_DEPRECATED: - case E_USER_DEPRECATED: - return ILogger::DEBUG; - - case E_USER_NOTICE: - return ILogger::INFO; - - case E_USER_ERROR: - default: - return ILogger::ERROR; - } + return match ($errno) { + E_USER_WARNING => ILogger::WARN, + E_DEPRECATED, E_USER_DEPRECATED => ILogger::DEBUG, + E_USER_NOTICE => ILogger::INFO, + default => ILogger::ERROR, + }; } } diff --git a/lib/private/Log/Errorlog.php b/lib/private/Log/Errorlog.php index 72d11aa098c..aaea8234f27 100644 --- a/lib/private/Log/Errorlog.php +++ b/lib/private/Log/Errorlog.php @@ -32,22 +32,19 @@ use OC\SystemConfig; use OCP\Log\IWriter; class Errorlog extends LogDetails implements IWriter { - /** @var string */ - protected $tag; - - public function __construct(SystemConfig $config, string $tag = 'nextcloud') { + public function __construct( + SystemConfig $config, + protected string $tag = 'nextcloud', + ) { parent::__construct($config); - $this->tag = $tag; } /** * Write a message in the log * - * @param string $app * @param string|array $message - * @param int $level */ - public function write(string $app, $message, int $level) { + public function write(string $app, $message, int $level): void { error_log('[' . $this->tag . ']['.$app.']['.$level.'] '.$this->logDetailsAsJSON($app, $message, $level)); } } diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php index b585461e8d9..8b895bcb6be 100644 --- a/lib/private/Log/ExceptionSerializer.php +++ b/lib/private/Log/ExceptionSerializer.php @@ -112,11 +112,9 @@ class ExceptionSerializer { ]; - /** @var SystemConfig */ - private $systemConfig; - - public function __construct(SystemConfig $systemConfig) { - $this->systemConfig = $systemConfig; + public function __construct( + private SystemConfig $systemConfig, + ) { } protected array $methodsWithSensitiveParametersByClass = [ @@ -219,7 +217,7 @@ class ExceptionSerializer { }, $trace); } - private function removeValuesFromArgs($args, $values) { + private function removeValuesFromArgs($args, $values): array { $workArgs = []; foreach ($args as $arg) { if (in_array($arg, $values, true)) { @@ -279,7 +277,7 @@ class ExceptionSerializer { return $arg; } - public function serializeException(\Throwable $exception) { + public function serializeException(\Throwable $exception): array { $data = [ 'Exception' => get_class($exception), 'Message' => $exception->getMessage(), diff --git a/lib/private/Log/File.php b/lib/private/Log/File.php index a33667c9b68..328b0346985 100644 --- a/lib/private/Log/File.php +++ b/lib/private/Log/File.php @@ -48,14 +48,15 @@ use OCP\Log\IWriter; */ class File extends LogDetails implements IWriter, IFileBased { - /** @var string */ - protected $logFile; - /** @var int */ - protected $logFileMode; - /** @var SystemConfig */ - private $config; + protected string $logFile; - public function __construct(string $path, string $fallbackPath, SystemConfig $config) { + protected int $logFileMode; + + public function __construct( + string $path, + string $fallbackPath, + private SystemConfig $config, + ) { parent::__construct($config); $this->logFile = $path; if (!file_exists($this->logFile)) { @@ -69,17 +70,14 @@ class File extends LogDetails implements IWriter, IFileBased { $this->logFile = $fallbackPath; } } - $this->config = $config; $this->logFileMode = $config->getValue('logfilemode', 0640); } /** * write a message in the log - * @param string $app * @param string|array $message - * @param int $level */ - public function write(string $app, $message, int $level) { + public function write(string $app, $message, int $level): void { $entry = $this->logDetailsAsJSON($app, $message, $level); $handle = @fopen($this->logFile, 'a'); if ($this->logFileMode > 0 && is_file($this->logFile) && (fileperms($this->logFile) & 0777) != $this->logFileMode) { @@ -102,11 +100,8 @@ class File extends LogDetails implements IWriter, IFileBased { /** * get entries from the log in reverse chronological order - * @param int $limit - * @param int $offset - * @return array */ - public function getEntries(int $limit = 50, int $offset = 0):array { + public function getEntries(int $limit = 50, int $offset = 0): array { $minLevel = $this->config->getValue("loglevel", ILogger::WARN); $entries = []; $handle = @fopen($this->logFile, 'rb'); @@ -148,9 +143,6 @@ class File extends LogDetails implements IWriter, IFileBased { return $entries; } - /** - * @return string - */ public function getLogFilePath():string { return $this->logFile; } diff --git a/lib/private/Log/LogDetails.php b/lib/private/Log/LogDetails.php index c82904d7cea..ec88aa767fb 100644 --- a/lib/private/Log/LogDetails.php +++ b/lib/private/Log/LogDetails.php @@ -28,11 +28,9 @@ namespace OC\Log; use OC\SystemConfig; abstract class LogDetails { - /** @var SystemConfig */ - private $config; - - public function __construct(SystemConfig $config) { - $this->config = $config; + public function __construct( + private SystemConfig $config, + ) { } public function logDetails(string $app, $message, int $level): array { diff --git a/lib/private/Log/LogFactory.php b/lib/private/Log/LogFactory.php index a5008f5ef77..c395c31eb98 100644 --- a/lib/private/Log/LogFactory.php +++ b/lib/private/Log/LogFactory.php @@ -33,57 +33,37 @@ use OCP\Log\IWriter; use Psr\Log\LoggerInterface; class LogFactory implements ILogFactory { - /** @var IServerContainer */ - private $c; - /** @var SystemConfig */ - private $systemConfig; - - public function __construct(IServerContainer $c, SystemConfig $systemConfig) { - $this->c = $c; - $this->systemConfig = $systemConfig; + public function __construct( + private IServerContainer $c, + private SystemConfig $systemConfig, + ) { } /** * @throws \OCP\AppFramework\QueryException */ public function get(string $type):IWriter { - switch (strtolower($type)) { - case 'errorlog': - return new Errorlog($this->systemConfig); - case 'syslog': - return $this->c->resolve(Syslog::class); - case 'systemd': - return $this->c->resolve(Systemdlog::class); - case 'file': - return $this->buildLogFile(); - - // Backwards compatibility for old and fallback for unknown log types - case 'owncloud': - case 'nextcloud': - default: - return $this->buildLogFile(); - } + return match (strtolower($type)) { + 'errorlog' => new Errorlog($this->systemConfig), + 'syslog' => $this->c->resolve(Syslog::class), + 'systemd' => $this->c->resolve(Systemdlog::class), + 'file' => $this->buildLogFile(), + default => $this->buildLogFile(), + }; } - public function getCustomLogger(string $path):ILogger { + public function getCustomLogger(string $path): ILogger { $log = $this->buildLogFile($path); return new Log($log, $this->systemConfig); } protected function createNewLogger(string $type, string $tag, string $path): IWriter { - switch (strtolower($type)) { - case 'errorlog': - return new Errorlog($this->systemConfig, $tag); - case 'syslog': - return new Syslog($this->systemConfig, $tag); - case 'systemd': - return new Systemdlog($this->systemConfig, $tag); - case 'file': - case 'owncloud': - case 'nextcloud': - default: - return $this->buildLogFile($path); - } + return match (strtolower($type)) { + 'errorlog' => new Errorlog($this->systemConfig, $tag), + 'syslog' => new Syslog($this->systemConfig, $tag), + 'systemd' => new Systemdlog($this->systemConfig, $tag), + default => $this->buildLogFile($path), + }; } public function getCustomPsrLogger(string $path, string $type = 'file', string $tag = 'Nextcloud'): LoggerInterface { @@ -93,7 +73,7 @@ class LogFactory implements ILogFactory { ); } - protected function buildLogFile(string $logFile = ''):File { + protected function buildLogFile(string $logFile = ''): File { $defaultLogFile = $this->systemConfig->getValue('datadirectory', \OC::$SERVERROOT.'/data').'/nextcloud.log'; if ($logFile === '') { $logFile = $this->systemConfig->getValue('logfile', $defaultLogFile); diff --git a/lib/private/Log/PsrLoggerAdapter.php b/lib/private/Log/PsrLoggerAdapter.php index 07a898e2528..12254bfc67f 100644 --- a/lib/private/Log/PsrLoggerAdapter.php +++ b/lib/private/Log/PsrLoggerAdapter.php @@ -36,14 +36,12 @@ use function array_key_exists; use function array_merge; final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { - /** @var Log */ - private $logger; - - public function __construct(Log $logger) { - $this->logger = $logger; + public function __construct( + private Log $logger, + ) { } - public function setEventDispatcher(IEventDispatcher $eventDispatcher) { + public function setEventDispatcher(IEventDispatcher $eventDispatcher): void { $this->logger->setEventDispatcher($eventDispatcher); } @@ -55,9 +53,6 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * System is unusable. * * @param string $message - * @param array $context - * - * @return void */ public function emergency($message, array $context = []): void { if ($this->containsThrowable($context)) { @@ -80,11 +75,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * trigger the SMS alerts and wake you up. * * @param string $message - * @param array $context - * - * @return void */ - public function alert($message, array $context = []) { + public function alert($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -104,11 +96,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Example: Application component unavailable, unexpected exception. * * @param string $message - * @param array $context - * - * @return void */ - public function critical($message, array $context = []) { + public function critical($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -127,11 +116,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * be logged and monitored. * * @param string $message - * @param array $context - * - * @return void */ - public function error($message, array $context = []) { + public function error($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -152,11 +138,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * that are not necessarily wrong. * * @param string $message - * @param array $context - * - * @return void */ - public function warning($message, array $context = []) { + public function warning($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -174,11 +157,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Normal but significant events. * * @param string $message - * @param array $context - * - * @return void */ - public function notice($message, array $context = []) { + public function notice($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -198,11 +178,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Example: User logs in, SQL logs. * * @param string $message - * @param array $context - * - * @return void */ - public function info($message, array $context = []) { + public function info($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -220,11 +197,8 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Detailed debug information. * * @param string $message - * @param array $context - * - * @return void */ - public function debug($message, array $context = []) { + public function debug($message, array $context = []): void { if ($this->containsThrowable($context)) { $this->logger->logException($context['exception'], array_merge( [ @@ -243,13 +217,10 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * * @param mixed $level * @param string $message - * @param array $context - * - * @return void * * @throws InvalidArgumentException */ - public function log($level, $message, array $context = []) { + public function log($level, $message, array $context = []): void { if (!is_int($level) || $level < ILogger::DEBUG || $level > ILogger::FATAL) { throw new InvalidArgumentException('Nextcloud allows only integer log levels'); } diff --git a/lib/private/Log/Rotate.php b/lib/private/Log/Rotate.php index dfb588837f3..4c0e258b2f9 100644 --- a/lib/private/Log/Rotate.php +++ b/lib/private/Log/Rotate.php @@ -35,7 +35,7 @@ use OCP\Log\RotationTrait; class Rotate extends \OCP\BackgroundJob\Job { use RotationTrait; - public function run($dummy) { + public function run($dummy): void { $systemConfig = \OC::$server->getSystemConfig(); $this->filePath = $systemConfig->getValue('logfile', $systemConfig->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/nextcloud.log'); diff --git a/lib/private/Log/Syslog.php b/lib/private/Log/Syslog.php index f4ba857742f..5f220ee1eb7 100644 --- a/lib/private/Log/Syslog.php +++ b/lib/private/Log/Syslog.php @@ -30,7 +30,7 @@ use OCP\ILogger; use OCP\Log\IWriter; class Syslog extends LogDetails implements IWriter { - protected $levels = [ + protected array $levels = [ ILogger::DEBUG => LOG_DEBUG, ILogger::INFO => LOG_INFO, ILogger::WARN => LOG_WARNING, @@ -38,7 +38,10 @@ class Syslog extends LogDetails implements IWriter { ILogger::FATAL => LOG_CRIT, ]; - public function __construct(SystemConfig $config, ?string $tag = null) { + public function __construct( + SystemConfig $config, + ?string $tag = null, + ) { parent::__construct($config); if ($tag === null) { $tag = $config->getValue('syslog_tag', 'Nextcloud'); @@ -52,11 +55,9 @@ class Syslog extends LogDetails implements IWriter { /** * write a message in the log - * @param string $app * @param string|array $message - * @param int $level */ - public function write(string $app, $message, int $level) { + public function write(string $app, $message, int $level): void { $syslog_level = $this->levels[$level]; syslog($syslog_level, $this->logDetailsAsJSON($app, $message, $level)); } diff --git a/lib/private/Log/Systemdlog.php b/lib/private/Log/Systemdlog.php index 8619cb5e4dd..e4b4ce35c12 100644 --- a/lib/private/Log/Systemdlog.php +++ b/lib/private/Log/Systemdlog.php @@ -46,7 +46,7 @@ use OCP\Log\IWriter; // Syslog compatibility fields class Systemdlog extends LogDetails implements IWriter { - protected $levels = [ + protected array $levels = [ ILogger::DEBUG => 7, ILogger::INFO => 6, ILogger::WARN => 4, @@ -54,9 +54,12 @@ class Systemdlog extends LogDetails implements IWriter { ILogger::FATAL => 2, ]; - protected $syslogId; + protected string $syslogId; - public function __construct(SystemConfig $config, ?string $tag = null) { + public function __construct( + SystemConfig $config, + ?string $tag = null, + ) { parent::__construct($config); if (!function_exists('sd_journal_send')) { throw new HintException( @@ -71,11 +74,9 @@ class Systemdlog extends LogDetails implements IWriter { /** * Write a message to the log. - * @param string $app * @param string|array $message - * @param int $level */ - public function write(string $app, $message, int $level) { + public function write(string $app, $message, int $level): void { $journal_level = $this->levels[$level]; sd_journal_send('PRIORITY='.$journal_level, 'SYSLOG_IDENTIFIER='.$this->syslogId, diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index 788a7c2e8c9..16d6ae32f72 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -31,6 +31,7 @@ */ namespace OC\Memcache; +use OCP\Cache\CappedMemoryCache; use OCP\Profiler\IProfiler; use OCP\ICache; use OCP\ICacheFactory; @@ -184,13 +185,8 @@ class Factory implements ICacheFactory { return $this->distributedCacheClass !== self::NULL_CACHE; } - /** - * @see \OC\Memcache\Factory::createLocal() - * @param string $prefix - * @return ICache - */ - public function createLowLatency(string $prefix = ''): ICache { - return $this->createLocal($prefix); + public function createInMemory(int $capacity = 512): ICache { + return new CappedMemoryCache($capacity); } /** diff --git a/lib/private/Metadata/Capabilities.php b/lib/private/Metadata/Capabilities.php deleted file mode 100644 index 2fa0006f581..00000000000 --- a/lib/private/Metadata/Capabilities.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata; - -use OCP\Capabilities\IPublicCapability; -use OCP\IConfig; - -class Capabilities implements IPublicCapability { - private IMetadataManager $manager; - private IConfig $config; - - public function __construct(IMetadataManager $manager, IConfig $config) { - $this->manager = $manager; - $this->config = $config; - } - - public function getCapabilities() { - if ($this->config->getSystemValueBool('enable_file_metadata', true)) { - return ['metadataAvailable' => $this->manager->getCapabilities()]; - } - - return []; - } -} diff --git a/lib/private/Metadata/FileEventListener.php b/lib/private/Metadata/FileEventListener.php deleted file mode 100644 index 162e85ff3aa..00000000000 --- a/lib/private/Metadata/FileEventListener.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata; - -use OC\Files\Filesystem; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; -use OCP\Files\Events\Node\NodeDeletedEvent; -use OCP\Files\Events\Node\NodeWrittenEvent; -use OCP\Files\Events\NodeRemovedFromCache; -use OCP\Files\File; -use OCP\Files\Node; -use OCP\Files\NotFoundException; -use OCP\Files\FileInfo; -use Psr\Log\LoggerInterface; - -/** - * @template-implements IEventListener<NodeRemovedFromCache> - * @template-implements IEventListener<NodeDeletedEvent> - * @template-implements IEventListener<NodeWrittenEvent> - */ -class FileEventListener implements IEventListener { - private IMetadataManager $manager; - private LoggerInterface $logger; - - public function __construct(IMetadataManager $manager, LoggerInterface $logger) { - $this->manager = $manager; - $this->logger = $logger; - } - - private function shouldExtractMetadata(Node $node): bool { - try { - if ($node->getMimetype() === 'httpd/unix-directory') { - return false; - } - } catch (NotFoundException $e) { - return false; - } - if ($node->getSize(false) <= 0) { - return false; - } - - $path = $node->getPath(); - return $this->isCorrectPath($path); - } - - private function isCorrectPath(string $path): bool { - // TODO make this more dynamic, we have the same issue in other places - return !str_starts_with($path, 'appdata_') && !str_starts_with($path, 'files_versions/') && !str_starts_with($path, 'files_trashbin/'); - } - - public function handle(Event $event): void { - if ($event instanceof NodeRemovedFromCache) { - if (!$this->isCorrectPath($event->getPath())) { - // Don't listen to paths for which we don't extract metadata - return; - } - $view = Filesystem::getView(); - if (!$view) { - // Should not happen since a scan in the user folder should setup - // the file system. - $e = new \Exception(); // don't trigger, just get backtrace - $this->logger->error('Detecting deletion of a file with possible metadata but file system setup is not setup', [ - 'exception' => $e, - 'app' => 'metadata' - ]); - return; - } - $info = $view->getFileInfo($event->getPath()); - if ($info && $info->getType() === FileInfo::TYPE_FILE) { - $this->manager->clearMetadata($info->getId()); - } - } - - if ($event instanceof NodeDeletedEvent) { - $node = $event->getNode(); - if ($this->shouldExtractMetadata($node)) { - /** @var File $node */ - $this->manager->clearMetadata($event->getNode()->getId()); - } - } - - if ($event instanceof NodeWrittenEvent) { - $node = $event->getNode(); - if ($this->shouldExtractMetadata($node)) { - /** @var File $node */ - $this->manager->generateMetadata($event->getNode(), false); - } - } - } -} diff --git a/lib/private/Metadata/FileMetadata.php b/lib/private/Metadata/FileMetadata.php deleted file mode 100644 index a9808a86998..00000000000 --- a/lib/private/Metadata/FileMetadata.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata; - -use OCP\AppFramework\Db\Entity; -use OCP\DB\Types; - -/** - * @method string getGroupName() - * @method void setGroupName(string $groupName) - * @method string getValue() - * @method void setValue(string $value) - * @see \OC\Core\Migrations\Version240000Date20220404230027 - */ -class FileMetadata extends Entity { - protected ?string $groupName = null; - protected ?string $value = null; - - public function __construct() { - $this->addType('groupName', 'string'); - $this->addType('value', Types::STRING); - } - - public function getDecodedValue(): array { - return json_decode($this->getValue(), true) ?? []; - } - - public function setArrayAsValue(array $value): void { - $this->setValue(json_encode($value, JSON_THROW_ON_ERROR)); - } -} diff --git a/lib/private/Metadata/FileMetadataMapper.php b/lib/private/Metadata/FileMetadataMapper.php deleted file mode 100644 index 003ab13126e..00000000000 --- a/lib/private/Metadata/FileMetadataMapper.php +++ /dev/null @@ -1,177 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * @copyright Copyright 2022 Louis Chmn <louis@chmn.me> - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata; - -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\AppFramework\Db\QBMapper; -use OCP\AppFramework\Db\Entity; -use OCP\DB\Exception; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; - -/** - * @template-extends QBMapper<FileMetadata> - */ -class FileMetadataMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'file_metadata', FileMetadata::class); - } - - /** - * @return FileMetadata[] - * @throws Exception - */ - public function findForFile(int $fileId): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - - return $this->findEntities($qb); - } - - /** - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - * @throws Exception - */ - public function findForGroupForFile(int $fileId, string $groupName): FileMetadata { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, IQueryBuilder::PARAM_STR))); - - return $this->findEntity($qb); - } - - /** - * @return array<int, FileMetadata> - * @throws Exception - */ - public function findForGroupForFiles(array $fileIds, string $groupName): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->in('id', $qb->createParameter('fileIds'))) - ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, IQueryBuilder::PARAM_STR))); - - $metadata = []; - foreach (array_chunk($fileIds, 1000) as $fileIdsChunk) { - $qb->setParameter('fileIds', $fileIdsChunk, IQueryBuilder::PARAM_INT_ARRAY); - /** @var FileMetadata[] $rawEntities */ - $rawEntities = $this->findEntities($qb); - foreach ($rawEntities as $entity) { - $metadata[$entity->getId()] = $entity; - } - } - - foreach ($fileIds as $id) { - if (isset($metadata[$id])) { - continue; - } - $empty = new FileMetadata(); - $empty->setValue(''); - $empty->setGroupName($groupName); - $empty->setId($id); - $metadata[$id] = $empty; - } - return $metadata; - } - - public function clear(int $fileId): void { - $qb = $this->db->getQueryBuilder(); - $qb->delete($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - - $qb->executeStatement(); - } - - /** - * Updates an entry in the db from an entity - * - * @param FileMetadata $entity the entity that should be created - * @return FileMetadata the saved entity with the set id - * @throws Exception - * @throws \InvalidArgumentException if entity has no id - */ - public function update(Entity $entity): FileMetadata { - if (!($entity instanceof FileMetadata)) { - throw new \Exception("Entity should be a FileMetadata entity"); - } - - // entity needs an id - $id = $entity->getId(); - if ($id === null) { - throw new \InvalidArgumentException('Entity which should be updated has no id'); - } - - // entity needs an group_name - $groupName = $entity->getGroupName(); - if ($groupName === null) { - throw new \InvalidArgumentException('Entity which should be updated has no group_name'); - } - - $idType = $this->getParameterTypeForProperty($entity, 'id'); - $groupNameType = $this->getParameterTypeForProperty($entity, 'groupName'); - $value = $entity->getValue(); - $valueType = $this->getParameterTypeForProperty($entity, 'value'); - - $qb = $this->db->getQueryBuilder(); - - $qb->update($this->tableName) - ->set('value', $qb->createNamedParameter($value, $valueType)) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))) - ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, $groupNameType))) - ->executeStatement(); - - return $entity; - } - - /** - * Override the insertOrUpdate as we could be in a transaction in which case we can not afford on error. - * - * @param FileMetadata $entity the entity that should be created/updated - * @return FileMetadata the saved entity with the (new) id - * @throws Exception - * @throws \InvalidArgumentException if entity has no id - */ - public function insertOrUpdate(Entity $entity): FileMetadata { - try { - $existingEntity = $this->findForGroupForFile($entity->getId(), $entity->getGroupName()); - } catch (\Throwable) { - $existingEntity = null; - } - - if ($existingEntity !== null) { - if ($entity->getValue() !== $existingEntity->getValue()) { - return $this->update($entity); - } else { - return $existingEntity; - } - } else { - return parent::insertOrUpdate($entity); - } - } -} diff --git a/lib/private/Metadata/IMetadataManager.php b/lib/private/Metadata/IMetadataManager.php deleted file mode 100644 index fa0bcc22801..00000000000 --- a/lib/private/Metadata/IMetadataManager.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace OC\Metadata; - -use OCP\Files\File; - -/** - * Interface to manage additional metadata for files - */ -interface IMetadataManager { - /** - * @param class-string<IMetadataProvider> $className - */ - public function registerProvider(string $className): void; - - /** - * Generate the metadata for one file - */ - public function generateMetadata(File $file, bool $checkExisting = false): void; - - /** - * Clear the metadata for one file - */ - public function clearMetadata(int $fileId): void; - - /** @return array<int, FileMetadata> */ - public function fetchMetadataFor(string $group, array $fileIds): array; - - /** - * Get the capabilities as an array of mimetype regex to the type provided - */ - public function getCapabilities(): array; -} diff --git a/lib/private/Metadata/IMetadataProvider.php b/lib/private/Metadata/IMetadataProvider.php deleted file mode 100644 index 7cbe102a538..00000000000 --- a/lib/private/Metadata/IMetadataProvider.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -namespace OC\Metadata; - -use OCP\Files\File; - -/** - * Interface for the metadata providers. If you want an application to provide - * some metadata, you can use this to store them. - */ -interface IMetadataProvider { - /** - * The list of groups that this metadata provider is able to provide. - * - * @return string[] - */ - public static function groupsProvided(): array; - - /** - * Check if the metadata provider is available. A metadata provider might be - * unavailable due to a php extension not being installed. - */ - public static function isAvailable(): bool; - - /** - * Get the mimetypes supported as a regex. - */ - public static function getMimetypesSupported(): string; - - /** - * Execute the extraction on the specified file. The metadata should be - * grouped by metadata - * - * Each group should be json serializable and the string representation - * shouldn't be longer than 4000 characters. - * - * @param File $file The file to extract the metadata from - * @param array<string, FileMetadata> An array containing all the metadata fetched. - */ - public function execute(File $file): array; -} diff --git a/lib/private/Metadata/MetadataManager.php b/lib/private/Metadata/MetadataManager.php deleted file mode 100644 index 6d96ff1ab68..00000000000 --- a/lib/private/Metadata/MetadataManager.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata; - -use OC\Metadata\Provider\ExifProvider; -use OCP\Files\File; - -class MetadataManager implements IMetadataManager { - /** @var array<string, IMetadataProvider> */ - private array $providers; - private array $providerClasses; - private FileMetadataMapper $fileMetadataMapper; - - public function __construct( - FileMetadataMapper $fileMetadataMapper - ) { - $this->providers = []; - $this->providerClasses = []; - $this->fileMetadataMapper = $fileMetadataMapper; - - // TODO move to another place, where? - $this->registerProvider(ExifProvider::class); - } - - /** - * @param class-string<IMetadataProvider> $className - */ - public function registerProvider(string $className):void { - if (in_array($className, $this->providerClasses)) { - return; - } - - if (call_user_func([$className, 'isAvailable'])) { - $this->providers[call_user_func([$className, 'getMimetypesSupported'])] = \OC::$server->get($className); - } - } - - public function generateMetadata(File $file, bool $checkExisting = false): void { - $existingMetadataGroups = []; - - if ($checkExisting) { - $existingMetadata = $this->fileMetadataMapper->findForFile($file->getId()); - foreach ($existingMetadata as $metadata) { - $existingMetadataGroups[] = $metadata->getGroupName(); - } - } - - foreach ($this->providers as $supportedMimetype => $provider) { - if (preg_match($supportedMimetype, $file->getMimeType())) { - if (count(array_diff($provider::groupsProvided(), $existingMetadataGroups)) > 0) { - $metaDataGroup = $provider->execute($file); - foreach ($metaDataGroup as $group => $metadata) { - $this->fileMetadataMapper->insertOrUpdate($metadata); - } - } - } - } - } - - public function clearMetadata(int $fileId): void { - $this->fileMetadataMapper->clear($fileId); - } - - /** - * @return array<int, FileMetadata> - */ - public function fetchMetadataFor(string $group, array $fileIds): array { - return $this->fileMetadataMapper->findForGroupForFiles($fileIds, $group); - } - - public function getCapabilities(): array { - $capabilities = []; - foreach ($this->providers as $supportedMimetype => $provider) { - foreach ($provider::groupsProvided() as $group) { - if (isset($capabilities[$group])) { - $capabilities[$group][] = $supportedMimetype; - } - $capabilities[$group] = [$supportedMimetype]; - } - } - return $capabilities; - } -} diff --git a/lib/private/Metadata/Provider/ExifProvider.php b/lib/private/Metadata/Provider/ExifProvider.php deleted file mode 100644 index b1598abbbc8..00000000000 --- a/lib/private/Metadata/Provider/ExifProvider.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> - * @copyright Copyright 2022 Louis Chmn <louis@chmn.me> - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Metadata\Provider; - -use OC\Metadata\FileMetadata; -use OC\Metadata\IMetadataProvider; -use OCP\Files\File; -use Psr\Log\LoggerInterface; - -class ExifProvider implements IMetadataProvider { - private LoggerInterface $logger; - - public function __construct( - LoggerInterface $logger - ) { - $this->logger = $logger; - } - - public static function groupsProvided(): array { - return ['size', 'gps']; - } - - public static function isAvailable(): bool { - return extension_loaded('exif'); - } - - /** @return array{'gps'?: FileMetadata, 'size'?: FileMetadata} */ - public function execute(File $file): array { - $exifData = []; - $fileDescriptor = $file->fopen('rb'); - - if ($fileDescriptor === false) { - return []; - } - - $data = null; - try { - // Needed to make reading exif data reliable. - // This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710 - // But I don't understand why 1 as a special meaning. - // Revert right after reading the exif data. - $oldBufferSize = stream_set_chunk_size($fileDescriptor, 1); - $data = @exif_read_data($fileDescriptor, 'ANY_TAG', true); - stream_set_chunk_size($fileDescriptor, $oldBufferSize); - } catch (\Exception $ex) { - $this->logger->info("Couldn't extract metadata for ".$file->getId(), ['exception' => $ex]); - } - - $size = new FileMetadata(); - $size->setGroupName('size'); - $size->setId($file->getId()); - $size->setArrayAsValue([]); - - if (!$data) { - $sizeResult = getimagesizefromstring($file->getContent()); - if ($sizeResult !== false) { - $size->setArrayAsValue([ - 'width' => $sizeResult[0], - 'height' => $sizeResult[1], - ]); - - $exifData['size'] = $size; - } - } elseif (array_key_exists('COMPUTED', $data)) { - if (array_key_exists('Width', $data['COMPUTED']) && array_key_exists('Height', $data['COMPUTED'])) { - $size->setArrayAsValue([ - 'width' => $data['COMPUTED']['Width'], - 'height' => $data['COMPUTED']['Height'], - ]); - - $exifData['size'] = $size; - } - } - - if ($data && array_key_exists('GPS', $data) - && array_key_exists('GPSLatitude', $data['GPS']) && array_key_exists('GPSLatitudeRef', $data['GPS']) - && array_key_exists('GPSLongitude', $data['GPS']) && array_key_exists('GPSLongitudeRef', $data['GPS']) - ) { - $gps = new FileMetadata(); - $gps->setGroupName('gps'); - $gps->setId($file->getId()); - $gps->setArrayAsValue([ - 'latitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLatitude'], $data['GPS']['GPSLatitudeRef']), - 'longitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLongitude'], $data['GPS']['GPSLongitudeRef']), - ]); - - $exifData['gps'] = $gps; - } - - return $exifData; - } - - public static function getMimetypesSupported(): string { - return '/image\/(png|jpeg|heif|webp|tiff)/'; - } - - /** - * @param array|string $coordinates - */ - private static function gpsDegreesToDecimal($coordinates, ?string $hemisphere): float { - if (is_string($coordinates)) { - $coordinates = array_map("trim", explode(",", $coordinates)); - } - - if (count($coordinates) !== 3) { - throw new \Exception('Invalid coordinate format: ' . json_encode($coordinates)); - } - - [$degrees, $minutes, $seconds] = array_map(function (string $rawDegree) { - $parts = explode('/', $rawDegree); - - if ($parts[1] === '0') { - return 0; - } - - return floatval($parts[0]) / floatval($parts[1] ?? 1); - }, $coordinates); - - $sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1; - return $sign * ($degrees + $minutes / 60 + $seconds / 3600); - } -} diff --git a/lib/private/Migration/BackgroundRepair.php b/lib/private/Migration/BackgroundRepair.php index 579ba494e58..f48a62131bd 100644 --- a/lib/private/Migration/BackgroundRepair.php +++ b/lib/private/Migration/BackgroundRepair.php @@ -41,15 +41,13 @@ use Psr\Log\LoggerInterface; * @package OC\Migration */ class BackgroundRepair extends TimedJob { - private IJobList $jobList; - private LoggerInterface $logger; - private IEventDispatcher $dispatcher; - - public function __construct(IEventDispatcher $dispatcher, ITimeFactory $time, LoggerInterface $logger, IJobList $jobList) { + public function __construct( + private IEventDispatcher $dispatcher, + ITimeFactory $time, + private LoggerInterface $logger, + private IJobList $jobList, + ) { parent::__construct($time); - $this->dispatcher = $dispatcher; - $this->logger = $logger; - $this->jobList = $jobList; $this->setInterval(15 * 60); } @@ -58,7 +56,7 @@ class BackgroundRepair extends TimedJob { * @throws \Exception * @throws \OC\NeedsUpdateException */ - protected function run($argument) { + protected function run($argument): void { if (!isset($argument['app']) || !isset($argument['step'])) { // remove the job - we can never execute it $this->jobList->remove($this, $this->argument); @@ -101,7 +99,7 @@ class BackgroundRepair extends TimedJob { * @param $app * @throws NeedsUpdateException */ - protected function loadApp($app) { + protected function loadApp($app): void { OC_App::loadApp($app); } } diff --git a/lib/private/Migration/ConsoleOutput.php b/lib/private/Migration/ConsoleOutput.php index 9e3396f2a75..841e3d302fc 100644 --- a/lib/private/Migration/ConsoleOutput.php +++ b/lib/private/Migration/ConsoleOutput.php @@ -34,34 +34,35 @@ use Symfony\Component\Console\Output\OutputInterface; * @package OC\Migration */ class ConsoleOutput implements IOutput { - /** @var OutputInterface */ - private $output; + private ?ProgressBar $progressBar = null; - /** @var ProgressBar */ - private $progressBar; + public function __construct( + private OutputInterface $output, + ) { + } - public function __construct(OutputInterface $output) { - $this->output = $output; + public function debug(string $message): void { + $this->output->writeln($message, OutputInterface::VERBOSITY_VERBOSE); } /** * @param string $message */ - public function info($message) { + public function info($message): void { $this->output->writeln("<info>$message</info>"); } /** * @param string $message */ - public function warning($message) { + public function warning($message): void { $this->output->writeln("<comment>$message</comment>"); } /** * @param int $max */ - public function startProgress($max = 0) { + public function startProgress($max = 0): void { if (!is_null($this->progressBar)) { $this->progressBar->finish(); } @@ -73,7 +74,7 @@ class ConsoleOutput implements IOutput { * @param int $step * @param string $description */ - public function advance($step = 1, $description = '') { + public function advance($step = 1, $description = ''): void { if (is_null($this->progressBar)) { $this->progressBar = new ProgressBar($this->output); $this->progressBar->start(); @@ -84,7 +85,7 @@ class ConsoleOutput implements IOutput { } } - public function finishProgress() { + public function finishProgress(): void { if (is_null($this->progressBar)) { return; } diff --git a/lib/private/Migration/SimpleOutput.php b/lib/private/Migration/SimpleOutput.php index f97bcb767f8..f1b06d008bb 100644 --- a/lib/private/Migration/SimpleOutput.php +++ b/lib/private/Migration/SimpleOutput.php @@ -33,19 +33,21 @@ use Psr\Log\LoggerInterface; * @package OC\Migration */ class SimpleOutput implements IOutput { - private LoggerInterface $logger; - private $appName; + public function __construct( + private LoggerInterface $logger, + private $appName, + ) { + } - public function __construct(LoggerInterface $logger, $appName) { - $this->logger = $logger; - $this->appName = $appName; + public function debug(string $message): void { + $this->logger->debug($message, ['app' => $this->appName]); } /** * @param string $message * @since 9.1.0 */ - public function info($message) { + public function info($message): void { $this->logger->info($message, ['app' => $this->appName]); } @@ -53,7 +55,7 @@ class SimpleOutput implements IOutput { * @param string $message * @since 9.1.0 */ - public function warning($message) { + public function warning($message): void { $this->logger->warning($message, ['app' => $this->appName]); } @@ -61,7 +63,7 @@ class SimpleOutput implements IOutput { * @param int $max * @since 9.1.0 */ - public function startProgress($max = 0) { + public function startProgress($max = 0): void { } /** @@ -69,12 +71,12 @@ class SimpleOutput implements IOutput { * @param string $description * @since 9.1.0 */ - public function advance($step = 1, $description = '') { + public function advance($step = 1, $description = ''): void { } /** * @since 9.1.0 */ - public function finishProgress() { + public function finishProgress(): void { } } diff --git a/lib/private/NavigationManager.php b/lib/private/NavigationManager.php index 56f55e80331..d323b7c1e43 100644 --- a/lib/private/NavigationManager.php +++ b/lib/private/NavigationManager.php @@ -65,6 +65,10 @@ class NavigationManager implements INavigationManager { private $groupManager; /** @var IConfig */ private $config; + /** The default app for the current user (cached for the `add` function) */ + private ?string $defaultApp; + /** User defined app order (cached for the `add` function) */ + private array $customAppOrder; public function __construct(IAppManager $appManager, IURLGenerator $urlGenerator, @@ -78,6 +82,8 @@ class NavigationManager implements INavigationManager { $this->userSession = $userSession; $this->groupManager = $groupManager; $this->config = $config; + + $this->defaultApp = null; } /** @@ -101,7 +107,13 @@ class NavigationManager implements INavigationManager { } $id = $entry['id']; - $entry['unread'] = isset($this->unreadCounters[$id]) ? $this->unreadCounters[$id] : 0; + $entry['unread'] = $this->unreadCounters[$id] ?? 0; + if ($entry['type'] === 'link') { + // This is the default app that will always be shown first + $entry['default'] = ($entry['app'] ?? false) === $this->defaultApp; + // Set order from user defined app order + $entry['order'] = $this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100; + } $this->entries[$id] = $entry; } @@ -123,26 +135,44 @@ class NavigationManager implements INavigationManager { }); } - return $this->proceedNavigation($result); + return $this->proceedNavigation($result, $type); } /** - * Sort navigation entries by order, name and set active flag + * Sort navigation entries default app is always sorted first, then by order, name and set active flag * * @param array $list * @return array */ - private function proceedNavigation(array $list): array { + private function proceedNavigation(array $list, string $type): array { uasort($list, function ($a, $b) { - if (isset($a['order']) && isset($b['order'])) { + if (($a['default'] ?? false) xor ($b['default'] ?? false)) { + // Always sort the default app first + return ($a['default'] ?? false) ? -1 : 1; + } elseif (isset($a['order']) && isset($b['order'])) { + // Sort by order return ($a['order'] < $b['order']) ? -1 : 1; } elseif (isset($a['order']) || isset($b['order'])) { + // Sort the one that has an order property first return isset($a['order']) ? -1 : 1; } else { + // Sort by name otherwise return ($a['name'] < $b['name']) ? -1 : 1; } }); + if ($type === 'all' || $type === 'link') { + // There might be the case that no default app was set, in this case the first app is the default app. + // Otherwise the default app is already the ordered first, so setting the default prop will make no difference. + foreach ($list as $index => &$navEntry) { + if ($navEntry['type'] === 'link') { + $navEntry['default'] = true; + break; + } + } + unset($navEntry); + } + $activeApp = $this->getActiveEntry(); if ($activeApp !== null) { foreach ($list as $index => &$navEntry) { @@ -171,8 +201,8 @@ class NavigationManager implements INavigationManager { /** * @inheritDoc */ - public function setActiveEntry($id) { - $this->activeEntry = $id; + public function setActiveEntry($appId) { + $this->activeEntry = $appId; } /** @@ -200,7 +230,25 @@ class NavigationManager implements INavigationManager { ]); } + if ($this->appManager === 'null') { + return; + } + + $this->defaultApp = $this->appManager->getDefaultAppForUser($this->userSession->getUser(), false); + if ($this->userSession->isLoggedIn()) { + // Profile + $this->add([ + 'type' => 'settings', + 'id' => 'profile', + 'order' => 1, + 'href' => $this->urlGenerator->linkToRoute( + 'core.ProfilePage.index', + ['targetUserId' => $this->userSession->getUser()->getUID()], + ), + 'name' => $l->t('View profile'), + ]); + // Accessibility settings if ($this->appManager->isEnabledForUser('theming', $this->userSession->getUser())) { $this->add([ @@ -212,6 +260,7 @@ class NavigationManager implements INavigationManager { 'icon' => $this->urlGenerator->imagePath('theming', 'accessibility-dark.svg'), ]); } + if ($this->isAdmin()) { // App management $this->add([ @@ -280,14 +329,13 @@ class NavigationManager implements INavigationManager { } } - if ($this->appManager === 'null') { - return; - } - if ($this->userSession->isLoggedIn()) { - $apps = $this->appManager->getEnabledAppsForUser($this->userSession->getUser()); + $user = $this->userSession->getUser(); + $apps = $this->appManager->getEnabledAppsForUser($user); + $this->customAppOrder = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR); } else { $apps = $this->appManager->getInstalledApps(); + $this->customAppOrder = []; } foreach ($apps as $app) { @@ -309,16 +357,16 @@ class NavigationManager implements INavigationManager { if (!isset($nav['route']) && $nav['type'] !== 'settings') { continue; } - $role = isset($nav['@attributes']['role']) ? $nav['@attributes']['role'] : 'all'; + $role = $nav['@attributes']['role'] ?? 'all'; if ($role === 'admin' && !$this->isAdmin()) { continue; } $l = $this->l10nFac->get($app); $id = $nav['id'] ?? $app . ($key === 0 ? '' : $key); - $order = isset($nav['order']) ? $nav['order'] : 100; + $order = $nav['order'] ?? 100; $type = $nav['type']; $route = !empty($nav['route']) ? $this->urlGenerator->linkToRoute($nav['route']) : ''; - $icon = isset($nav['icon']) ? $nav['icon'] : 'app.svg'; + $icon = $nav['icon'] ?? 'app.svg'; foreach ([$icon, "$app.svg"] as $i) { try { $icon = $this->urlGenerator->imagePath($app, $i); @@ -331,14 +379,24 @@ class NavigationManager implements INavigationManager { $icon = $this->urlGenerator->imagePath('core', 'default-app-icon'); } - $this->add([ + $this->add(array_merge([ + // Navigation id 'id' => $id, + // Order where this entry should be shown 'order' => $order, + // Target of the navigation entry 'href' => $route, + // The icon used for the naviation entry 'icon' => $icon, + // Type of the navigation entry ('link' vs 'settings') 'type' => $type, + // Localized name of the navigation entry 'name' => $l->t($nav['name']), - ]); + ], $type === 'link' ? [ + // App that registered this navigation entry (not necessarly the same as the id) + 'app' => $app, + ] : [] + )); } } } diff --git a/lib/private/Net/HostnameClassifier.php b/lib/private/Net/HostnameClassifier.php index 626aa47083e..42dae790152 100644 --- a/lib/private/Net/HostnameClassifier.php +++ b/lib/private/Net/HostnameClassifier.php @@ -52,10 +52,6 @@ class HostnameClassifier { * Check host identifier for local hostname * * IP addresses are not considered local. Use the IpAddressClassifier for those. - * - * @param string $hostname - * - * @return bool */ public function isLocalHostname(string $hostname): bool { // Disallow local network top-level domains from RFC 6762 diff --git a/lib/private/Net/IpAddressClassifier.php b/lib/private/Net/IpAddressClassifier.php index d4698864ec8..b012ca8e956 100644 --- a/lib/private/Net/IpAddressClassifier.php +++ b/lib/private/Net/IpAddressClassifier.php @@ -46,10 +46,6 @@ class IpAddressClassifier { * Check host identifier for local IPv4 and IPv6 address ranges * * Hostnames are not considered local. Use the HostnameClassifier for those. - * - * @param string $ip - * - * @return bool */ public function isLocalAddress(string $ip): bool { $parsedIp = Factory::parseAddressString( diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php new file mode 100644 index 00000000000..084d4f8479d --- /dev/null +++ b/lib/private/OCM/Model/OCMProvider.php @@ -0,0 +1,234 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\OCM\Model; + +use OCP\EventDispatcher\IEventDispatcher; +use OCP\OCM\Events\ResourceTypeRegisterEvent; +use OCP\OCM\Exceptions\OCMArgumentException; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMProvider; +use OCP\OCM\IOCMResource; + +/** + * @since 28.0.0 + */ +class OCMProvider implements IOCMProvider { + private bool $enabled = false; + private string $apiVersion = ''; + private string $endPoint = ''; + /** @var IOCMResource[] */ + private array $resourceTypes = []; + + private bool $emittedEvent = false; + + public function __construct( + protected IEventDispatcher $dispatcher, + ) { + } + + /** + * @param bool $enabled + * + * @return $this + */ + public function setEnabled(bool $enabled): static { + $this->enabled = $enabled; + + return $this; + } + + /** + * @return bool + */ + public function isEnabled(): bool { + return $this->enabled; + } + + /** + * @param string $apiVersion + * + * @return $this + */ + public function setApiVersion(string $apiVersion): static { + $this->apiVersion = $apiVersion; + + return $this; + } + + /** + * @return string + */ + public function getApiVersion(): string { + return $this->apiVersion; + } + + /** + * @param string $endPoint + * + * @return $this + */ + public function setEndPoint(string $endPoint): static { + $this->endPoint = $endPoint; + + return $this; + } + + /** + * @return string + */ + public function getEndPoint(): string { + return $this->endPoint; + } + + /** + * create a new resource to later add it with {@see IOCMProvider::addResourceType()} + * @return IOCMResource + */ + public function createNewResourceType(): IOCMResource { + return new OCMResource(); + } + + /** + * @param IOCMResource $resource + * + * @return $this + */ + public function addResourceType(IOCMResource $resource): static { + $this->resourceTypes[] = $resource; + + return $this; + } + + /** + * @param IOCMResource[] $resourceTypes + * + * @return $this + */ + public function setResourceTypes(array $resourceTypes): static { + $this->resourceTypes = $resourceTypes; + + return $this; + } + + /** + * @return IOCMResource[] + */ + public function getResourceTypes(): array { + if (!$this->emittedEvent) { + $this->emittedEvent = true; + $event = new ResourceTypeRegisterEvent($this); + $this->dispatcher->dispatchTyped($event); + } + + return $this->resourceTypes; + } + + /** + * @param string $resourceName + * @param string $protocol + * + * @return string + * @throws OCMArgumentException + */ + public function extractProtocolEntry(string $resourceName, string $protocol): string { + foreach ($this->getResourceTypes() as $resource) { + if ($resource->getName() === $resourceName) { + $entry = $resource->getProtocols()[$protocol] ?? null; + if (is_null($entry)) { + throw new OCMArgumentException('protocol not found'); + } + + return (string)$entry; + } + } + + throw new OCMArgumentException('resource not found'); + } + + /** + * import data from an array + * + * @param array $data + * + * @return $this + * @throws OCMProviderException in case a descent provider cannot be generated from data + * @see self::jsonSerialize() + */ + public function import(array $data): static { + $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) + ->setApiVersion((string)($data['apiVersion'] ?? '')) + ->setEndPoint($data['endPoint'] ?? ''); + + $resources = []; + foreach (($data['resourceTypes'] ?? []) as $resourceData) { + $resource = new OCMResource(); + $resources[] = $resource->import($resourceData); + } + $this->setResourceTypes($resources); + + if (!$this->looksValid()) { + throw new OCMProviderException('remote provider does not look valid'); + } + + return $this; + } + + + /** + * @return bool + */ + private function looksValid(): bool { + return ($this->getApiVersion() !== '' && $this->getEndPoint() !== ''); + } + + + /** + * @return array{ + * enabled: bool, + * apiVersion: string, + * endPoint: string, + * resourceTypes: array{ + * name: string, + * shareTypes: string[], + * protocols: array<string, string> + * }[] + * } + */ + public function jsonSerialize(): array { + $resourceTypes = []; + foreach ($this->getResourceTypes() as $res) { + $resourceTypes[] = $res->jsonSerialize(); + } + + return [ + 'enabled' => $this->isEnabled(), + 'apiVersion' => $this->getApiVersion(), + 'endPoint' => $this->getEndPoint(), + 'resourceTypes' => $resourceTypes + ]; + } +} diff --git a/lib/private/OCM/Model/OCMResource.php b/lib/private/OCM/Model/OCMResource.php new file mode 100644 index 00000000000..c4a91f2eabf --- /dev/null +++ b/lib/private/OCM/Model/OCMResource.php @@ -0,0 +1,123 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\OCM\Model; + +use OCP\OCM\IOCMResource; + +/** + * @since 28.0.0 + */ +class OCMResource implements IOCMResource { + private string $name = ''; + /** @var string[] */ + private array $shareTypes = []; + /** @var array<string, string> */ + private array $protocols = []; + + /** + * @param string $name + * + * @return $this + */ + public function setName(string $name): static { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * @param string[] $shareTypes + * + * @return $this + */ + public function setShareTypes(array $shareTypes): static { + $this->shareTypes = $shareTypes; + + return $this; + } + + /** + * @return string[] + */ + public function getShareTypes(): array { + return $this->shareTypes; + } + + /** + * @param array<string, string> $protocols + * + * @return $this + */ + public function setProtocols(array $protocols): static { + $this->protocols = $protocols; + + return $this; + } + + /** + * @return array<string, string> + */ + public function getProtocols(): array { + return $this->protocols; + } + + /** + * import data from an array + * + * @param array $data + * + * @return $this + * @see self::jsonSerialize() + */ + public function import(array $data): static { + return $this->setName((string)($data['name'] ?? '')) + ->setShareTypes($data['shareTypes'] ?? []) + ->setProtocols($data['protocols'] ?? []); + } + + /** + * @return array{ + * name: string, + * shareTypes: string[], + * protocols: array<string, string> + * } + */ + public function jsonSerialize(): array { + return [ + 'name' => $this->getName(), + 'shareTypes' => $this->getShareTypes(), + 'protocols' => $this->getProtocols() + ]; + } +} diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php new file mode 100644 index 00000000000..ac9bf2a3965 --- /dev/null +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\OCM; + +use JsonException; +use OCP\AppFramework\Http; +use OCP\Http\Client\IClientService; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMDiscoveryService; +use OCP\OCM\IOCMProvider; +use Psr\Log\LoggerInterface; + +/** + * @since 28.0.0 + */ +class OCMDiscoveryService implements IOCMDiscoveryService { + private ICache $cache; + private array $supportedAPIVersion = + [ + '1.0-proposal1', + '1.0', + '1.1' + ]; + + public function __construct( + ICacheFactory $cacheFactory, + private IClientService $clientService, + private IConfig $config, + private IOCMProvider $provider, + private LoggerInterface $logger, + ) { + $this->cache = $cacheFactory->createDistributed('ocm-discovery'); + } + + + /** + * @param string $remote + * @param bool $skipCache + * + * @return IOCMProvider + * @throws OCMProviderException + */ + public function discover(string $remote, bool $skipCache = false): IOCMProvider { + $remote = rtrim($remote, '/'); + + if (!$skipCache) { + try { + $this->provider->import(json_decode($this->cache->get($remote) ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []); + if ($this->supportedAPIVersion($this->provider->getApiVersion())) { + return $this->provider; // if cache looks valid, we use it + } + } catch (JsonException|OCMProviderException $e) { + // we ignore cache on issues + } + } + + $client = $this->clientService->newClient(); + try { + $response = $client->get( + $remote . '/ocm-provider/', + [ + 'timeout' => 10, + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates'), + 'connect_timeout' => 10, + ] + ); + + if ($response->getStatusCode() === Http::STATUS_OK) { + $body = $response->getBody(); + // update provider with data returned by the request + $this->provider->import(json_decode($body, true, 8, JSON_THROW_ON_ERROR) ?? []); + $this->cache->set($remote, $body, 60 * 60 * 24); + } + } catch (JsonException|OCMProviderException $e) { + throw new OCMProviderException('data returned by remote seems invalid - ' . ($body ?? '')); + } catch (\Exception $e) { + $this->logger->warning('error while discovering ocm provider', [ + 'exception' => $e, + 'remote' => $remote + ]); + throw new OCMProviderException('error while requesting remote ocm provider'); + } + + if (!$this->supportedAPIVersion($this->provider->getApiVersion())) { + throw new OCMProviderException('API version not supported'); + } + + return $this->provider; + } + + /** + * Check the version from remote is supported. + * The minor version of the API will be ignored: + * 1.0.1 is identified as 1.0 + * + * @param string $version + * + * @return bool + */ + private function supportedAPIVersion(string $version): bool { + $dot1 = strpos($version, '.'); + $dot2 = strpos($version, '.', $dot1 + 1); + + if ($dot2 > 0) { + $version = substr($version, 0, $dot2); + } + + return (in_array($version, $this->supportedAPIVersion)); + } +} diff --git a/lib/private/PhoneNumberUtil.php b/lib/private/PhoneNumberUtil.php new file mode 100644 index 00000000000..a1eb2f13233 --- /dev/null +++ b/lib/private/PhoneNumberUtil.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC; + +use libphonenumber\NumberParseException; +use libphonenumber\PhoneNumberFormat; +use OCP\IPhoneNumberUtil; + +/** + * @since 28.0.0 + */ +class PhoneNumberUtil implements IPhoneNumberUtil { + /** + * {@inheritDoc} + */ + public function getCountryCodeForRegion(string $regionCode): ?int { + $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); + $countryCode = $phoneUtil->getCountryCodeForRegion($regionCode); + return $countryCode === 0 ? null : $countryCode; + } + + /** + * {@inheritDoc} + */ + public function convertToStandardFormat(string $input, ?string $defaultRegion = null): ?string { + $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); + try { + $phoneNumber = $phoneUtil->parse($input, $defaultRegion); + if ($phoneUtil->isValidNumber($phoneNumber)) { + return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164); + } + } catch (NumberParseException) { + } + + return null; + } +} diff --git a/lib/private/Preview/EMF.php b/lib/private/Preview/EMF.php new file mode 100644 index 00000000000..2b5f40e66af --- /dev/null +++ b/lib/private/Preview/EMF.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Daniel Kesselberg <mail@danielkesselberg.de> + * + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Preview; + +class EMF extends Office { + public function getMimeType(): string { + return '/image\/emf/'; + } +} diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 4e4571f0857..695d4a3357f 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -139,23 +139,6 @@ class Generator { $previewVersion = $file->getPreviewVersion() . '-'; } - // If imaginary is enabled, and we request a small thumbnail, - // let's not generate the max preview for performance reasons - if (count($specifications) === 1 - && ($specifications[0]['width'] <= 256 || $specifications[0]['height'] <= 256) - && preg_match(Imaginary::supportedMimeTypes(), $mimeType) - && $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') { - $crop = $specifications[0]['crop'] ?? false; - $preview = $this->getSmallImagePreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion, $crop); - - if ($preview->getSize() === 0) { - $preview->delete(); - throw new NotFoundException('Cached preview size 0, invalid!'); - } - - return $preview; - } - // Get the max preview and infer the max preview sizes from that $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion); $maxPreviewImage = null; // only load the image when we need it @@ -232,32 +215,13 @@ class Generator { } /** - * Generate a small image straight away without generating a max preview first - * Preview generated is 256x256 - * - * @param ISimpleFile[] $previewFiles - * - * @throws NotFoundException - */ - private function getSmallImagePreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile { - $width = 256; - $height = 256; - - try { - return $this->getCachedPreview($previewFiles, $width, $height, $crop, $mimeType, $prefix); - } catch (NotFoundException $e) { - return $this->generateProviderPreview($previewFolder, $file, $width, $height, $crop, false, $mimeType, $prefix); - } - } - - /** * Acquire a semaphore of the specified id and concurrency, blocking if necessary. * Return an identifier of the semaphore on success, which can be used to release it via * {@see Generator::unguardWithSemaphore()}. * * @param int $semId * @param int $concurrency - * @return false|resource the semaphore on success or false on failure + * @return false|\SysvSemaphore the semaphore on success or false on failure */ public static function guardWithSemaphore(int $semId, int $concurrency) { if (!extension_loaded('sysvsem')) { @@ -276,11 +240,11 @@ class Generator { /** * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}. * - * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore + * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore * @return bool */ - public static function unguardWithSemaphore($semId): bool { - if (!is_resource($semId) || !extension_loaded('sysvsem')) { + public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool { + if ($semId === false || !($semId instanceof \SysvSemaphore)) { return false; } return sem_release($semId); @@ -293,9 +257,15 @@ class Generator { */ public static function getHardwareConcurrency(): int { static $width; + if (!isset($width)) { - if (is_file("/proc/cpuinfo")) { - $width = substr_count(file_get_contents("/proc/cpuinfo"), "processor"); + if (function_exists('ini_get')) { + $openBasedir = ini_get('open_basedir'); + if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) { + $width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0; + } else { + $width = 0; + } } else { $width = 0; } @@ -651,6 +621,8 @@ class Generator { return 'png'; case 'image/jpeg': return 'jpg'; + case 'image/webp': + return 'webp'; case 'image/gif': return 'gif'; default: diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index da4864b1a22..ae2752dd91c 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -78,6 +78,9 @@ class Imaginary extends ProviderV2 { // Object store $stream = $file->fopen('r'); + if (!$stream || !is_resource($stream) || feof($stream)) { + return null; + } $httpClient = $this->service->newClient(); @@ -106,6 +109,15 @@ class Imaginary extends ProviderV2 { $mimeType = 'jpeg'; } + $preview_format = $this->config->getSystemValueString('preview_format', 'jpeg'); + + switch ($preview_format) { // Change the format to the correct one + case 'webp': + $mimeType = 'webp'; + break; + default: + } + $operations = []; if ($convert) { @@ -121,7 +133,16 @@ class Imaginary extends ProviderV2 { ]; } - $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + switch ($mimeType) { + case 'jpeg': + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + break; + case 'webp': + $quality = $this->config->getAppValue('preview', 'webp_quality', '80'); + break; + default: + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + } $operations[] = [ 'operation' => ($crop ? 'smartcrop' : 'fit'), @@ -147,7 +168,7 @@ class Imaginary extends ProviderV2 { 'timeout' => 120, 'connect_timeout' => 3, ]); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->logger->info('Imaginary preview generation failed: ' . $e->getMessage(), [ 'exception' => $e, ]); diff --git a/lib/private/Preview/Office.php b/lib/private/Preview/Office.php index 3ba7c5a21a0..68499a6fea6 100644 --- a/lib/private/Preview/Office.php +++ b/lib/private/Preview/Office.php @@ -31,7 +31,8 @@ namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; -use Psr\Log\LoggerInterface; +use OCP\ITempManager; +use OCP\Server; abstract class Office extends ProviderV2 { /** @@ -49,51 +50,60 @@ abstract class Office extends ProviderV2 { return null; } - $absPath = $this->getLocalFile($file); - - $tmpDir = \OC::$server->getTempManager()->getTempBaseDir(); + $tempManager = Server::get(ITempManager::class); - $defaultParameters = ' -env:UserInstallation=file://' . escapeshellarg($tmpDir . '/owncloud-' . \OC_Util::getInstanceId() . '/') . ' --headless --nologo --nofirststartwizard --invisible --norestore --convert-to png --outdir '; - $clParameters = \OC::$server->getConfig()->getSystemValue('preview_office_cl_parameters', $defaultParameters); + // The file to generate the preview for. + $absPath = $this->getLocalFile($file); - $cmd = $this->options['officeBinary'] . $clParameters . escapeshellarg($tmpDir) . ' ' . escapeshellarg($absPath); + // The destination for the LibreOffice user profile. + // LibreOffice can rune once per user profile and therefore instance id and file id are included. + $profile = $tempManager->getTemporaryFolder( + 'nextcloud-office-profile-' . \OC_Util::getInstanceId() . '-' . $file->getId() + ); - exec($cmd, $output, $returnCode); + // The destination for the LibreOffice convert result. + $outdir = $tempManager->getTemporaryFolder( + 'nextcloud-office-preview-' . \OC_Util::getInstanceId() . '-' . $file->getId() + ); - if ($returnCode !== 0) { + if ($profile === false || $outdir === false) { $this->cleanTmpFiles(); return null; } - //create imagick object from png - $pngPreview = null; - try { - [$dirname, , , $filename] = array_values(pathinfo($absPath)); - $pngPreview = $tmpDir . '/' . $filename . '.png'; + $parameters = [ + $this->options['officeBinary'], + '-env:UserInstallation=file://' . escapeshellarg($profile), + '--headless', + '--nologo', + '--nofirststartwizard', + '--invisible', + '--norestore', + '--convert-to png', + '--outdir ' . escapeshellarg($outdir), + escapeshellarg($absPath), + ]; - $png = new \Imagick($pngPreview . '[0]'); - $png->setImageFormat('jpg'); - } catch (\Exception $e) { + $cmd = implode(' ', $parameters); + exec($cmd, $output, $returnCode); + + if ($returnCode !== 0) { $this->cleanTmpFiles(); - unlink($pngPreview); - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [ - 'exception' => $e, - 'app' => 'core', - ]); return null; } + $preview = $outdir . pathinfo($absPath, PATHINFO_FILENAME) . '.png'; + $image = new \OCP\Image(); - $image->loadFromData((string) $png); + $image->loadFromFile($preview); $this->cleanTmpFiles(); - unlink($pngPreview); if ($image->valid()) { $image->scaleDownToFit($maxX, $maxY); - return $image; } + return null; } } diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index 3af6848686e..aedcbbce335 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -366,7 +366,7 @@ class PreviewManager implements IPreview { $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/'); $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes()); - // SVG, Office and Bitmap require imagick + // SVG and Bitmap require imagick if ($this->imagickSupport->hasExtension()) { $imagickProviders = [ 'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class], @@ -391,27 +391,10 @@ class PreviewManager implements IPreview { $this->registerCoreProvider($class, $provider['mimetype']); } } - - if ($this->imagickSupport->supportsFormat('PDF')) { - // Office requires openoffice or libreoffice - $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', null); - if (!is_string($officeBinary)) { - $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice'); - } - if (!is_string($officeBinary)) { - $officeBinary = $this->binaryFinder->findBinaryPath('openoffice'); - } - - if (is_string($officeBinary)) { - $this->registerCoreProvider(Preview\MSOfficeDoc::class, '/application\/msword/', ["officeBinary" => $officeBinary]); - $this->registerCoreProvider(Preview\MSOffice2003::class, '/application\/vnd.ms-.*/', ["officeBinary" => $officeBinary]); - $this->registerCoreProvider(Preview\MSOffice2007::class, '/application\/vnd.openxmlformats-officedocument.*/', ["officeBinary" => $officeBinary]); - $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/', ["officeBinary" => $officeBinary]); - $this->registerCoreProvider(Preview\StarOffice::class, '/application\/vnd.sun.xml.*/', ["officeBinary" => $officeBinary]); - } - } } + $this->registerCoreProvidersOffice(); + // Video requires avconv or ffmpeg if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) { $movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null); @@ -429,6 +412,43 @@ class PreviewManager implements IPreview { } } + private function registerCoreProvidersOffice(): void { + $officeProviders = [ + ['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class], + ['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class], + ['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class], + ['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class], + ['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class], + ['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class], + ]; + + $findBinary = true; + $officeBinary = false; + + foreach ($officeProviders as $provider) { + $class = $provider['class']; + if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { + continue; + } + + if ($findBinary) { + // Office requires openoffice or libreoffice + $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false); + if ($officeBinary === false) { + $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice'); + } + if ($officeBinary === false) { + $officeBinary = $this->binaryFinder->findBinaryPath('openoffice'); + } + $findBinary = false; + } + + if ($officeBinary) { + $this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]); + } + } + } + private function registerBootstrapProviders(): void { $context = $this->bootstrapCoordinator->getRegistrationContext(); diff --git a/lib/private/Profile/Actions/EmailAction.php b/lib/private/Profile/Actions/EmailAction.php index 8ab4939b515..a676f6e228e 100644 --- a/lib/private/Profile/Actions/EmailAction.php +++ b/lib/private/Profile/Actions/EmailAction.php @@ -33,26 +33,13 @@ use OCP\L10N\IFactory; use OCP\Profile\ILinkAction; class EmailAction implements ILinkAction { - /** @var string */ - private $value; - - /** @var IAccountManager */ - private $accountManager; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IUrlGenerator */ - private $urlGenerator; + private string $value = ''; public function __construct( - IAccountManager $accountManager, - IFactory $l10nFactory, - IURLGenerator $urlGenerator + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, ) { - $this->accountManager = $accountManager; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; } public function preload(IUser $targetUser): void { diff --git a/lib/private/Profile/Actions/FediverseAction.php b/lib/private/Profile/Actions/FediverseAction.php index ed3fcd80b52..f96d2c07de4 100644 --- a/lib/private/Profile/Actions/FediverseAction.php +++ b/lib/private/Profile/Actions/FediverseAction.php @@ -26,7 +26,7 @@ declare(strict_types=1); namespace OC\Profile\Actions; -use function Safe\substr; +use function substr; use OCP\Accounts\IAccountManager; use OCP\IURLGenerator; use OCP\IUser; @@ -34,19 +34,13 @@ use OCP\L10N\IFactory; use OCP\Profile\ILinkAction; class FediverseAction implements ILinkAction { - private ?string $value = null; - private IAccountManager $accountManager; - private IFactory $l10nFactory; - private IURLGenerator $urlGenerator; + private string $value = ''; public function __construct( - IAccountManager $accountManager, - IFactory $l10nFactory, - IURLGenerator $urlGenerator + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, ) { - $this->accountManager = $accountManager; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; } public function preload(IUser $targetUser): void { diff --git a/lib/private/Profile/Actions/PhoneAction.php b/lib/private/Profile/Actions/PhoneAction.php index 6081a04ad7e..6a4b2dd49d4 100644 --- a/lib/private/Profile/Actions/PhoneAction.php +++ b/lib/private/Profile/Actions/PhoneAction.php @@ -33,26 +33,13 @@ use OCP\L10N\IFactory; use OCP\Profile\ILinkAction; class PhoneAction implements ILinkAction { - /** @var string */ - private $value; - - /** @var IAccountManager */ - private $accountManager; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IUrlGenerator */ - private $urlGenerator; + private string $value = ''; public function __construct( - IAccountManager $accountManager, - IFactory $l10nFactory, - IURLGenerator $urlGenerator + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, ) { - $this->accountManager = $accountManager; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; } public function preload(IUser $targetUser): void { diff --git a/lib/private/Profile/Actions/TwitterAction.php b/lib/private/Profile/Actions/TwitterAction.php index 041da42e539..d63c2d3ee08 100644 --- a/lib/private/Profile/Actions/TwitterAction.php +++ b/lib/private/Profile/Actions/TwitterAction.php @@ -26,7 +26,7 @@ declare(strict_types=1); namespace OC\Profile\Actions; -use function Safe\substr; +use function substr; use OCP\Accounts\IAccountManager; use OCP\IURLGenerator; use OCP\IUser; @@ -34,26 +34,13 @@ use OCP\L10N\IFactory; use OCP\Profile\ILinkAction; class TwitterAction implements ILinkAction { - /** @var string */ - private $value; - - /** @var IAccountManager */ - private $accountManager; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IUrlGenerator */ - private $urlGenerator; + private string $value = ''; public function __construct( - IAccountManager $accountManager, - IFactory $l10nFactory, - IURLGenerator $urlGenerator + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, ) { - $this->accountManager = $accountManager; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; } public function preload(IUser $targetUser): void { diff --git a/lib/private/Profile/Actions/WebsiteAction.php b/lib/private/Profile/Actions/WebsiteAction.php index 6b052be57bd..22e2692c4c5 100644 --- a/lib/private/Profile/Actions/WebsiteAction.php +++ b/lib/private/Profile/Actions/WebsiteAction.php @@ -33,26 +33,13 @@ use OCP\L10N\IFactory; use OCP\Profile\ILinkAction; class WebsiteAction implements ILinkAction { - /** @var string */ - private $value; - - /** @var IAccountManager */ - private $accountManager; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IUrlGenerator */ - private $urlGenerator; + private string $value = ''; public function __construct( - IAccountManager $accountManager, - IFactory $l10nFactory, - IURLGenerator $urlGenerator + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, ) { - $this->accountManager = $accountManager; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; } public function preload(IUser $targetUser): void { diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php index f20ae74768e..39c51ea0e77 100644 --- a/lib/private/Profile/ProfileManager.php +++ b/lib/private/Profile/ProfileManager.php @@ -26,8 +26,9 @@ declare(strict_types=1); namespace OC\Profile; -use function Safe\array_flip; -use function Safe\usort; +use OCP\Profile\IProfileManager; +use function array_flip; +use function usort; use OC\AppFramework\Bootstrap\Coordinator; use OC\Core\Db\ProfileConfig; use OC\Core\Db\ProfileConfigMapper; @@ -49,39 +50,12 @@ use OCP\Cache\CappedMemoryCache; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -class ProfileManager { - /** @var IAccountManager */ - private $accountManager; - - /** @var IAppManager */ - private $appManager; - - /** @var IConfig */ - private $config; - - /** @var ProfileConfigMapper */ - private $configMapper; - - /** @var ContainerInterface */ - private $container; - - /** @var KnownUserService */ - private $knownUserService; - - /** @var IFactory */ - private $l10nFactory; - - /** @var LoggerInterface */ - private $logger; - - /** @var Coordinator */ - private $coordinator; - +class ProfileManager implements IProfileManager { /** @var ILinkAction[] */ - private $actions = []; + private array $actions = []; /** @var null|ILinkAction[] */ - private $sortedActions = null; + private ?array $sortedActions = null; /** @var CappedMemoryCache<ProfileConfig> */ private CappedMemoryCache $configCache; @@ -112,32 +86,23 @@ class ProfileManager { ]; public function __construct( - IAccountManager $accountManager, - IAppManager $appManager, - IConfig $config, - ProfileConfigMapper $configMapper, - ContainerInterface $container, - KnownUserService $knownUserService, - IFactory $l10nFactory, - LoggerInterface $logger, - Coordinator $coordinator + private IAccountManager $accountManager, + private IAppManager $appManager, + private IConfig $config, + private ProfileConfigMapper $configMapper, + private ContainerInterface $container, + private KnownUserService $knownUserService, + private IFactory $l10nFactory, + private LoggerInterface $logger, + private Coordinator $coordinator, ) { - $this->accountManager = $accountManager; - $this->appManager = $appManager; - $this->config = $config; - $this->configMapper = $configMapper; - $this->container = $container; - $this->knownUserService = $knownUserService; - $this->l10nFactory = $l10nFactory; - $this->logger = $logger; - $this->coordinator = $coordinator; $this->configCache = new CappedMemoryCache(); } /** * If no user is passed as an argument return whether profile is enabled globally in `config.php` */ - public function isProfileEnabled(?IUser $user = null): ?bool { + public function isProfileEnabled(?IUser $user = null): bool { $profileEnabledGlobally = $this->config->getSystemValueBool('profile.enabled', true); if (empty($user) || !$profileEnabledGlobally) { @@ -145,7 +110,7 @@ class ProfileManager { } $account = $this->accountManager->getAccount($user); - return filter_var( + return (bool) filter_var( $account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE, @@ -229,57 +194,54 @@ class ProfileManager { * Return whether the profile parameter of the target user * is visible to the visiting user */ - private function isParameterVisible(string $paramId, IUser $targetUser, ?IUser $visitingUser): bool { + public function isProfileFieldVisible(string $profileField, IUser $targetUser, ?IUser $visitingUser): bool { try { $account = $this->accountManager->getAccount($targetUser); - $scope = $account->getProperty($paramId)->getScope(); + $scope = $account->getProperty($profileField)->getScope(); } catch (PropertyDoesNotExistException $e) { // Allow the exception as not all profile parameters are account properties } - $visibility = $this->getProfileConfig($targetUser, $visitingUser)[$paramId]['visibility']; + $visibility = $this->getProfileConfig($targetUser, $visitingUser)[$profileField]['visibility']; // Handle profile visibility and account property scope - switch ($visibility) { - case ProfileConfig::VISIBILITY_HIDE: - return false; - case ProfileConfig::VISIBILITY_SHOW_USERS_ONLY: - if (!empty($scope)) { - switch ($scope) { - case IAccountManager::SCOPE_PRIVATE: - return $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()); - case IAccountManager::SCOPE_LOCAL: - case IAccountManager::SCOPE_FEDERATED: - case IAccountManager::SCOPE_PUBLISHED: - return $visitingUser !== null; - default: - return false; - } - } + + if ($visibility === self::VISIBILITY_SHOW_USERS_ONLY) { + if (empty($scope)) { return $visitingUser !== null; - case ProfileConfig::VISIBILITY_SHOW: - if (!empty($scope)) { - switch ($scope) { - case IAccountManager::SCOPE_PRIVATE: - return $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()); - case IAccountManager::SCOPE_LOCAL: - case IAccountManager::SCOPE_FEDERATED: - case IAccountManager::SCOPE_PUBLISHED: - return true; - default: - return false; - } - } + } + + return match ($scope) { + IAccountManager::SCOPE_PRIVATE => $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()), + IAccountManager::SCOPE_LOCAL, + IAccountManager::SCOPE_FEDERATED, + IAccountManager::SCOPE_PUBLISHED => $visitingUser !== null, + default => false, + }; + } + + if ($visibility === self::VISIBILITY_SHOW) { + if (empty($scope)) { return true; - default: - return false; + } + + return match ($scope) { + IAccountManager::SCOPE_PRIVATE => $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()), + IAccountManager::SCOPE_LOCAL, + IAccountManager::SCOPE_FEDERATED, + IAccountManager::SCOPE_PUBLISHED => true, + default => false, + }; } + + return false; } /** * Return the profile parameters of the target user that are visible to the visiting user * in an associative array + * @return array{userId: string, address?: string|null, biography?: string|null, displayname?: string|null, headline?: string|null, isUserAvatarVisible?: bool, organisation?: string|null, role?: string|null, actions: list<array{id: string, icon: string, title: string, target: ?string}>} */ - public function getProfileParams(IUser $targetUser, ?IUser $visitingUser): array { + public function getProfileFields(IUser $targetUser, ?IUser $visitingUser): array { $account = $this->accountManager->getAccount($targetUser); // Initialize associative array of profile parameters @@ -297,14 +259,14 @@ class ProfileManager { case IAccountManager::PROPERTY_ORGANISATION: case IAccountManager::PROPERTY_ROLE: $profileParameters[$property] = - $this->isParameterVisible($property, $targetUser, $visitingUser) + $this->isProfileFieldVisible($property, $targetUser, $visitingUser) // Explicitly set to null when value is empty string ? ($account->getProperty($property)->getValue() ?: null) : null; break; case IAccountManager::PROPERTY_AVATAR: // Add avatar visibility - $profileParameters['isUserAvatarVisible'] = $this->isParameterVisible($property, $targetUser, $visitingUser); + $profileParameters['isUserAvatarVisible'] = $this->isProfileFieldVisible($property, $targetUser, $visitingUser); break; } } @@ -324,7 +286,7 @@ class ProfileManager { array_filter( $this->getActions($targetUser, $visitingUser), function (ILinkAction $action) use ($targetUser, $visitingUser) { - return $this->isParameterVisible($action->getId(), $targetUser, $visitingUser); + return $this->isProfileFieldVisible($action->getId(), $targetUser, $visitingUser); } ), ) @@ -356,12 +318,12 @@ class ProfileManager { // Construct the default config for actions $actionsConfig = []; foreach ($this->getActions($targetUser, $visitingUser) as $action) { - $actionsConfig[$action->getId()] = ['visibility' => ProfileConfig::DEFAULT_VISIBILITY]; + $actionsConfig[$action->getId()] = ['visibility' => self::DEFAULT_VISIBILITY]; } // Construct the default config for account properties $propertiesConfig = []; - foreach (ProfileConfig::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) { + foreach (self::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) { $propertiesConfig[$property] = ['visibility' => $visibility]; } diff --git a/lib/private/Remote/User.php b/lib/private/Remote/User.php index 5590fcfba38..d67b279bccb 100644 --- a/lib/private/Remote/User.php +++ b/lib/private/Remote/User.php @@ -92,7 +92,7 @@ class User implements IUser { * @return string */ public function getTwitter() { - return isset($this->data['twitter']) ? $this->data['twitter'] : ''; + return $this->data['twitter'] ?? ''; } /** diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 05624a2423a..a12e00f071c 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -46,6 +46,7 @@ use OC\DB\Connection; use OC\DB\ConnectionAdapter; use OC\Repair\AddBruteForceCleanupJob; use OC\Repair\AddCleanupUpdaterBackupsJob; +use OC\Repair\AddMetadataGenerationJob; use OC\Repair\CleanTags; use OC\Repair\ClearFrontendCaches; use OC\Repair\ClearGeneratedAvatarCache; @@ -84,7 +85,6 @@ use OC\Repair\RemoveLinkShares; use OC\Repair\RepairDavShares; use OC\Repair\RepairInvalidShares; use OC\Repair\RepairMimeTypes; -use OC\Repair\SqliteAutoincrement; use OC\Template\JSCombiner; use Psr\Log\LoggerInterface; use Throwable; @@ -212,6 +212,7 @@ class Repair implements IOutput { \OCP\Server::get(CleanUpAbandonedApps::class), \OCP\Server::get(AddMissingSecretJob::class), \OCP\Server::get(AddRemoveOldTasksBackgroundJob::class), + \OCP\Server::get(AddMetadataGenerationJob::class), ]; } @@ -235,14 +236,11 @@ class Repair implements IOutput { * @return IRepairStep[] */ public static function getBeforeUpgradeRepairSteps() { - /** @var Connection $connection */ - $connection = \OC::$server->get(Connection::class); /** @var ConnectionAdapter $connectionAdapter */ $connectionAdapter = \OC::$server->get(ConnectionAdapter::class); $config = \OC::$server->getConfig(); $steps = [ new Collation(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), $connectionAdapter, true), - new SqliteAutoincrement($connection), new SaveAccountsTableData($connectionAdapter, $config), new DropAccountTermsTable($connectionAdapter), ]; @@ -250,6 +248,9 @@ class Repair implements IOutput { return $steps; } + public function debug(string $message): void { + } + /** * @param string $message */ diff --git a/lib/private/Repair/AddMetadataGenerationJob.php b/lib/private/Repair/AddMetadataGenerationJob.php new file mode 100644 index 00000000000..72e5df03bbd --- /dev/null +++ b/lib/private/Repair/AddMetadataGenerationJob.php @@ -0,0 +1,43 @@ +<?php +/** + * @copyright Copyright (c) 2023 Louis Chmn <louis@chmn.me> + * + * @author Louis Chmn <louis@chmn.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Repair; + +use OC\Core\BackgroundJobs\GenerateMetadataJob; +use OCP\BackgroundJob\IJobList; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class AddMetadataGenerationJob implements IRepairStep { + public function __construct( + private IJobList $jobList, + ) { + } + + public function getName() { + return 'Queue a job to generate metadata'; + } + + public function run(IOutput $output) { + $this->jobList->add(GenerateMetadataJob::class); + } +} diff --git a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php index 94ae39f2183..00badbb726d 100644 --- a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php +++ b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php @@ -25,7 +25,8 @@ declare(strict_types=1); */ namespace OC\Repair; -use OC\TextProcessing\RemoveOldTasksBackgroundJob; +use OC\TextProcessing\RemoveOldTasksBackgroundJob as RemoveOldTextProcessingTasksBackgroundJob; +use OC\TextToImage\RemoveOldTasksBackgroundJob as RemoveOldTextToImageTasksBackgroundJob; use OCP\BackgroundJob\IJobList; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -38,10 +39,11 @@ class AddRemoveOldTasksBackgroundJob implements IRepairStep { } public function getName(): string { - return 'Add language model tasks cleanup job'; + return 'Add AI tasks cleanup job'; } public function run(IOutput $output) { - $this->jobList->add(RemoveOldTasksBackgroundJob::class); + $this->jobList->add(RemoveOldTextProcessingTasksBackgroundJob::class); + $this->jobList->add(RemoveOldTextToImageTasksBackgroundJob::class); } } diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php index ee5a84ad65c..cab8d8ca02a 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -229,6 +229,13 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } + private function introduceEnhancedMetafileFormatType() { + $updatedMimetypes = [ + 'emf' => 'image/emf', + ]; + + return $this->updateMimetypes($updatedMimetypes); + } /** * Fix mime types @@ -286,5 +293,9 @@ class RepairMimeTypes implements IRepairStep { if (version_compare($ocVersionFromBeforeUpdate, '26.0.0.1', '<') && $this->introduceAsciidocType()) { $out->info('Fixed AsciiDoc mime types'); } + + if (version_compare($ocVersionFromBeforeUpdate, '28.0.0.5', '<') && $this->introduceEnhancedMetafileFormatType()) { + $out->info('Fixed Enhanced Metafile Format mime types'); + } } } diff --git a/lib/private/Repair/SqliteAutoincrement.php b/lib/private/Repair/SqliteAutoincrement.php deleted file mode 100644 index 4a8b2a45d3f..00000000000 --- a/lib/private/Repair/SqliteAutoincrement.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Repair; - -use Doctrine\DBAL\Platforms\SqlitePlatform; -use Doctrine\DBAL\Schema\ColumnDiff; -use Doctrine\DBAL\Schema\SchemaDiff; -use Doctrine\DBAL\Schema\SchemaException; -use Doctrine\DBAL\Schema\TableDiff; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -/** - * Fixes Sqlite autoincrement by forcing the SQLite table schemas to be - * altered in order to retrigger SQL schema generation through OCSqlitePlatform. - */ -class SqliteAutoincrement implements IRepairStep { - /** - * @var \OC\DB\Connection - */ - protected $connection; - - /** - * @param \OC\DB\Connection $connection - */ - public function __construct($connection) { - $this->connection = $connection; - } - - public function getName() { - return 'Repair SQLite autoincrement'; - } - - /** - * Fix mime types - */ - public function run(IOutput $out) { - if (!$this->connection->getDatabasePlatform() instanceof SqlitePlatform) { - return; - } - - $sourceSchema = $this->connection->getSchemaManager()->createSchema(); - - $schemaDiff = new SchemaDiff(); - - foreach ($sourceSchema->getTables() as $tableSchema) { - $primaryKey = $tableSchema->getPrimaryKey(); - if (!$primaryKey) { - continue; - } - - $columnNames = $primaryKey->getColumns(); - - // add a column diff for every primary key column, - // but do not actually change anything, this will - // force the generation of SQL statements to alter - // those tables, which will then trigger the - // specific SQL code from OCSqlitePlatform - try { - $tableDiff = new TableDiff($tableSchema->getName()); - $tableDiff->fromTable = $tableSchema; - foreach ($columnNames as $columnName) { - $columnSchema = $tableSchema->getColumn($columnName); - $columnDiff = new ColumnDiff($columnSchema->getName(), $columnSchema); - $tableDiff->changedColumns[$columnSchema->getName()] = $columnDiff; - $schemaDiff->changedTables[] = $tableDiff; - } - } catch (SchemaException $e) { - // ignore - } - } - - $this->connection->beginTransaction(); - foreach ($schemaDiff->toSql($this->connection->getDatabasePlatform()) as $sql) { - $this->connection->query($sql); - } - $this->connection->commit(); - } -} diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index fe97623176d..5ce6c7c5c8f 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -247,23 +247,23 @@ class Router implements IRouter { */ public function findMatchingRoute(string $url): array { $this->eventLogger->start('route:match', 'Match route'); - if (substr($url, 0, 6) === '/apps/') { + if (str_starts_with($url, '/apps/')) { // empty string / 'apps' / $app / rest of the route [, , $app,] = explode('/', $url, 4); $app = \OC_App::cleanAppId($app); \OC::$REQUESTEDAPP = $app; $this->loadRoutes($app); - } elseif (substr($url, 0, 13) === '/ocsapp/apps/') { + } elseif (str_starts_with($url, '/ocsapp/apps/')) { // empty string / 'ocsapp' / 'apps' / $app / rest of the route [, , , $app,] = explode('/', $url, 5); $app = \OC_App::cleanAppId($app); \OC::$REQUESTEDAPP = $app; $this->loadRoutes($app); - } elseif (substr($url, 0, 10) === '/settings/') { + } elseif (str_starts_with($url, '/settings/')) { $this->loadRoutes('settings'); - } elseif (substr($url, 0, 6) === '/core/') { + } elseif (str_starts_with($url, '/core/')) { \OC::$REQUESTEDAPP = $url; if (!$this->config->getSystemValueBool('maintenance') && !Util::needUpgrade()) { \OC_App::loadApps(); @@ -277,7 +277,7 @@ class Router implements IRouter { try { $parameters = $matcher->match($url); } catch (ResourceNotFoundException $e) { - if (substr($url, -1) !== '/') { + if (!str_ends_with($url, '/')) { // We allow links to apps/files? for backwards compatibility reasons // However, since Symfony does not allow empty route names, the route // we need to match is '/', so we need to append the '/' here. diff --git a/lib/private/Search/Filter/BooleanFilter.php b/lib/private/Search/Filter/BooleanFilter.php new file mode 100644 index 00000000000..a64bf17f31c --- /dev/null +++ b/lib/private/Search/Filter/BooleanFilter.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class BooleanFilter implements IFilter { + private bool $value; + + public function __construct(string $value) { + $this->value = match ($value) { + 'true', 'yes', 'y', '1' => true, + 'false', 'no', 'n', '0', '' => false, + default => throw new InvalidArgumentException('Invalid boolean value '. $value), + }; + } + + public function get(): bool { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/DateTimeFilter.php b/lib/private/Search/Filter/DateTimeFilter.php new file mode 100644 index 00000000000..79abf9ad542 --- /dev/null +++ b/lib/private/Search/Filter/DateTimeFilter.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use DateTimeImmutable; +use OCP\Search\IFilter; + +class DateTimeFilter implements IFilter { + private DateTimeImmutable $value; + + public function __construct(string $value) { + if (filter_var($value, FILTER_VALIDATE_INT)) { + $value = '@'.$value; + } + + $this->value = new DateTimeImmutable($value); + } + + public function get(): DateTimeImmutable { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/FloatFilter.php b/lib/private/Search/Filter/FloatFilter.php new file mode 100644 index 00000000000..3db19ded59b --- /dev/null +++ b/lib/private/Search/Filter/FloatFilter.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class FloatFilter implements IFilter { + private float $value; + + public function __construct(string $value) { + $this->value = filter_var($value, FILTER_VALIDATE_FLOAT); + if ($this->value === false) { + throw new InvalidArgumentException('Invalid float value '. $value); + } + } + + public function get(): float { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/GroupFilter.php b/lib/private/Search/Filter/GroupFilter.php new file mode 100644 index 00000000000..f0b34a360ca --- /dev/null +++ b/lib/private/Search/Filter/GroupFilter.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\Search\IFilter; + +class GroupFilter implements IFilter { + private IGroup $group; + + public function __construct( + string $value, + IGroupManager $groupManager, + ) { + $group = $groupManager->get($value); + if ($group === null) { + throw new InvalidArgumentException('Group '.$value.' not found'); + } + $this->group = $group; + } + + public function get(): IGroup { + return $this->group; + } +} diff --git a/lib/private/Search/Filter/IntegerFilter.php b/lib/private/Search/Filter/IntegerFilter.php new file mode 100644 index 00000000000..b5b907b220e --- /dev/null +++ b/lib/private/Search/Filter/IntegerFilter.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class IntegerFilter implements IFilter { + private int $value; + + public function __construct(string $value) { + $this->value = filter_var($value, FILTER_VALIDATE_INT); + if ($this->value === false) { + throw new InvalidArgumentException('Invalid integer value '. $value); + } + } + + public function get(): int { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/StringFilter.php b/lib/private/Search/Filter/StringFilter.php new file mode 100644 index 00000000000..8f754d12051 --- /dev/null +++ b/lib/private/Search/Filter/StringFilter.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class StringFilter implements IFilter { + public function __construct( + private string $value, + ) { + if ($value === '') { + throw new InvalidArgumentException('String filter can’t be empty'); + } + } + + public function get(): string { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/StringsFilter.php b/lib/private/Search/Filter/StringsFilter.php new file mode 100644 index 00000000000..7a8d88768e8 --- /dev/null +++ b/lib/private/Search/Filter/StringsFilter.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class StringsFilter implements IFilter { + /** + * @var string[] + */ + private array $values; + + public function __construct(string ...$values) { + $this->values = array_unique(array_filter($values)); + if (empty($this->values)) { + throw new InvalidArgumentException('Strings filter can’t be empty'); + } + } + + /** + * @return string[] + */ + public function get(): array { + return $this->values; + } +} diff --git a/lib/private/Search/Filter/UserFilter.php b/lib/private/Search/Filter/UserFilter.php new file mode 100644 index 00000000000..963d5e123ac --- /dev/null +++ b/lib/private/Search/Filter/UserFilter.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Search\IFilter; + +class UserFilter implements IFilter { + private IUser $user; + + public function __construct( + string $value, + IUserManager $userManager, + ) { + $user = $userManager->get($value); + if ($user === null) { + throw new InvalidArgumentException('User '.$value.' not found'); + } + $this->user = $user; + } + + public function get(): IUser { + return $this->user; + } +} diff --git a/lib/private/Search/FilterCollection.php b/lib/private/Search/FilterCollection.php new file mode 100644 index 00000000000..8c23cc7c110 --- /dev/null +++ b/lib/private/Search/FilterCollection.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Search; + +use Generator; +use OCP\Search\IFilterCollection; +use OCP\Search\IFilter; + +/** + * Interface for search filters + * + * @since 28.0.0 + */ +class FilterCollection implements IFilterCollection { + /** + * @var IFilter[] + */ + private array $filters; + + public function __construct(IFilter ...$filters) { + $this->filters = $filters; + } + + public function has(string $name): bool { + return isset($this->filters[$name]); + } + + public function get(string $name): ?IFilter { + return $this->filters[$name] ?? null; + } + + public function getIterator(): Generator { + foreach ($this->filters as $k => $v) { + yield $k => $v; + } + } +} diff --git a/lib/private/Search/FilterFactory.php b/lib/private/Search/FilterFactory.php new file mode 100644 index 00000000000..2e96dfb7960 --- /dev/null +++ b/lib/private/Search/FilterFactory.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Search; + +use OCP\Search\FilterDefinition; +use OCP\Search\IFilter; +use OCP\IGroupManager; +use OCP\IUserManager; +use RuntimeException; + +final class FilterFactory { + private const PERSON_TYPE_SEPARATOR = '/'; + + public static function get(string $type, string|array $filter): IFilter { + return match ($type) { + FilterDefinition::TYPE_BOOL => new Filter\BooleanFilter($filter), + FilterDefinition::TYPE_DATETIME => new Filter\DateTimeFilter($filter), + FilterDefinition::TYPE_FLOAT => new Filter\FloatFilter($filter), + FilterDefinition::TYPE_INT => new Filter\IntegerFilter($filter), + FilterDefinition::TYPE_NC_GROUP => new Filter\GroupFilter($filter, \OC::$server->get(IGroupManager::class)), + FilterDefinition::TYPE_NC_USER => new Filter\UserFilter($filter, \OC::$server->get(IUserManager::class)), + FilterDefinition::TYPE_PERSON => self::getPerson($filter), + FilterDefinition::TYPE_STRING => new Filter\StringFilter($filter), + FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array) $filter), + default => throw new RuntimeException('Invalid filter type '. $type), + }; + } + + private static function getPerson(string $person): IFilter { + $parts = explode(self::PERSON_TYPE_SEPARATOR, $person, 2); + + return match (count($parts)) { + 1 => self::get(FilterDefinition::TYPE_NC_USER, $person), + 2 => self::get(... $parts), + }; + } +} diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php index 4ec73ec54e9..22da362bf40 100644 --- a/lib/private/Search/SearchComposer.php +++ b/lib/private/Search/SearchComposer.php @@ -28,14 +28,20 @@ declare(strict_types=1); namespace OC\Search; use InvalidArgumentException; -use OCP\AppFramework\QueryException; -use OCP\IServerContainer; +use OCP\IURLGenerator; +use OCP\Search\FilterDefinition; +use OCP\Search\IFilteringProvider; +use OCP\Search\IInAppSearch; +use OC\AppFramework\Bootstrap\Coordinator; use OCP\IUser; +use OCP\Search\IFilter; use OCP\Search\IProvider; use OCP\Search\ISearchQuery; use OCP\Search\SearchResult; -use OC\AppFramework\Bootstrap\Coordinator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use RuntimeException; use function array_map; /** @@ -58,31 +64,40 @@ use function array_map; * @see IProvider::search() for the arguments of the individual search requests */ class SearchComposer { - /** @var IProvider[] */ - private $providers = []; - - /** @var Coordinator */ - private $bootstrapCoordinator; + /** + * @var array<string, array{appId: string, provider: IProvider}> + */ + private array $providers = []; - /** @var IServerContainer */ - private $container; + private array $commonFilters; + private array $customFilters = []; - private LoggerInterface $logger; + private array $handlers = []; - public function __construct(Coordinator $bootstrapCoordinator, - IServerContainer $container, - LoggerInterface $logger) { - $this->container = $container; - $this->logger = $logger; - $this->bootstrapCoordinator = $bootstrapCoordinator; + public function __construct( + private Coordinator $bootstrapCoordinator, + private ContainerInterface $container, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger + ) { + $this->commonFilters = [ + IFilter::BUILTIN_TERM => new FilterDefinition(IFilter::BUILTIN_TERM, FilterDefinition::TYPE_STRING), + IFilter::BUILTIN_SINCE => new FilterDefinition(IFilter::BUILTIN_SINCE, FilterDefinition::TYPE_DATETIME), + IFilter::BUILTIN_UNTIL => new FilterDefinition(IFilter::BUILTIN_UNTIL, FilterDefinition::TYPE_DATETIME), + IFilter::BUILTIN_TITLE_ONLY => new FilterDefinition(IFilter::BUILTIN_TITLE_ONLY, FilterDefinition::TYPE_BOOL, false), + IFilter::BUILTIN_PERSON => new FilterDefinition(IFilter::BUILTIN_PERSON, FilterDefinition::TYPE_PERSON), + IFilter::BUILTIN_PLACES => new FilterDefinition(IFilter::BUILTIN_PLACES, FilterDefinition::TYPE_STRINGS, false), + IFilter::BUILTIN_PROVIDER => new FilterDefinition(IFilter::BUILTIN_PROVIDER, FilterDefinition::TYPE_STRING, false), + ]; } /** * Load all providers dynamically that were registered through `registerProvider` * + * If $targetProviderId is provided, only this provider is loaded * If a provider can't be loaded we log it but the operation continues nevertheless */ - private function loadLazyProviders(): void { + private function loadLazyProviders(?string $targetProviderId = null): void { $context = $this->bootstrapCoordinator->getRegistrationContext(); if ($context === null) { // Too early, nothing registered yet @@ -93,9 +108,20 @@ class SearchComposer { foreach ($registrations as $registration) { try { /** @var IProvider $provider */ - $provider = $this->container->query($registration->getService()); - $this->providers[$provider->getId()] = $provider; - } catch (QueryException $e) { + $provider = $this->container->get($registration->getService()); + $providerId = $provider->getId(); + if ($targetProviderId !== null && $targetProviderId !== $providerId) { + continue; + } + $this->providers[$providerId] = [ + 'appId' => $registration->getAppId(), + 'provider' => $provider, + ]; + $this->handlers[$providerId] = [$providerId]; + if ($targetProviderId !== null) { + break; + } + } catch (ContainerExceptionInterface $e) { // Log an continue. We can be fault tolerant here. $this->logger->error('Could not load search provider dynamically: ' . $e->getMessage(), [ 'exception' => $e, @@ -103,6 +129,43 @@ class SearchComposer { ]); } } + + $this->loadFilters(); + } + + private function loadFilters(): void { + foreach ($this->providers as $providerId => $providerData) { + $appId = $providerData['appId']; + $provider = $providerData['provider']; + if (!$provider instanceof IFilteringProvider) { + continue; + } + + foreach ($provider->getCustomFilters() as $filter) { + $this->registerCustomFilter($filter, $providerId); + } + foreach ($provider->getAlternateIds() as $alternateId) { + $this->handlers[$alternateId][] = $providerId; + } + foreach ($provider->getSupportedFilters() as $filterName) { + if ($this->getFilterDefinition($filterName, $providerId) === null) { + throw new InvalidArgumentException('Invalid filter '. $filterName); + } + } + } + } + + private function registerCustomFilter(FilterDefinition $filter, string $providerId): void { + $name = $filter->name(); + if (isset($this->commonFilters[$name])) { + throw new InvalidArgumentException('Filter name is already used'); + } + + if (isset($this->customFilters[$providerId])) { + $this->customFilters[$providerId][$name] = $filter; + } else { + $this->customFilters[$providerId] = [$name => $filter]; + } } /** @@ -117,26 +180,146 @@ class SearchComposer { public function getProviders(string $route, array $routeParameters): array { $this->loadLazyProviders(); - $providers = array_values( - array_map(function (IProvider $provider) use ($route, $routeParameters) { + $providers = array_map( + function (array $providerData) use ($route, $routeParameters) { + $appId = $providerData['appId']; + $provider = $providerData['provider']; + $order = $provider->getOrder($route, $routeParameters); + if ($order === null) { + return; + } + $triggers = [$provider->getId()]; + if ($provider instanceof IFilteringProvider) { + $triggers += $provider->getAlternateIds(); + $filters = $provider->getSupportedFilters(); + } else { + $filters = [IFilter::BUILTIN_TERM]; + } + return [ 'id' => $provider->getId(), + 'appId' => $appId, 'name' => $provider->getName(), - 'order' => $provider->getOrder($route, $routeParameters), + 'icon' => $this->fetchIcon($appId, $provider->getId()), + 'order' => $order, + 'triggers' => $triggers, + 'filters' => $this->getFiltersType($filters, $provider->getId()), + 'inAppSearch' => $provider instanceof IInAppSearch, ]; - }, $this->providers) + }, + $this->providers, ); + $providers = array_filter($providers); + // Sort providers by order and strip associative keys usort($providers, function ($provider1, $provider2) { return $provider1['order'] <=> $provider2['order']; }); - /** - * Return an array with the IDs, but strip the associative keys - */ return $providers; } + private function fetchIcon(string $appId, string $providerId): string { + $icons = [ + [$providerId, $providerId.'.svg'], + [$providerId, 'app.svg'], + [$appId, $providerId.'.svg'], + [$appId, $appId.'.svg'], + [$appId, 'app.svg'], + ['core', 'places/default-app-icon.svg'], + ]; + if ($appId === 'settings' && $providerId === 'users') { + // Conflict: + // the file /apps/settings/users.svg is already used in black version by top right user menu + // Override icon name here + $icons = [['settings', 'users-white.svg']]; + } + foreach ($icons as $i => $icon) { + try { + return $this->urlGenerator->imagePath(... $icon); + } catch (RuntimeException $e) { + // Ignore error + } + } + + return ''; + } + + /** + * @param $filters string[] + * @return array<string, string> + */ + private function getFiltersType(array $filters, string $providerId): array { + $filterList = []; + foreach ($filters as $filter) { + $filterList[$filter] = $this->getFilterDefinition($filter, $providerId)->type(); + } + + return $filterList; + } + + private function getFilterDefinition(string $name, string $providerId): ?FilterDefinition { + if (isset($this->commonFilters[$name])) { + return $this->commonFilters[$name]; + } + if (isset($this->customFilters[$providerId][$name])) { + return $this->customFilters[$providerId][$name]; + } + + return null; + } + + /** + * @param array<string, string> $parameters + */ + public function buildFilterList(string $providerId, array $parameters): FilterCollection { + $this->loadLazyProviders($providerId); + + $list = []; + foreach ($parameters as $name => $value) { + $filter = $this->buildFilter($name, $value, $providerId); + if ($filter === null) { + continue; + } + $list[$name] = $filter; + } + + return new FilterCollection(... $list); + } + + private function buildFilter(string $name, string $value, string $providerId): ?IFilter { + $filterDefinition = $this->getFilterDefinition($name, $providerId); + if ($filterDefinition === null) { + $this->logger->debug('Unable to find {name} definition', [ + 'name' => $name, + 'value' => $value, + ]); + + return null; + } + + if (!$this->filterSupportedByProvider($filterDefinition, $providerId)) { + // FIXME Use dedicated exception and handle it + throw new UnsupportedFilter($name, $providerId); + } + + return FilterFactory::get($filterDefinition->type(), $value); + } + + private function filterSupportedByProvider(FilterDefinition $filterDefinition, string $providerId): bool { + // Non exclusive filters can be ommited by apps + if (!$filterDefinition->exclusive()) { + return true; + } + + $provider = $this->providers[$providerId]['provider']; + $supportedFilters = $provider instanceof IFilteringProvider + ? $provider->getSupportedFilters() + : [IFilter::BUILTIN_TERM]; + + return in_array($filterDefinition->name(), $supportedFilters, true); + } + /** * Query an individual search provider for results * @@ -147,15 +330,18 @@ class SearchComposer { * @return SearchResult * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider */ - public function search(IUser $user, - string $providerId, - ISearchQuery $query): SearchResult { - $this->loadLazyProviders(); + public function search( + IUser $user, + string $providerId, + ISearchQuery $query, + ): SearchResult { + $this->loadLazyProviders($providerId); - $provider = $this->providers[$providerId] ?? null; + $provider = $this->providers[$providerId]['provider'] ?? null; if ($provider === null) { throw new InvalidArgumentException("Provider $providerId is unknown"); } + return $provider->search($user, $query); } } diff --git a/lib/private/Search/SearchQuery.php b/lib/private/Search/SearchQuery.php index c89446d5970..aae2044bcd6 100644 --- a/lib/private/Search/SearchQuery.php +++ b/lib/private/Search/SearchQuery.php @@ -27,89 +27,57 @@ declare(strict_types=1); */ namespace OC\Search; +use OCP\Search\IFilterCollection; +use OCP\Search\IFilter; use OCP\Search\ISearchQuery; class SearchQuery implements ISearchQuery { public const LIMIT_DEFAULT = 5; - /** @var string */ - private $term; - - /** @var int */ - private $sortOrder; - - /** @var int */ - private $limit; - - /** @var int|string|null */ - private $cursor; - - /** @var string */ - private $route; - - /** @var array */ - private $routeParameters; - /** - * @param string $term - * @param int $sortOrder - * @param int $limit - * @param int|string|null $cursor - * @param string $route - * @param array $routeParameters + * @param string[] $params Request query + * @param string[] $routeParameters */ - public function __construct(string $term, - int $sortOrder = ISearchQuery::SORT_DATE_DESC, - int $limit = self::LIMIT_DEFAULT, - $cursor = null, - string $route = '', - array $routeParameters = []) { - $this->term = $term; - $this->sortOrder = $sortOrder; - $this->limit = $limit; - $this->cursor = $cursor; - $this->route = $route; - $this->routeParameters = $routeParameters; + public function __construct( + private IFilterCollection $filters, + private int $sortOrder = ISearchQuery::SORT_DATE_DESC, + private int $limit = self::LIMIT_DEFAULT, + private int|string|null $cursor = null, + private string $route = '', + private array $routeParameters = [], + ) { } - /** - * @inheritDoc - */ public function getTerm(): string { - return $this->term; + return $this->getFilter('term')?->get() ?? ''; + } + + public function getFilter(string $name): ?IFilter { + return $this->filters->has($name) + ? $this->filters->get($name) + : null; + } + + public function getFilters(): IFilterCollection { + return $this->filters; } - /** - * @inheritDoc - */ public function getSortOrder(): int { return $this->sortOrder; } - /** - * @inheritDoc - */ public function getLimit(): int { return $this->limit; } - /** - * @inheritDoc - */ - public function getCursor() { + public function getCursor(): int|string|null { return $this->cursor; } - /** - * @inheritDoc - */ public function getRoute(): string { return $this->route; } - /** - * @inheritDoc - */ public function getRouteParameters(): array { return $this->routeParameters; } diff --git a/lib/private/Search/UnsupportedFilter.php b/lib/private/Search/UnsupportedFilter.php new file mode 100644 index 00000000000..84b6163d2fa --- /dev/null +++ b/lib/private/Search/UnsupportedFilter.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Search; + +use Exception; + +final class UnsupportedFilter extends Exception { + public function __construct(string $filerName, $providerId) { + parent::__construct('Provider '.$providerId.' doesn’t support filter '.$filerName.'.'); + } +} diff --git a/lib/private/Security/Bruteforce/CleanupJob.php b/lib/private/Security/Bruteforce/CleanupJob.php index 45cfe572acb..13628dd300d 100644 --- a/lib/private/Security/Bruteforce/CleanupJob.php +++ b/lib/private/Security/Bruteforce/CleanupJob.php @@ -32,19 +32,18 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; class CleanupJob extends TimedJob { - /** @var IDBConnection */ - private $connection; - - public function __construct(ITimeFactory $time, IDBConnection $connection) { + public function __construct( + ITimeFactory $time, + private IDBConnection $connection, + ) { parent::__construct($time); - $this->connection = $connection; // Run once a day $this->setInterval(3600 * 24); $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } - protected function run($argument) { + protected function run($argument): void { // Delete all entries more than 48 hours old $time = $this->time->getTime() - (48 * 3600); diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 2803373e8ba..5316071f25c 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -106,9 +106,6 @@ class Throttler implements IThrottler { /** * Check if the IP is whitelisted - * - * @param string $ip - * @return bool */ public function isBypassListed(string $ip): bool { if (isset($this->ipIsWhitelisted[$ip])) { diff --git a/lib/private/Security/CSP/ContentSecurityPolicy.php b/lib/private/Security/CSP/ContentSecurityPolicy.php index e2d115cf34e..ee525af4c2a 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicy.php +++ b/lib/private/Security/CSP/ContentSecurityPolicy.php @@ -34,33 +34,22 @@ namespace OC\Security\CSP; * @package OC\Security\CSP */ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy { - /** - * @return boolean - */ public function isInlineScriptAllowed(): bool { return $this->inlineScriptAllowed; } - /** - * @param boolean $inlineScriptAllowed - */ - public function setInlineScriptAllowed(bool $inlineScriptAllowed) { + public function setInlineScriptAllowed(bool $inlineScriptAllowed): void { $this->inlineScriptAllowed = $inlineScriptAllowed; } - /** - * @return boolean - */ public function isEvalScriptAllowed(): bool { return $this->evalScriptAllowed; } /** - * @param boolean $evalScriptAllowed - * * @deprecated 17.0.0 Unsafe eval should not be used anymore. */ - public function setEvalScriptAllowed(bool $evalScriptAllowed) { + public function setEvalScriptAllowed(bool $evalScriptAllowed): void { $this->evalScriptAllowed = $evalScriptAllowed; } @@ -72,134 +61,79 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy $this->evalWasmAllowed = $evalWasmAllowed; } - /** - * @return array - */ public function getAllowedScriptDomains(): array { return $this->allowedScriptDomains; } - /** - * @param array $allowedScriptDomains - */ - public function setAllowedScriptDomains(array $allowedScriptDomains) { + public function setAllowedScriptDomains(array $allowedScriptDomains): void { $this->allowedScriptDomains = $allowedScriptDomains; } - /** - * @return boolean - */ public function isInlineStyleAllowed(): bool { return $this->inlineStyleAllowed; } - /** - * @param boolean $inlineStyleAllowed - */ - public function setInlineStyleAllowed(bool $inlineStyleAllowed) { + public function setInlineStyleAllowed(bool $inlineStyleAllowed): void { $this->inlineStyleAllowed = $inlineStyleAllowed; } - /** - * @return array - */ public function getAllowedStyleDomains(): array { return $this->allowedStyleDomains; } - /** - * @param array $allowedStyleDomains - */ - public function setAllowedStyleDomains(array $allowedStyleDomains) { + public function setAllowedStyleDomains(array $allowedStyleDomains): void { $this->allowedStyleDomains = $allowedStyleDomains; } - /** - * @return array - */ public function getAllowedImageDomains(): array { return $this->allowedImageDomains; } - /** - * @param array $allowedImageDomains - */ - public function setAllowedImageDomains(array $allowedImageDomains) { + public function setAllowedImageDomains(array $allowedImageDomains): void { $this->allowedImageDomains = $allowedImageDomains; } - /** - * @return array - */ public function getAllowedConnectDomains(): array { return $this->allowedConnectDomains; } - /** - * @param array $allowedConnectDomains - */ - public function setAllowedConnectDomains(array $allowedConnectDomains) { + public function setAllowedConnectDomains(array $allowedConnectDomains): void { $this->allowedConnectDomains = $allowedConnectDomains; } - /** - * @return array - */ public function getAllowedMediaDomains(): array { return $this->allowedMediaDomains; } - /** - * @param array $allowedMediaDomains - */ - public function setAllowedMediaDomains(array $allowedMediaDomains) { + public function setAllowedMediaDomains(array $allowedMediaDomains): void { $this->allowedMediaDomains = $allowedMediaDomains; } - /** - * @return array - */ public function getAllowedObjectDomains(): array { return $this->allowedObjectDomains; } - /** - * @param array $allowedObjectDomains - */ - public function setAllowedObjectDomains(array $allowedObjectDomains) { + public function setAllowedObjectDomains(array $allowedObjectDomains): void { $this->allowedObjectDomains = $allowedObjectDomains; } - /** - * @return array - */ public function getAllowedFrameDomains(): array { return $this->allowedFrameDomains; } - /** - * @param array $allowedFrameDomains - */ - public function setAllowedFrameDomains(array $allowedFrameDomains) { + public function setAllowedFrameDomains(array $allowedFrameDomains): void { $this->allowedFrameDomains = $allowedFrameDomains; } - /** - * @return array - */ public function getAllowedFontDomains(): array { return $this->allowedFontDomains; } - /** - * @param array $allowedFontDomains - */ - public function setAllowedFontDomains($allowedFontDomains) { + public function setAllowedFontDomains($allowedFontDomains): void { $this->allowedFontDomains = $allowedFontDomains; } /** - * @return array * @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains */ public function getAllowedChildSrcDomains(): array { @@ -210,13 +144,10 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy * @param array $allowedChildSrcDomains * @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains */ - public function setAllowedChildSrcDomains($allowedChildSrcDomains) { + public function setAllowedChildSrcDomains($allowedChildSrcDomains): void { $this->allowedChildSrcDomains = $allowedChildSrcDomains; } - /** - * @return array - */ public function getAllowedFrameAncestors(): array { return $this->allowedFrameAncestors; } @@ -224,7 +155,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy /** * @param array $allowedFrameAncestors */ - public function setAllowedFrameAncestors($allowedFrameAncestors) { + public function setAllowedFrameAncestors($allowedFrameAncestors): void { $this->allowedFrameAncestors = $allowedFrameAncestors; } @@ -232,7 +163,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy return $this->allowedWorkerSrcDomains; } - public function setAllowedWorkerSrcDomains(array $allowedWorkerSrcDomains) { + public function setAllowedWorkerSrcDomains(array $allowedWorkerSrcDomains): void { $this->allowedWorkerSrcDomains = $allowedWorkerSrcDomains; } @@ -249,21 +180,23 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy return $this->reportTo; } - public function setReportTo(array $reportTo) { + public function setReportTo(array $reportTo): void { $this->reportTo = $reportTo; } - /** - * @return boolean - */ public function isStrictDynamicAllowed(): bool { return $this->strictDynamicAllowed; } - /** - * @param boolean $strictDynamicAllowed - */ - public function setStrictDynamicAllowed(bool $strictDynamicAllowed) { + public function setStrictDynamicAllowed(bool $strictDynamicAllowed): void { $this->strictDynamicAllowed = $strictDynamicAllowed; } + + public function isStrictDynamicAllowedOnScripts(): bool { + return $this->strictDynamicAllowedOnScripts; + } + + public function setStrictDynamicAllowedOnScripts(bool $strictDynamicAllowedOnScripts): void { + $this->strictDynamicAllowedOnScripts = $strictDynamicAllowedOnScripts; + } } diff --git a/lib/private/Security/CSP/ContentSecurityPolicyManager.php b/lib/private/Security/CSP/ContentSecurityPolicyManager.php index 4930dcb759c..503933ef980 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicyManager.php +++ b/lib/private/Security/CSP/ContentSecurityPolicyManager.php @@ -35,25 +35,21 @@ use OCP\Security\IContentSecurityPolicyManager; class ContentSecurityPolicyManager implements IContentSecurityPolicyManager { /** @var ContentSecurityPolicy[] */ - private $policies = []; + private array $policies = []; - /** @var IEventDispatcher */ - private $dispatcher; - - public function __construct(IEventDispatcher $dispatcher) { - $this->dispatcher = $dispatcher; + public function __construct( + private IEventDispatcher $dispatcher, + ) { } /** {@inheritdoc} */ - public function addDefaultPolicy(EmptyContentSecurityPolicy $policy) { + public function addDefaultPolicy(EmptyContentSecurityPolicy $policy): void { $this->policies[] = $policy; } /** * Get the configured default policy. This is not in the public namespace * as it is only supposed to be used by core itself. - * - * @return ContentSecurityPolicy */ public function getDefaultPolicy(): ContentSecurityPolicy { $event = new AddContentSecurityPolicyEvent($this); @@ -68,13 +64,11 @@ class ContentSecurityPolicyManager implements IContentSecurityPolicyManager { /** * Merges the first given policy with the second one - * - * @param ContentSecurityPolicy $defaultPolicy - * @param EmptyContentSecurityPolicy $originalPolicy - * @return ContentSecurityPolicy */ - public function mergePolicies(ContentSecurityPolicy $defaultPolicy, - EmptyContentSecurityPolicy $originalPolicy): ContentSecurityPolicy { + public function mergePolicies( + ContentSecurityPolicy $defaultPolicy, + EmptyContentSecurityPolicy $originalPolicy, + ): ContentSecurityPolicy { foreach ((object)(array)$originalPolicy as $name => $value) { $setter = 'set'.ucfirst($name); if (\is_array($value)) { diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php index 1167b3358d2..6573007a459 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php +++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php @@ -38,27 +38,16 @@ use OCP\IRequest; * @package OC\Security\CSP */ class ContentSecurityPolicyNonceManager { - /** @var CsrfTokenManager */ - private $csrfTokenManager; - /** @var IRequest */ - private $request; - /** @var string */ - private $nonce = ''; + private string $nonce = ''; - /** - * @param CsrfTokenManager $csrfTokenManager - * @param IRequest $request - */ - public function __construct(CsrfTokenManager $csrfTokenManager, - IRequest $request) { - $this->csrfTokenManager = $csrfTokenManager; - $this->request = $request; + public function __construct( + private CsrfTokenManager $csrfTokenManager, + private IRequest $request, + ) { } /** - * Returns the current CSP nounce - * - * @return string + * Returns the current CSP nonce */ public function getNonce(): string { if ($this->nonce === '') { @@ -74,8 +63,6 @@ class ContentSecurityPolicyNonceManager { /** * Check if the browser supports CSP v3 - * - * @return bool */ public function browserSupportsCspV3(): bool { $browserWhitelist = [ diff --git a/lib/private/Security/CSRF/CsrfToken.php b/lib/private/Security/CSRF/CsrfToken.php index a76e169e5b9..45e628b3f3c 100644 --- a/lib/private/Security/CSRF/CsrfToken.php +++ b/lib/private/Security/CSRF/CsrfToken.php @@ -36,23 +36,19 @@ namespace OC\Security\CSRF; * @package OC\Security\CSRF */ class CsrfToken { - /** @var string */ - private $value; - /** @var string */ - private $encryptedValue = ''; + private string $encryptedValue = ''; /** * @param string $value Value of the token. Can be encrypted or not encrypted. */ - public function __construct(string $value) { - $this->value = $value; + public function __construct( + private string $value, + ) { } /** * Encrypted value of the token. This is used to mitigate BREACH alike * vulnerabilities. For display measures do use this functionality. - * - * @return string */ public function getEncryptedValue(): string { if ($this->encryptedValue === '') { @@ -66,8 +62,6 @@ class CsrfToken { /** * The unencrypted value of the token. Used for decrypting an already * encrypted token. - * - * @return string */ public function getDecryptedValue(): string { $token = explode(':', $this->value); diff --git a/lib/private/Security/CSRF/CsrfTokenGenerator.php b/lib/private/Security/CSRF/CsrfTokenGenerator.php index 0576fda9e06..c3d89247de1 100644 --- a/lib/private/Security/CSRF/CsrfTokenGenerator.php +++ b/lib/private/Security/CSRF/CsrfTokenGenerator.php @@ -34,21 +34,15 @@ use OCP\Security\ISecureRandom; * @package OC\Security\CSRF */ class CsrfTokenGenerator { - /** @var ISecureRandom */ - private $random; - - /** - * @param ISecureRandom $random - */ - public function __construct(ISecureRandom $random) { - $this->random = $random; + public function __construct( + private ISecureRandom $random, + ) { } /** * Generate a new CSRF token. * * @param int $length Length of the token in characters. - * @return string */ public function generateToken(int $length = 32): string { return $this->random->generate($length); diff --git a/lib/private/Security/CSRF/CsrfTokenManager.php b/lib/private/Security/CSRF/CsrfTokenManager.php index 2c6dd45866d..dceacf45e2a 100644 --- a/lib/private/Security/CSRF/CsrfTokenManager.php +++ b/lib/private/Security/CSRF/CsrfTokenManager.php @@ -34,27 +34,18 @@ use OC\Security\CSRF\TokenStorage\SessionStorage; * @package OC\Security\CSRF */ class CsrfTokenManager { - /** @var CsrfTokenGenerator */ - private $tokenGenerator; - /** @var SessionStorage */ - private $sessionStorage; - /** @var CsrfToken|null */ - private $csrfToken = null; + private SessionStorage $sessionStorage; + private ?CsrfToken $csrfToken = null; - /** - * @param CsrfTokenGenerator $tokenGenerator - * @param SessionStorage $storageInterface - */ - public function __construct(CsrfTokenGenerator $tokenGenerator, - SessionStorage $storageInterface) { - $this->tokenGenerator = $tokenGenerator; + public function __construct( + private CsrfTokenGenerator $tokenGenerator, + SessionStorage $storageInterface, + ) { $this->sessionStorage = $storageInterface; } /** * Returns the current CSRF token, if none set it will create a new one. - * - * @return CsrfToken */ public function getToken(): CsrfToken { if (!\is_null($this->csrfToken)) { @@ -74,8 +65,6 @@ class CsrfTokenManager { /** * Invalidates any current token and sets a new one. - * - * @return CsrfToken */ public function refreshToken(): CsrfToken { $value = $this->tokenGenerator->generateToken(); @@ -87,16 +76,13 @@ class CsrfTokenManager { /** * Remove the current token from the storage. */ - public function removeToken() { + public function removeToken(): void { $this->csrfToken = null; $this->sessionStorage->removeToken(); } /** * Verifies whether the provided token is valid. - * - * @param CsrfToken $token - * @return bool */ public function isTokenValid(CsrfToken $token): bool { if (!$this->sessionStorage->hasToken()) { diff --git a/lib/private/Security/CSRF/TokenStorage/SessionStorage.php b/lib/private/Security/CSRF/TokenStorage/SessionStorage.php index ab05d5b1493..0ffe043e2f9 100644 --- a/lib/private/Security/CSRF/TokenStorage/SessionStorage.php +++ b/lib/private/Security/CSRF/TokenStorage/SessionStorage.php @@ -35,27 +35,18 @@ use OCP\ISession; * @package OC\Security\CSRF\TokenStorage */ class SessionStorage { - /** @var ISession */ - private $session; - - /** - * @param ISession $session - */ - public function __construct(ISession $session) { - $this->session = $session; + public function __construct( + private ISession $session, + ) { } - /** - * @param ISession $session - */ - public function setSession(ISession $session) { + public function setSession(ISession $session): void { $this->session = $session; } /** * Returns the current token or throws an exception if none is found. * - * @return string * @throws \Exception */ public function getToken(): string { @@ -69,23 +60,20 @@ class SessionStorage { /** * Set the valid current token to $value. - * - * @param string $value */ - public function setToken(string $value) { + public function setToken(string $value): void { $this->session->set('requesttoken', $value); } /** * Removes the current token. */ - public function removeToken() { + public function removeToken(): void { $this->session->remove('requesttoken'); } + /** * Whether the storage has a storage. - * - * @return bool */ public function hasToken(): bool { return $this->session->exists('requesttoken'); diff --git a/lib/private/Security/Certificate.php b/lib/private/Security/Certificate.php index fb5b9aa8a93..759c71b2eec 100644 --- a/lib/private/Security/Certificate.php +++ b/lib/private/Security/Certificate.php @@ -30,25 +30,23 @@ namespace OC\Security; use OCP\ICertificate; class Certificate implements ICertificate { - protected $name; + protected string $name; - protected $commonName; + protected ?string $commonName; - protected $organization; + protected ?string $organization; - protected $serial; - protected $issueDate; + protected \DateTime $issueDate; - protected $expireDate; + protected \DateTime $expireDate; - protected $issuerName; + protected ?string $issuerName; - protected $issuerOrganization; + protected ?string $issuerOrganization; /** * @param string $data base64 encoded certificate - * @param string $name * @throws \Exception If the certificate could not get parsed */ public function __construct(string $data, string $name) { @@ -66,67 +64,43 @@ class Certificate implements ICertificate { throw new \Exception('Certificate could not get parsed.'); } - $this->commonName = isset($info['subject']['CN']) ? $info['subject']['CN'] : null; - $this->organization = isset($info['subject']['O']) ? $info['subject']['O'] : null; + $this->commonName = $info['subject']['CN'] ?? null; + $this->organization = $info['subject']['O'] ?? null; $this->issueDate = new \DateTime('@' . $info['validFrom_time_t'], $gmt); $this->expireDate = new \DateTime('@' . $info['validTo_time_t'], $gmt); - $this->issuerName = isset($info['issuer']['CN']) ? $info['issuer']['CN'] : null; - $this->issuerOrganization = isset($info['issuer']['O']) ? $info['issuer']['O'] : null; + $this->issuerName = $info['issuer']['CN'] ?? null; + $this->issuerOrganization = $info['issuer']['O'] ?? null; } - /** - * @return string - */ public function getName(): string { return $this->name; } - /** - * @return string|null - */ public function getCommonName(): ?string { return $this->commonName; } - /** - * @return string|null - */ public function getOrganization(): ?string { return $this->organization; } - /** - * @return \DateTime - */ public function getIssueDate(): \DateTime { return $this->issueDate; } - /** - * @return \DateTime - */ public function getExpireDate(): \DateTime { return $this->expireDate; } - /** - * @return bool - */ public function isExpired(): bool { $now = new \DateTime(); return $this->issueDate > $now or $now > $this->expireDate; } - /** - * @return string|null - */ public function getIssuerName(): ?string { return $this->issuerName; } - /** - * @return string|null - */ public function getIssuerOrganization(): ?string { return $this->issuerOrganization; } diff --git a/lib/private/Security/CertificateManager.php b/lib/private/Security/CertificateManager.php index 3a87b7f1a00..cf5f0f41d56 100644 --- a/lib/private/Security/CertificateManager.php +++ b/lib/private/Security/CertificateManager.php @@ -44,21 +44,14 @@ use Psr\Log\LoggerInterface; * Manage trusted certificates for users */ class CertificateManager implements ICertificateManager { - protected View $view; - protected IConfig $config; - protected LoggerInterface $logger; - protected ISecureRandom $random; - private ?string $bundlePath = null; - public function __construct(View $view, - IConfig $config, - LoggerInterface $logger, - ISecureRandom $random) { - $this->view = $view; - $this->config = $config; - $this->logger = $logger; - $this->random = $random; + public function __construct( + protected View $view, + protected IConfig $config, + protected LoggerInterface $logger, + protected ISecureRandom $random, + ) { } /** @@ -178,7 +171,6 @@ class CertificateManager implements ICertificateManager { * * @param string $certificate the certificate data * @param string $name the filename for the certificate - * @return \OCP\ICertificate * @throws \Exception If the certificate could not get added */ public function addCertificate(string $certificate, string $name): ICertificate { @@ -205,9 +197,6 @@ class CertificateManager implements ICertificateManager { /** * Remove the certificate and re-generate the certificate bundle - * - * @param string $name - * @return bool */ public function removeCertificate(string $name): bool { if (!Filesystem::isValidPath($name)) { @@ -225,8 +214,6 @@ class CertificateManager implements ICertificateManager { /** * Get the path to the certificate bundle - * - * @return string */ public function getCertificateBundle(): string { return $this->getPathToCertificates() . 'rootcerts.crt'; @@ -267,8 +254,6 @@ class CertificateManager implements ICertificateManager { /** * Check if we need to re-bundle the certificates because one of the sources has updated - * - * @return bool */ private function needsRebundling(): bool { $targetBundle = $this->getCertificateBundle(); @@ -282,8 +267,6 @@ class CertificateManager implements ICertificateManager { /** * get mtime of ca-bundle shipped by Nextcloud - * - * @return int */ protected function getFilemtimeOfCaBundle(): int { return filemtime(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'); diff --git a/lib/private/Security/CredentialsManager.php b/lib/private/Security/CredentialsManager.php index 0bddaeda1b0..ea59a24d646 100644 --- a/lib/private/Security/CredentialsManager.php +++ b/lib/private/Security/CredentialsManager.php @@ -40,26 +40,16 @@ use OCP\Security\ICrypto; class CredentialsManager implements ICredentialsManager { public const DB_TABLE = 'storages_credentials'; - /** @var ICrypto */ - protected $crypto; - - /** @var IDBConnection */ - protected $dbConnection; - - /** - * @param ICrypto $crypto - * @param IDBConnection $dbConnection - */ - public function __construct(ICrypto $crypto, IDBConnection $dbConnection) { - $this->crypto = $crypto; - $this->dbConnection = $dbConnection; + public function __construct( + protected ICrypto $crypto, + protected IDBConnection $dbConnection, + ) { } /** * Store a set of credentials * * @param string $userId empty string for system-wide credentials - * @param string $identifier * @param mixed $credentials */ public function store(string $userId, string $identifier, $credentials): void { @@ -77,10 +67,8 @@ class CredentialsManager implements ICredentialsManager { * Retrieve a set of credentials * * @param string $userId empty string for system-wide credentials - * @param string $identifier - * @return mixed */ - public function retrieve(string $userId, string $identifier) { + public function retrieve(string $userId, string $identifier): mixed { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('credentials') ->from(self::DB_TABLE) @@ -108,7 +96,6 @@ class CredentialsManager implements ICredentialsManager { * Delete a set of credentials * * @param string $userId empty string for system-wide credentials - * @param string $identifier * @return int rows removed */ public function delete(string $userId, string $identifier): int { @@ -128,7 +115,6 @@ class CredentialsManager implements ICredentialsManager { /** * Erase all credentials stored for a user * - * @param string $userId * @return int rows removed */ public function erase(string $userId): int { diff --git a/lib/private/Security/Crypto.php b/lib/private/Security/Crypto.php index 2a7905376ef..033456f3f2e 100644 --- a/lib/private/Security/Crypto.php +++ b/lib/private/Security/Crypto.php @@ -32,7 +32,6 @@ namespace OC\Security; use Exception; use OCP\IConfig; use OCP\Security\ICrypto; -use OCP\Security\ISecureRandom; use phpseclib\Crypt\AES; use phpseclib\Crypt\Hash; @@ -47,20 +46,13 @@ use phpseclib\Crypt\Hash; * @package OC\Security */ class Crypto implements ICrypto { - /** @var AES $cipher */ - private $cipher; - /** @var int */ - private $ivLength = 16; - /** @var IConfig */ - private $config; + private AES $cipher; + private int $ivLength = 16; - /** - * @param IConfig $config - * @param ISecureRandom $random - */ - public function __construct(IConfig $config) { + public function __construct( + private IConfig $config, + ) { $this->cipher = new AES(); - $this->config = $config; } /** @@ -84,7 +76,6 @@ class Crypto implements ICrypto { /** * Encrypts a value and adds an HMAC (Encrypt-Then-MAC) * - * @param string $plaintext * @param string $password Password to encrypt, if not specified the secret from config.php will be taken * @return string Authenticated ciphertext * @throws Exception if it was not possible to gather sufficient entropy @@ -115,9 +106,7 @@ class Crypto implements ICrypto { /** * Decrypts a value and verifies the HMAC (Encrypt-Then-Mac) - * @param string $authenticatedCiphertext * @param string $password Password to encrypt, if not specified the secret from config.php will be taken - * @return string plaintext * @throws Exception If the HMAC does not match * @throws Exception If the decryption failed */ diff --git a/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php index 3aa93ac3da4..bb9fc41332f 100644 --- a/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php +++ b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php @@ -32,13 +32,11 @@ use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; class FeaturePolicyManager { /** @var EmptyFeaturePolicy[] */ - private $policies = []; + private array $policies = []; - /** @var IEventDispatcher */ - private $dispatcher; - - public function __construct(IEventDispatcher $dispatcher) { - $this->dispatcher = $dispatcher; + public function __construct( + private IEventDispatcher $dispatcher, + ) { } public function addDefaultPolicy(EmptyFeaturePolicy $policy): void { @@ -60,8 +58,10 @@ class FeaturePolicyManager { * Merges the first given policy with the second one * */ - public function mergePolicies(FeaturePolicy $defaultPolicy, - EmptyFeaturePolicy $originalPolicy): FeaturePolicy { + public function mergePolicies( + FeaturePolicy $defaultPolicy, + EmptyFeaturePolicy $originalPolicy, + ): FeaturePolicy { foreach ((object)(array)$originalPolicy as $name => $value) { $setter = 'set' . ucfirst($name); if (\is_array($value)) { diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php index 85f69263925..23747751053 100644 --- a/lib/private/Security/Hasher.php +++ b/lib/private/Security/Hasher.php @@ -51,19 +51,14 @@ use OCP\Security\IHasher; * @package OC\Security */ class Hasher implements IHasher { - /** @var IConfig */ - private $config; - /** @var array Options passed to password_hash and password_needs_rehash */ - private $options = []; - /** @var string Salt used for legacy passwords */ - private $legacySalt = null; - - /** - * @param IConfig $config - */ - public function __construct(IConfig $config) { - $this->config = $config; - + /** Options passed to password_hash and password_needs_rehash */ + private array $options = []; + /** Salt used for legacy passwords */ + private ?string $legacySalt = null; + + public function __construct( + private IConfig $config, + ) { if (\defined('PASSWORD_ARGON2ID') || \defined('PASSWORD_ARGON2I')) { // password_hash fails, when the minimum values are undershot. // In this case, apply minimum. @@ -106,7 +101,7 @@ class Hasher implements IHasher { * @param string $prefixedHash * @return null|array Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo') */ - protected function splitHash(string $prefixedHash) { + protected function splitHash(string $prefixedHash): ?array { $explodedString = explode('|', $prefixedHash, 2); if (\count($explodedString) === 2) { if ((int)$explodedString[0] > 0) { @@ -198,7 +193,7 @@ class Hasher implements IHasher { return password_needs_rehash($hash, $algorithm, $this->options); } - private function getPrefferedAlgorithm() { + private function getPrefferedAlgorithm(): string { $default = PASSWORD_BCRYPT; if (\defined('PASSWORD_ARGON2I')) { $default = PASSWORD_ARGON2I; diff --git a/lib/private/Security/Normalizer/IpAddress.php b/lib/private/Security/Normalizer/IpAddress.php index 98d85ce07a1..f8e55370da7 100644 --- a/lib/private/Security/Normalizer/IpAddress.php +++ b/lib/private/Security/Normalizer/IpAddress.php @@ -37,43 +37,18 @@ namespace OC\Security\Normalizer; * @package OC\Security\Normalizer */ class IpAddress { - /** @var string */ - private $ip; - /** - * @param string $ip IP to normalized + * @param string $ip IP to normalize */ - public function __construct(string $ip) { - $this->ip = $ip; + public function __construct( + private string $ip, + ) { } /** - * Return the given subnet for an IPv4 address and mask bits - * - * @param string $ip - * @param int $maskBits - * @return string - */ - private function getIPv4Subnet(string $ip, int $maskBits = 32): string { - $binary = \inet_pton($ip); - for ($i = 32; $i > $maskBits; $i -= 8) { - $j = \intdiv($i, 8) - 1; - $k = \min(8, $i - $maskBits); - $mask = (0xff - ((2 ** $k) - 1)); - $int = \unpack('C', $binary[$j]); - $binary[$j] = \pack('C', $int[1] & $mask); - } - return \inet_ntop($binary).'/'.$maskBits; - } - - /** - * Return the given subnet for an IPv6 address and mask bits - * - * @param string $ip - * @param int $maskBits - * @return string + * Return the given subnet for an IPv6 address (64 first bits) */ - private function getIPv6Subnet(string $ip, int $maskBits = 48): string { + private function getIPv6Subnet(string $ip): string { if ($ip[0] === '[' && $ip[-1] === ']') { // If IP is with brackets, for example [::1] $ip = substr($ip, 1, strlen($ip) - 2); } @@ -81,15 +56,11 @@ class IpAddress { if ($pos !== false) { $ip = substr($ip, 0, $pos - 1); } + $binary = \inet_pton($ip); - for ($i = 128; $i > $maskBits; $i -= 8) { - $j = \intdiv($i, 8) - 1; - $k = \min(8, $i - $maskBits); - $mask = (0xff - ((2 ** $k) - 1)); - $int = \unpack('C', $binary[$j]); - $binary[$j] = \pack('C', $int[1] & $mask); - } - return \inet_ntop($binary).'/'.$maskBits; + $mask = inet_pton('FFFF:FFFF:FFFF:FFFF::'); + + return inet_ntop($binary & $mask).'/64'; } /** @@ -103,58 +74,34 @@ class IpAddress { if (!$binary) { return null; } - for ($i = 0; $i <= 9; $i++) { - if (unpack('C', $binary[$i])[1] !== 0) { - return null; - } - } - for ($i = 10; $i <= 11; $i++) { - if (unpack('C', $binary[$i])[1] !== 255) { - return null; - } - } - - $binary4 = ''; - for ($i = 12; $i < 16; $i++) { - $binary4 .= $binary[$i]; + $mask = inet_pton('::FFFF:FFFF'); + if (($binary & ~$mask) !== inet_pton('::FFFF:0.0.0.0')) { + return null; } - return inet_ntop($binary4); + return inet_ntop(substr($binary, -4)); } /** * Gets either the /32 (IPv4) or the /64 (IPv6) subnet of an IP address - * - * @return string */ public function getSubnet(): string { - if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $this->ip)) { - return $this->getIPv4Subnet( - $this->ip, - 32 - ); + if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return $this->ip.'/32'; } $ipv4 = $this->getEmbeddedIpv4($this->ip); if ($ipv4 !== null) { - return $this->getIPv4Subnet( - $ipv4, - 32 - ); + return $ipv4.'/32'; } - return $this->getIPv6Subnet( - $this->ip, - 64 - ); + return $this->getIPv6Subnet($this->ip); } /** * Returns the specified IP address - * - * @return string */ public function __toString(): string { return $this->ip; diff --git a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php index 08091e997ca..baf74927886 100644 --- a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php +++ b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php @@ -27,8 +27,9 @@ namespace OC\Security\RateLimiting\Exception; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OCP\AppFramework\Http; +use OCP\Security\RateLimiting\IRateLimitExceededException; -class RateLimitExceededException extends SecurityException { +class RateLimitExceededException extends SecurityException implements IRateLimitExceededException { public function __construct() { parent::__construct('Rate limit exceeded', Http::STATUS_TOO_MANY_REQUESTS); } diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php index c8c0e2ce101..689e7b14558 100644 --- a/lib/private/Security/RateLimiting/Limiter.php +++ b/lib/private/Security/RateLimiting/Limiter.php @@ -30,8 +30,9 @@ use OC\Security\Normalizer\IpAddress; use OC\Security\RateLimiting\Backend\IBackend; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OCP\IUser; +use OCP\Security\RateLimiting\ILimiter; -class Limiter { +class Limiter implements ILimiter { public function __construct( private IBackend $backend, ) { diff --git a/lib/private/Security/RemoteHostValidator.php b/lib/private/Security/RemoteHostValidator.php index 38129fbd81b..385b38cff98 100644 --- a/lib/private/Security/RemoteHostValidator.php +++ b/lib/private/Security/RemoteHostValidator.php @@ -38,19 +38,12 @@ use function urldecode; * @internal */ final class RemoteHostValidator implements IRemoteHostValidator { - private IConfig $config; - private HostnameClassifier $hostnameClassifier; - private IpAddressClassifier $ipAddressClassifier; - private LoggerInterface $logger; - - public function __construct(IConfig $config, - HostnameClassifier $hostnameClassifier, - IpAddressClassifier $ipAddressClassifier, - LoggerInterface $logger) { - $this->config = $config; - $this->hostnameClassifier = $hostnameClassifier; - $this->ipAddressClassifier = $ipAddressClassifier; - $this->logger = $logger; + public function __construct( + private IConfig $config, + private HostnameClassifier $hostnameClassifier, + private IpAddressClassifier $ipAddressClassifier, + private LoggerInterface $logger, + ) { } public function isValid(string $host): bool { diff --git a/lib/private/Security/SecureRandom.php b/lib/private/Security/SecureRandom.php index cbd1dc8db6d..f5bc5ddfb5e 100644 --- a/lib/private/Security/SecureRandom.php +++ b/lib/private/Security/SecureRandom.php @@ -44,11 +44,12 @@ class SecureRandom implements ISecureRandom { * @param int $length The length of the generated string * @param string $characters An optional list of characters to use if no character list is * specified all valid base64 characters are used. - * @return string * @throws \LengthException if an invalid length is requested */ - public function generate(int $length, - string $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'): string { + public function generate( + int $length, + string $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + ): string { if ($length <= 0) { throw new \LengthException('Invalid length specified: ' . $length . ' must be bigger than 0'); } diff --git a/lib/private/Security/TrustedDomainHelper.php b/lib/private/Security/TrustedDomainHelper.php index ca6a5cba073..e91f230a9c9 100644 --- a/lib/private/Security/TrustedDomainHelper.php +++ b/lib/private/Security/TrustedDomainHelper.php @@ -34,19 +34,13 @@ use OCP\IConfig; use OCP\Security\ITrustedDomainHelper; class TrustedDomainHelper implements ITrustedDomainHelper { - /** @var IConfig */ - private $config; - - /** - * @param IConfig $config - */ - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private IConfig $config, + ) { } /** * Strips a potential port from a domain (in format domain:port) - * @param string $host * @return string $host without appended port */ private function getDomainWithoutPort(string $host): string { diff --git a/lib/private/Server.php b/lib/private/Server.php index e8ade23d8fe..2dd97412eba 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -102,6 +102,7 @@ use OC\Files\Storage\StorageFactory; use OC\Files\Template\TemplateManager; use OC\Files\Type\Loader; use OC\Files\View; +use OC\FilesMetadata\FilesMetadataManager; use OC\FullTextSearch\FullTextSearchManager; use OC\Http\Client\ClientService; use OC\Http\Client\NegativeDnsCache; @@ -120,14 +121,14 @@ use OC\Log\PsrLoggerAdapter; use OC\Mail\Mailer; use OC\Memcache\ArrayCache; use OC\Memcache\Factory; -use OC\Metadata\Capabilities as MetadataCapabilities; -use OC\Metadata\IMetadataManager; -use OC\Metadata\MetadataManager; use OC\Notification\Manager; +use OC\OCM\Model\OCMProvider; +use OC\OCM\OCMDiscoveryService; use OC\OCS\DiscoveryService; use OC\Preview\GeneratorHelper; use OC\Preview\IMagickSupport; use OC\Preview\MimeIconProvider; +use OC\Profile\ProfileManager; use OC\Remote\Api\ApiFactory; use OC\Remote\InstanceFactory; use OC\RichObjectStrings\Validator; @@ -142,11 +143,14 @@ use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSRF\CsrfTokenManager; use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; +use OC\Security\RateLimiting\Limiter; use OC\Security\SecureRandom; use OC\Security\TrustedDomainHelper; use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; +use OC\SetupCheck\SetupCheckManager; use OC\Share20\ProviderFactory; +use OC\Share20\ShareDisableChecker; use OC\Share20\ShareHelper; use OC\SpeechToText\SpeechToTextManager; use OC\SystemTag\ManagerFactory as SystemTagManagerFactory; @@ -154,6 +158,7 @@ use OC\Tagging\TagMapper; use OC\Talk\Broker; use OC\Template\JSCombiner; use OC\Translation\TranslationManager; +use OC\User\AvailabilityCoordinator; use OC\User\DisplayNameCache; use OC\User\Listeners\BeforeUserDeletedListener; use OC\User\Listeners\UserChangedListener; @@ -190,6 +195,7 @@ use OCP\Files\Lock\ILockManager; use OCP\Files\Mount\IMountManager; use OCP\Files\Storage\IStorageFactory; use OCP\Files\Template\ITemplateManager; +use OCP\FilesMetadata\IFilesMetadataManager; use OCP\FullTextSearch\IFullTextSearchManager; use OCP\GlobalScale\IConfig; use OCP\Group\ISubAdmin; @@ -209,6 +215,7 @@ use OCP\IInitialStateService; use OCP\IL10N; use OCP\ILogger; use OCP\INavigationManager; +use OCP\IPhoneNumberUtil; use OCP\IPreview; use OCP\IRequest; use OCP\IRequestId; @@ -227,6 +234,9 @@ use OCP\Lock\ILockingProvider; use OCP\Lockdown\ILockdownManager; use OCP\Log\ILogFactory; use OCP\Mail\IMailer; +use OCP\OCM\IOCMDiscoveryService; +use OCP\OCM\IOCMProvider; +use OCP\Profile\IProfileManager; use OCP\Remote\Api\IApiFactory; use OCP\Remote\IInstanceFactory; use OCP\RichObjectStrings\IValidator; @@ -238,7 +248,9 @@ use OCP\Security\ICrypto; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use OCP\Security\ITrustedDomainHelper; +use OCP\Security\RateLimiting\ILimiter; use OCP\Security\VerificationToken\IVerificationToken; +use OCP\SetupCheck\ISetupCheckManager; use OCP\Share\IShareHelper; use OCP\SpeechToText\ISpeechToTextManager; use OCP\SystemTag\ISystemTagManager; @@ -254,6 +266,7 @@ use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserLoggedInEvent; use OCP\User\Events\UserLoggedInWithCookieEvent; use OCP\User\Events\UserLoggedOutEvent; +use OCP\User\IAvailabilityCoordinator; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -785,8 +798,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('Search', ISearch::class); $this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) { - $cacheFactory = $c->get(ICacheFactory::class); - if ($cacheFactory->isAvailable()) { + $config = $c->get(\OCP\IConfig::class); + if (ltrim($config->getSystemValueString('memcache.distributed', ''), '\\') === \OC\Memcache\Redis::class) { $backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend( $c->get(AllConfig::class), $this->get(ICacheFactory::class), @@ -1122,9 +1135,6 @@ class Server extends ServerContainer implements IServerContainer { $manager->registerCapability(function () use ($c) { return $c->get(\OC\Security\Bruteforce\Capabilities::class); }); - $manager->registerCapability(function () use ($c) { - return $c->get(MetadataCapabilities::class); - }); return $manager; }); /** @deprecated 19.0.0 */ @@ -1167,7 +1177,7 @@ class Server extends ServerContainer implements IServerContainer { $c->getAppDataDir('theming'), $c->get(IURLGenerator::class), $this->get(ICacheFactory::class), - $this->get(ILogger::class), + $this->get(LoggerInterface::class), $this->get(ITempManager::class) ); return new ThemingDefaults( @@ -1252,7 +1262,8 @@ class Server extends ServerContainer implements IServerContainer { $c->get('ThemingDefaults'), $c->get(IEventDispatcher::class), $c->get(IUserSession::class), - $c->get(KnownUserService::class) + $c->get(KnownUserService::class), + $c->get(ShareDisableChecker::class) ); return $manager; @@ -1304,6 +1315,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(IClientService::class) ); }); + $this->registerAlias(IOCMDiscoveryService::class, OCMDiscoveryService::class); $this->registerService(ICloudIdManager::class, function (ContainerInterface $c) { return new CloudIdManager( @@ -1319,9 +1331,11 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService(ICloudFederationProviderManager::class, function (ContainerInterface $c) { return new CloudFederationProviderManager( + $c->get(\OCP\IConfig::class), $c->get(IAppManager::class), $c->get(IClientService::class), $c->get(ICloudIdManager::class), + $c->get(IOCMDiscoveryService::class), $c->get(LoggerInterface::class) ); }); @@ -1385,6 +1399,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\Dashboard\IManager::class, \OC\Dashboard\Manager::class); $this->registerAlias(IFullTextSearchManager::class, FullTextSearchManager::class); + $this->registerAlias(IFilesMetadataManager::class, FilesMetadataManager::class); $this->registerAlias(ISubAdmin::class, SubAdmin::class); @@ -1396,8 +1411,6 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(IBroker::class, Broker::class); - $this->registerAlias(IMetadataManager::class, MetadataManager::class); - $this->registerAlias(\OCP\Files\AppData\IAppDataFactory::class, \OC\Files\AppData\Factory::class); $this->registerAlias(IBinaryFinder::class, BinaryFinder::class); @@ -1412,6 +1425,20 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\TextProcessing\IManager::class, \OC\TextProcessing\Manager::class); + $this->registerAlias(\OCP\TextToImage\IManager::class, \OC\TextToImage\Manager::class); + + $this->registerAlias(ILimiter::class, Limiter::class); + + $this->registerAlias(IPhoneNumberUtil::class, PhoneNumberUtil::class); + + $this->registerAlias(IOCMProvider::class, OCMProvider::class); + + $this->registerAlias(ISetupCheckManager::class, SetupCheckManager::class); + + $this->registerAlias(IProfileManager::class, ProfileManager::class); + + $this->registerAlias(IAvailabilityCoordinator::class, AvailabilityCoordinator::class); + $this->connectDispatcher(); } @@ -1452,6 +1479,8 @@ class Server extends ServerContainer implements IServerContainer { $eventDispatcher->addServiceListener(PostLoginEvent::class, UserLoggedInListener::class); $eventDispatcher->addServiceListener(UserChangedEvent::class, UserChangedListener::class); $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, BeforeUserDeletedListener::class); + + FilesMetadataManager::loadListeners($eventDispatcher); } /** diff --git a/lib/private/Session/CryptoSessionData.php b/lib/private/Session/CryptoSessionData.php index 1eb6987fc18..ae4b80209d5 100644 --- a/lib/private/Session/CryptoSessionData.php +++ b/lib/private/Session/CryptoSessionData.php @@ -32,6 +32,8 @@ namespace OC\Session; use OCP\ISession; use OCP\Security\ICrypto; use OCP\Session\Exceptions\SessionNotAvailableException; +use function json_decode; +use function OCP\Log\logger; /** * Class CryptoSessionData @@ -79,14 +81,24 @@ class CryptoSessionData implements \ArrayAccess, ISession { protected function initializeSession() { $encryptedSessionData = $this->session->get(self::encryptedSessionName) ?: ''; - try { - $this->sessionValues = json_decode( - $this->crypto->decrypt($encryptedSessionData, $this->passphrase), - true - ); - } catch (\Exception $e) { + if ($encryptedSessionData === '') { + // Nothing to decrypt $this->sessionValues = []; - $this->regenerateId(true, false); + } else { + try { + $this->sessionValues = json_decode( + $this->crypto->decrypt($encryptedSessionData, $this->passphrase), + true, + 512, + JSON_THROW_ON_ERROR, + ); + } catch (\Exception $e) { + logger('core')->critical('Could not decrypt or decode encrypted session data', [ + 'exception' => $e, + ]); + $this->sessionValues = []; + $this->regenerateId(true, false); + } } } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 0993fe54f47..4fea5d72591 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -60,6 +60,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\IGroup; use OCP\IL10N; +use OCP\Migration\IOutput; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; @@ -275,7 +276,7 @@ class Setup { * @param $options * @return array */ - public function install($options) { + public function install($options, ?IOutput $output = null) { $l = $this->l10n; $error = []; @@ -349,6 +350,7 @@ class Setup { $this->config->setValues($newConfigValues); + $this->outputDebug($output, 'Configuring database'); $dbSetup->initialize($options); try { $dbSetup->setupDatabase($username); @@ -367,9 +369,11 @@ class Setup { ]; return $error; } + + $this->outputDebug($output, 'Run server migrations'); try { // apply necessary migrations - $dbSetup->runMigrations(); + $dbSetup->runMigrations($output); } catch (Exception $e) { $error[] = [ 'error' => 'Error while trying to initialise the database: ' . $e->getMessage(), @@ -379,6 +383,7 @@ class Setup { return $error; } + $this->outputDebug($output, 'Create admin user'); //create the user and group $user = null; try { @@ -407,16 +412,19 @@ class Setup { } // Install shipped apps and specified app bundles - Installer::installShippedApps(); + $this->outputDebug($output, 'Install default apps'); + Installer::installShippedApps(false, $output); // create empty file in data dir, so we can later find // out that this is indeed an ownCloud data directory + $this->outputDebug($output, 'Setup data directory'); file_put_contents($config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', ''); // Update .htaccess files self::updateHtaccess(); self::protectDataDirectory(); + $this->outputDebug($output, 'Install background jobs'); self::installBackgroundJobs(); //and we are done @@ -531,13 +539,13 @@ class Setup { $content .= "\n Options -MultiViews"; $content .= "\n RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]"; $content .= "\n RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]"; - $content .= "\n RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|png|html|ttf|woff2?|ico|jpg|jpeg|map|webm|mp4|mp3|ogg|wav|wasm|tflite)$"; + $content .= "\n RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|png|html|ttf|woff2?|ico|jpg|jpeg|map|webm|mp4|mp3|ogg|wav|flac|wasm|tflite)$"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\\.php"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\\.ico|manifest\\.json)$"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\\.php"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\\.php"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/robots\\.txt"; - $content .= "\n RewriteCond %{REQUEST_FILENAME} !/(ocm-provider|ocs-provider|updater)/"; + $content .= "\n RewriteCond %{REQUEST_FILENAME} !/(ocs-provider|updater)/"; $content .= "\n RewriteCond %{REQUEST_URI} !^/\\.well-known/(acme-challenge|pki-validation)/.*"; $content .= "\n RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$"; $content .= "\n RewriteRule . index.php [PT,E=PATH_INFO:$1]"; @@ -552,6 +560,14 @@ class Setup { } if ($content !== '') { + // Never write file back if disk space should be too low + if (function_exists('disk_free_space')) { + $df = disk_free_space(\OC::$SERVERROOT); + $size = strlen($content) + 10240; + if ($df !== false && $df < (float)$size) { + throw new \Exception(\OC::$SERVERROOT . " does not have enough space for writing the htaccess file! Not writing it back!"); + } + } //suppress errors in case we don't have permissions for it return (bool)@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent . $content . "\n"); } @@ -616,4 +632,10 @@ class Setup { public function canInstallFileExists() { return is_file(\OC::$configDir.'/CAN_INSTALL'); } + + protected function outputDebug(?IOutput $output, string $message): void { + if ($output instanceof IOutput) { + $output->debug($message); + } + } } diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index 9ec4137cdef..6bef40338c9 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -33,6 +33,7 @@ use OC\DB\ConnectionFactory; use OC\DB\MigrationService; use OC\SystemConfig; use OCP\IL10N; +use OCP\Migration\IOutput; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; @@ -88,7 +89,7 @@ abstract class AbstractDatabase { $dbName = $config['dbname']; $dbHost = !empty($config['dbhost']) ? $config['dbhost'] : 'localhost'; $dbPort = !empty($config['dbport']) ? $config['dbport'] : ''; - $dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_'; + $dbTablePrefix = $config['dbtableprefix'] ?? 'oc_'; $createUserConfig = $this->config->getValue("setup_create_db_user", true); // accept `false` both as bool and string, since setting config values from env will result in a string @@ -150,11 +151,11 @@ abstract class AbstractDatabase { */ abstract public function setupDatabase($username); - public function runMigrations() { + public function runMigrations(?IOutput $output = null) { if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) { return; } - $ms = new MigrationService('core', \OC::$server->get(Connection::class)); + $ms = new MigrationService('core', \OC::$server->get(Connection::class), $output); $ms->migrate('latest', true); } } diff --git a/lib/private/SetupCheck/SetupCheckManager.php b/lib/private/SetupCheck/SetupCheckManager.php new file mode 100644 index 00000000000..b8b6cfa11e7 --- /dev/null +++ b/lib/private/SetupCheck/SetupCheckManager.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Côme Chilliet <come.chilliet@nextcloud.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\SetupCheck; + +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\Server; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\ISetupCheckManager; +use Psr\Log\LoggerInterface; + +class SetupCheckManager implements ISetupCheckManager { + public function __construct( + private Coordinator $coordinator, + private LoggerInterface $logger, + ) { + } + + public function runAll(): array { + $results = []; + $setupChecks = $this->coordinator->getRegistrationContext()->getSetupChecks(); + foreach ($setupChecks as $setupCheck) { + /** @var ISetupCheck $setupCheckObject */ + $setupCheckObject = Server::get($setupCheck->getService()); + $this->logger->debug('Running check '.get_class($setupCheckObject)); + $setupResult = $setupCheckObject->run(); + $setupResult->setName($setupCheckObject->getName()); + $category = $setupCheckObject->getCategory(); + $results[$category] ??= []; + $results[$category][$setupCheckObject::class] = $setupResult; + } + return $results; + } +} diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 5201cf074b1..55ac3eda644 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -38,12 +38,12 @@ use OC\Files\Cache\Cache; use OC\Share20\Exception\BackendError; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Defaults; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\Node; -use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IURLGenerator; @@ -90,19 +90,19 @@ class DefaultShareProvider implements IShareProvider { /** @var IURLGenerator */ private $urlGenerator; - /** @var IConfig */ - private $config; + private ITimeFactory $timeFactory; public function __construct( - IDBConnection $connection, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IMailer $mailer, - Defaults $defaults, - IFactory $l10nFactory, - IURLGenerator $urlGenerator, - IConfig $config) { + IDBConnection $connection, + IUserManager $userManager, + IGroupManager $groupManager, + IRootFolder $rootFolder, + IMailer $mailer, + Defaults $defaults, + IFactory $l10nFactory, + IURLGenerator $urlGenerator, + ITimeFactory $timeFactory, + ) { $this->dbConn = $connection; $this->userManager = $userManager; $this->groupManager = $groupManager; @@ -111,7 +111,7 @@ class DefaultShareProvider implements IShareProvider { $this->defaults = $defaults; $this->l10nFactory = $l10nFactory; $this->urlGenerator = $urlGenerator; - $this->config = $config; + $this->timeFactory = $timeFactory; } /** @@ -216,32 +216,22 @@ class DefaultShareProvider implements IShareProvider { } // Set the time this share was created - $qb->setValue('stime', $qb->createNamedParameter(time())); + $shareTime = $this->timeFactory->now(); + $qb->setValue('stime', $qb->createNamedParameter($shareTime->getTimestamp())); // insert the data and fetch the id of the share - $this->dbConn->beginTransaction(); - $qb->execute(); - $id = $this->dbConn->lastInsertId('*PREFIX*share'); - - // Now fetch the inserted share and create a complete share object - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + $qb->executeStatement(); - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $this->dbConn->commit(); - $cursor->closeCursor(); + // Update mandatory data + $id = $qb->getLastInsertId(); + $share->setId((string)$id); + $share->setProviderId($this->identifier()); - if ($data === false) { - throw new ShareNotFound('Newly created share could not be found'); - } + $share->setShareTime(\DateTime::createFromImmutable($shareTime)); $mailSendValue = $share->getMailSend(); - $data['mail_send'] = ($mailSendValue === null) ? true : $mailSendValue; + $share->setMailSend(($mailSendValue === null) ? true : $mailSendValue); - $share = $this->createShare($data); return $share; } @@ -1374,7 +1364,7 @@ class DefaultShareProvider implements IShareProvider { $type = (int)$row['share_type']; if ($type === IShare::TYPE_USER) { $uid = $row['share_with']; - $users[$uid] = isset($users[$uid]) ? $users[$uid] : []; + $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } elseif ($type === IShare::TYPE_GROUP) { $gid = $row['share_with']; @@ -1387,14 +1377,14 @@ class DefaultShareProvider implements IShareProvider { $userList = $group->getUsers(); foreach ($userList as $user) { $uid = $user->getUID(); - $users[$uid] = isset($users[$uid]) ? $users[$uid] : []; + $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } } elseif ($type === IShare::TYPE_LINK) { $link = true; } elseif ($type === IShare::TYPE_USERGROUP && $currentAccess === true) { $uid = $row['share_with']; - $users[$uid] = isset($users[$uid]) ? $users[$uid] : []; + $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index b03608f9872..4606101b7e6 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -41,7 +41,6 @@ */ namespace OC\Share20; -use OCP\Cache\CappedMemoryCache; use OC\Files\Mount\MoveableMount; use OC\KnownUser\KnownUserService; use OC\Share20\Exception\ProviderException; @@ -106,8 +105,6 @@ class Manager implements IManager { private $userManager; /** @var IRootFolder */ private $rootFolder; - /** @var CappedMemoryCache */ - private $sharingDisabledForUsersCache; /** @var LegacyHooks */ private $legacyHooks; /** @var IMailer */ @@ -122,6 +119,7 @@ class Manager implements IManager { private $userSession; /** @var KnownUserService */ private $knownUserService; + private ShareDisableChecker $shareDisableChecker; public function __construct( LoggerInterface $logger, @@ -140,7 +138,8 @@ class Manager implements IManager { \OC_Defaults $defaults, IEventDispatcher $dispatcher, IUserSession $userSession, - KnownUserService $knownUserService + KnownUserService $knownUserService, + ShareDisableChecker $shareDisableChecker ) { $this->logger = $logger; $this->config = $config; @@ -153,7 +152,6 @@ class Manager implements IManager { $this->factory = $factory; $this->userManager = $userManager; $this->rootFolder = $rootFolder; - $this->sharingDisabledForUsersCache = new CappedMemoryCache(); // The constructor of LegacyHooks registers the listeners of share events // do not remove if those are not properly migrated $this->legacyHooks = new LegacyHooks($dispatcher); @@ -163,6 +161,7 @@ class Manager implements IManager { $this->dispatcher = $dispatcher; $this->userSession = $userSession; $this->knownUserService = $knownUserService; + $this->shareDisableChecker = $shareDisableChecker; } /** @@ -2025,37 +2024,7 @@ class Manager implements IManager { * @return bool */ public function sharingDisabledForUser($userId) { - if ($userId === null) { - return false; - } - - if (isset($this->sharingDisabledForUsersCache[$userId])) { - return $this->sharingDisabledForUsersCache[$userId]; - } - - if ($this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') { - $groupsList = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); - $excludedGroups = json_decode($groupsList); - if (is_null($excludedGroups)) { - $excludedGroups = explode(',', $groupsList); - $newValue = json_encode($excludedGroups); - $this->config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue); - } - $user = $this->userManager->get($userId); - $usersGroups = $this->groupManager->getUserGroupIds($user); - if (!empty($usersGroups)) { - $remainingGroups = array_diff($usersGroups, $excludedGroups); - // if the user is only in groups which are disabled for sharing then - // sharing is also disabled for the user - if (empty($remainingGroups)) { - $this->sharingDisabledForUsersCache[$userId] = true; - return true; - } - } - } - - $this->sharingDisabledForUsersCache[$userId] = false; - return false; + return $this->shareDisableChecker->sharingDisabledForUser($userId); } /** diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index aaab5a3fd88..dbf1b21dabe 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -41,6 +41,7 @@ use OCA\FederatedFileSharing\TokenHandler; use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\ShareByMailProvider; use OCA\Talk\Share\RoomShareProvider; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; use OCP\IServerContainer; @@ -104,7 +105,7 @@ class ProviderFactory implements IProviderFactory { $this->serverContainer->query(Defaults::class), $this->serverContainer->getL10NFactory(), $this->serverContainer->getURLGenerator(), - $this->serverContainer->getConfig() + $this->serverContainer->query(ITimeFactory::class), ); } diff --git a/lib/private/Share20/ShareDisableChecker.php b/lib/private/Share20/ShareDisableChecker.php new file mode 100644 index 00000000000..9d0c2b8c2b4 --- /dev/null +++ b/lib/private/Share20/ShareDisableChecker.php @@ -0,0 +1,65 @@ +<?php + +namespace OC\Share20; + +use OCP\Cache\CappedMemoryCache; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUserManager; + +/** + * split of from the share manager to allow using it with minimal DI + */ +class ShareDisableChecker { + private CappedMemoryCache $sharingDisabledForUsersCache; + + public function __construct( + private IConfig $config, + private IUserManager $userManager, + private IGroupManager $groupManager, + ) { + $this->sharingDisabledForUsersCache = new CappedMemoryCache(); + } + + + /** + * @param ?string $userId + * @return bool + */ + public function sharingDisabledForUser(?string $userId) { + if ($userId === null) { + return false; + } + + if (isset($this->sharingDisabledForUsersCache[$userId])) { + return $this->sharingDisabledForUsersCache[$userId]; + } + + if ($this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') { + $groupsList = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); + $excludedGroups = json_decode($groupsList); + if (is_null($excludedGroups)) { + $excludedGroups = explode(',', $groupsList); + $newValue = json_encode($excludedGroups); + $this->config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue); + } + $user = $this->userManager->get($userId); + if (!$user) { + return false; + } + $usersGroups = $this->groupManager->getUserGroupIds($user); + if (!empty($usersGroups)) { + $remainingGroups = array_diff($usersGroups, $excludedGroups); + // if the user is only in groups which are disabled for sharing then + // sharing is also disabled for the user + if (empty($remainingGroups)) { + $this->sharingDisabledForUsersCache[$userId] = true; + return true; + } + } + } + + $this->sharingDisabledForUsersCache[$userId] = false; + return false; + } +} diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index a18c4c2a138..c104f001809 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -118,6 +118,9 @@ class SystemConfig { ], ], ], + 'onlyoffice' => [ + 'jwt_secret' => true, + ], ]; /** @var Config */ diff --git a/lib/private/SystemTag/ManagerFactory.php b/lib/private/SystemTag/ManagerFactory.php index 6670922407e..d8f7d4d772b 100644 --- a/lib/private/SystemTag/ManagerFactory.php +++ b/lib/private/SystemTag/ManagerFactory.php @@ -40,25 +40,16 @@ use OCP\SystemTag\ISystemTagObjectMapper; */ class ManagerFactory implements ISystemTagManagerFactory { /** - * Server container - * - * @var IServerContainer - */ - private $serverContainer; - - /** * Constructor for the system tag manager factory - * - * @param IServerContainer $serverContainer server container */ - public function __construct(IServerContainer $serverContainer) { - $this->serverContainer = $serverContainer; + public function __construct( + private IServerContainer $serverContainer, + ) { } /** * Creates and returns an instance of the system tag manager * - * @return ISystemTagManager * @since 9.0.0 */ public function getManager(): ISystemTagManager { @@ -73,7 +64,6 @@ class ManagerFactory implements ISystemTagManagerFactory { * Creates and returns an instance of the system tag object * mapper * - * @return ISystemTagObjectMapper * @since 9.0.0 */ public function getObjectMapper(): ISystemTagObjectMapper { diff --git a/lib/private/SystemTag/SystemTag.php b/lib/private/SystemTag/SystemTag.php index da6d4bd4b11..cd8010201d3 100644 --- a/lib/private/SystemTag/SystemTag.php +++ b/lib/private/SystemTag/SystemTag.php @@ -30,39 +30,12 @@ namespace OC\SystemTag; use OCP\SystemTag\ISystemTag; class SystemTag implements ISystemTag { - /** - * @var string - */ - private $id; - - /** - * @var string - */ - private $name; - - /** - * @var bool - */ - private $userVisible; - - /** - * @var bool - */ - private $userAssignable; - - /** - * Constructor. - * - * @param string $id tag id - * @param string $name tag name - * @param bool $userVisible whether the tag is user visible - * @param bool $userAssignable whether the tag is user assignable - */ - public function __construct(string $id, string $name, bool $userVisible, bool $userAssignable) { - $this->id = $id; - $this->name = $name; - $this->userVisible = $userVisible; - $this->userAssignable = $userAssignable; + public function __construct( + private string $id, + private string $name, + private bool $userVisible, + private bool $userAssignable, + ) { } /** @@ -97,14 +70,14 @@ class SystemTag implements ISystemTag { * {@inheritdoc} */ public function getAccessLevel(): int { - if ($this->userVisible) { - if ($this->userAssignable) { - return self::ACCESS_LEVEL_PUBLIC; - } else { - return self::ACCESS_LEVEL_RESTRICTED; - } - } else { + if (!$this->userVisible) { return self::ACCESS_LEVEL_INVISIBLE; } + + if (!$this->userAssignable) { + return self::ACCESS_LEVEL_RESTRICTED; + } + + return self::ACCESS_LEVEL_PUBLIC; } } diff --git a/lib/private/SystemTag/SystemTagManager.php b/lib/private/SystemTag/SystemTagManager.php index c52c350b6f8..67e1a7d921f 100644 --- a/lib/private/SystemTag/SystemTagManager.php +++ b/lib/private/SystemTag/SystemTagManager.php @@ -50,10 +50,8 @@ class SystemTagManager implements ISystemTagManager { /** * Prepared query for selecting tags directly - * - * @var \OCP\DB\QueryBuilder\IQueryBuilder */ - private $selectTagQuery; + private IQueryBuilder $selectTagQuery; public function __construct( protected IDBConnection $connection, @@ -219,7 +217,12 @@ class SystemTagManager implements ISystemTagManager { /** * {@inheritdoc} */ - public function updateTag(string $tagId, string $newName, bool $userVisible, bool $userAssignable) { + public function updateTag( + string $tagId, + string $newName, + bool $userVisible, + bool $userAssignable, + ): void { try { $tags = $this->getTagsByIds($tagId); } catch (TagNotFoundException $e) { @@ -271,7 +274,7 @@ class SystemTagManager implements ISystemTagManager { /** * {@inheritdoc} */ - public function deleteTags($tagIds) { + public function deleteTags($tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -363,14 +366,14 @@ class SystemTagManager implements ISystemTagManager { return false; } - private function createSystemTagFromRow($row) { + private function createSystemTagFromRow($row): SystemTag { return new SystemTag((string)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable']); } /** * {@inheritdoc} */ - public function setTagGroups(ISystemTag $tag, array $groupIds) { + public function setTagGroups(ISystemTag $tag, array $groupIds): void { // delete relationships first $this->connection->beginTransaction(); try { diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php index 66a21e58609..614d0274add 100644 --- a/lib/private/SystemTag/SystemTagObjectMapper.php +++ b/lib/private/SystemTag/SystemTagObjectMapper.php @@ -81,7 +81,6 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { $result->closeCursor(); } - return $mapping; } @@ -128,7 +127,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { /** * {@inheritdoc} */ - public function assignTags(string $objId, string $objectType, $tagIds) { + public function assignTags(string $objId, string $objectType, $tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -169,7 +168,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { /** * {@inheritdoc} */ - public function unassignTags(string $objId, string $objectType, $tagIds) { + public function unassignTags(string $objId, string $objectType, $tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -241,7 +240,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { * * @throws \OCP\SystemTag\TagNotFoundException if at least one tag did not exist */ - private function assertTagsExist($tagIds) { + private function assertTagsExist(array $tagIds): void { $tags = $this->tagManager->getTagsByIds($tagIds); if (\count($tags) !== \count($tagIds)) { // at least one tag missing, bail out diff --git a/lib/private/SystemTag/SystemTagsInFilesDetector.php b/lib/private/SystemTag/SystemTagsInFilesDetector.php index c9f26c58c02..044322733ea 100644 --- a/lib/private/SystemTag/SystemTagsInFilesDetector.php +++ b/lib/private/SystemTag/SystemTagsInFilesDetector.php @@ -36,7 +36,9 @@ use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; class SystemTagsInFilesDetector { - public function __construct(protected QuerySearchHelper $searchHelper) { + public function __construct( + protected QuerySearchHelper $searchHelper, + ) { } public function detectAssignedSystemTagsIn( diff --git a/lib/private/Tags.php b/lib/private/Tags.php index 8da1e7edc3b..766166beadd 100644 --- a/lib/private/Tags.php +++ b/lib/private/Tags.php @@ -529,7 +529,7 @@ class Tags implements ITags { if (is_string($tag) && !is_numeric($tag)) { $tag = trim($tag); if ($tag === '') { - \OCP\Util::writeLog('core', __METHOD__.', Cannot add an empty tag', ILogger::DEBUG); + $this->logger->debug(__METHOD__.', Cannot add an empty tag'); return false; } if (!$this->hasTag($tag)) { @@ -569,7 +569,7 @@ class Tags implements ITags { if (is_string($tag) && !is_numeric($tag)) { $tag = trim($tag); if ($tag === '') { - \OCP\Util::writeLog('core', __METHOD__.', Tag name is empty', ILogger::DEBUG); + $this->logger->debug(__METHOD__.', Tag name is empty'); return false; } $tagId = $this->getTagId($tag); @@ -609,8 +609,7 @@ class Tags implements ITags { $names = array_map('trim', $names); array_filter($names); - \OCP\Util::writeLog('core', __METHOD__ . ', before: ' - . print_r($this->tags, true), ILogger::DEBUG); + $this->logger->debug(__METHOD__ . ', before: ' . print_r($this->tags, true)); foreach ($names as $name) { $id = null; @@ -625,8 +624,7 @@ class Tags implements ITags { unset($this->tags[$key]); $this->mapper->delete($tag); } else { - \OCP\Util::writeLog('core', __METHOD__ . 'Cannot delete tag ' . $name - . ': not found.', ILogger::ERROR); + $this->logger->error(__METHOD__ . 'Cannot delete tag ' . $name . ': not found.'); } if (!is_null($id) && $id !== false) { try { diff --git a/lib/private/Template/JSResourceLocator.php b/lib/private/Template/JSResourceLocator.php index b283f0b610f..68a83fa4b73 100644 --- a/lib/private/Template/JSResourceLocator.php +++ b/lib/private/Template/JSResourceLocator.php @@ -60,8 +60,13 @@ class JSResourceLocator extends ResourceLocator { $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.'core/'.$script); $found += $this->appendScriptIfExist($this->serverroot, $script); $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.$script); - $found += $this->appendScriptIfExist($this->serverroot, 'apps/'.$script); - $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.'apps/'.$script); + + foreach (\OC::$APPSROOTS as $appRoot) { + $dirName = basename($appRoot['path']); + $rootPath = dirname($appRoot['path']); + $found += $this->appendScriptIfExist($rootPath, $dirName.'/'.$script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.$dirName.'/'.$script); + } if ($found) { return; diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 658a85152bf..2adf3c5e692 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -47,11 +47,13 @@ use OC\Search\SearchQuery; use OC\Template\CSSResourceLocator; use OC\Template\JSConfigHelper; use OC\Template\JSResourceLocator; +use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; use OCP\IConfig; use OCP\IInitialStateService; use OCP\INavigationManager; +use OCP\IURLGenerator; use OCP\IUserSession; use OCP\Support\Subscription\IRegistry; use OCP\Util; @@ -106,11 +108,18 @@ class TemplateLayout extends \OC_Template { $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); $this->initialState->provideInitialState('core', 'apps', $this->navigationManager->getAll()); - $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); - $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); - $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); - Util::addScript('core', 'unified-search', 'core'); - + /* + * NB : Unified search enabled, defaults to true since new advanced search is + * unstable. Once we think otherwise, the default should be false. + */ + if ($this->config->getSystemValueBool('unified_search.enabled', true)) { + $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); + $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); + $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); + Util::addScript('core', 'unified-search', 'core'); + } else { + Util::addScript('core', 'global-search', 'core'); + } // Set body data-theme $this->assign('enabledThemes', []); if (\OC::$server->getAppManager()->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) { @@ -189,13 +198,31 @@ class TemplateLayout extends \OC_Template { $this->assign('appid', $appId); $this->assign('bodyid', 'body-public'); + // Set logo link target + $logoUrl = $this->config->getSystemValueString('logo_url', ''); + $this->assign('logoUrl', $logoUrl); + /** @var IRegistry $subscription */ $subscription = \OCP\Server::get(IRegistry::class); $showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true); if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) { $showSimpleSignup = false; } + + $defaultSignUpLink = 'https://nextcloud.com/signup/'; + $signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink); + if ($signUpLink !== $defaultSignUpLink) { + $showSimpleSignup = true; + } + + $appManager = \OCP\Server::get(IAppManager::class); + if ($appManager->isEnabledForUser('registration')) { + $urlGenerator = \OCP\Server::get(IURLGenerator::class); + $signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/'); + } + $this->assign('showSimpleSignUpLink', $showSimpleSignup); + $this->assign('signUpLink', $signUpLink); } else { parent::__construct('core', 'layout.base'); } @@ -279,7 +306,7 @@ class TemplateLayout extends \OC_Template { $web = $info[1]; $file = $info[2]; - if (substr($file, -strlen('print.css')) === 'print.css') { + if (str_ends_with($file, 'print.css')) { $this->append('printcssfiles', $web.'/'.$file . $this->getVersionHashSuffix()); } else { $suffix = $this->getVersionHashSuffix($web, $file); diff --git a/lib/private/TextProcessing/Db/Task.php b/lib/private/TextProcessing/Db/Task.php index 9c6f16d11ae..5f362d429f3 100644 --- a/lib/private/TextProcessing/Db/Task.php +++ b/lib/private/TextProcessing/Db/Task.php @@ -45,6 +45,8 @@ use OCP\TextProcessing\Task as OCPTask; * @method string getAppId() * @method setIdentifier(string $identifier) * @method string getIdentifier() + * @method setCompletionExpectedAt(null|\DateTime $completionExpectedAt) + * @method null|\DateTime getCompletionExpectedAt() */ class Task extends Entity { protected $lastUpdated; @@ -55,16 +57,17 @@ class Task extends Entity { protected $userId; protected $appId; protected $identifier; + protected $completionExpectedAt; /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier']; + public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier', 'completion_expected_at']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier']; + public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier', 'completionExpectedAt']; public function __construct() { @@ -78,6 +81,7 @@ class Task extends Entity { $this->addType('userId', 'string'); $this->addType('appId', 'string'); $this->addType('identifier', 'string'); + $this->addType('completionExpectedAt', 'datetime'); } public function toRow(): array { @@ -98,6 +102,7 @@ class Task extends Entity { 'userId' => $task->getUserId(), 'appId' => $task->getAppId(), 'identifier' => $task->getIdentifier(), + 'completionExpectedAt' => $task->getCompletionExpectedAt(), ]); return $task; } @@ -107,6 +112,7 @@ class Task extends Entity { $task->setId($this->getId()); $task->setStatus($this->getStatus()); $task->setOutput($this->getOutput()); + $task->setCompletionExpectedAt($this->getCompletionExpectedAt()); return $task; } } diff --git a/lib/private/TextProcessing/Manager.php b/lib/private/TextProcessing/Manager.php index b9cb06c298e..c98ff893143 100644 --- a/lib/private/TextProcessing/Manager.php +++ b/lib/private/TextProcessing/Manager.php @@ -28,6 +28,8 @@ namespace OC\TextProcessing; use OC\AppFramework\Bootstrap\Coordinator; use OC\TextProcessing\Db\Task as DbTask; use OCP\IConfig; +use OCP\TextProcessing\Exception\TaskFailureException; +use OCP\TextProcessing\IProviderWithExpectedRuntime; use OCP\TextProcessing\Task; use OCP\TextProcessing\Task as OCPTask; use OC\TextProcessing\Db\TaskMapper; @@ -114,26 +116,16 @@ class Manager implements IManager { if (!$this->canHandleTask($task)) { throw new PreConditionNotMetException('No text processing provider is installed that can handle this task'); } - $providers = $this->getProviders(); - $json = $this->config->getAppValue('core', 'ai.textprocessing_provider_preferences', ''); - if ($json !== '') { - $preferences = json_decode($json, true); - if (isset($preferences[$task->getType()])) { - // If a preference for this task type is set, move the preferred provider to the start - $provider = current(array_filter($providers, fn ($provider) => $provider::class === $preferences[$task->getType()])); - if ($provider !== false) { - $providers = array_filter($providers, fn ($p) => $p !== $provider); - array_unshift($providers, $provider); - } - } - } + $providers = $this->getPreferredProviders($task); foreach ($providers as $provider) { - if (!$task->canUseProvider($provider)) { - continue; - } try { $task->setStatus(OCPTask::STATUS_RUNNING); + if ($provider instanceof IProviderWithExpectedRuntime) { + $completionExpectedAt = new \DateTime('now'); + $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S')); + $task->setCompletionExpectedAt($completionExpectedAt); + } if ($task->getId() === null) { $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); $task->setId($taskEntity->getId()); @@ -145,31 +137,37 @@ class Manager implements IManager { $task->setStatus(OCPTask::STATUS_SUCCESSFUL); $this->taskMapper->update(DbTask::fromPublicTask($task)); return $output; - } catch (\RuntimeException $e) { - $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); - $task->setStatus(OCPTask::STATUS_FAILED); - $this->taskMapper->update(DbTask::fromPublicTask($task)); - throw $e; } catch (\Throwable $e) { $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); $task->setStatus(OCPTask::STATUS_FAILED); $this->taskMapper->update(DbTask::fromPublicTask($task)); - throw new RuntimeException('LanguageModel call using provider ' . $provider->getName() . ' failed: ' . $e->getMessage(), 0, $e); + throw new TaskFailureException('LanguageModel call using provider ' . $provider->getName() . ' failed: ' . $e->getMessage(), 0, $e); } } - throw new RuntimeException('Could not run task'); + $task->setStatus(OCPTask::STATUS_FAILED); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + throw new TaskFailureException('Could not run task'); } /** * @inheritDoc - * @throws Exception */ public function scheduleTask(OCPTask $task): void { if (!$this->canHandleTask($task)) { throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); } $task->setStatus(OCPTask::STATUS_SCHEDULED); + $providers = $this->getPreferredProviders($task); + if (count($providers) === 0) { + throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); + } + [$provider,] = $providers; + if ($provider instanceof IProviderWithExpectedRuntime) { + $completionExpectedAt = new \DateTime('now'); + $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S')); + $task->setCompletionExpectedAt($completionExpectedAt); + } $taskEntity = DbTask::fromPublicTask($task); $this->taskMapper->insert($taskEntity); $task->setId($taskEntity->getId()); @@ -181,6 +179,25 @@ class Manager implements IManager { /** * @inheritDoc */ + public function runOrScheduleTask(OCPTask $task): bool { + if (!$this->canHandleTask($task)) { + throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); + } + [$provider,] = $this->getPreferredProviders($task); + $maxExecutionTime = (int) ini_get('max_execution_time'); + // Offload the task to a background job if the expected runtime of the likely provider is longer than 80% of our max execution time + // or if the provider doesn't provide a getExpectedRuntime() method + if (!$provider instanceof IProviderWithExpectedRuntime || $provider->getExpectedRuntime() > $maxExecutionTime * 0.8) { + $this->scheduleTask($task); + return false; + } + $this->runTask($task); + return true; + } + + /** + * @inheritDoc + */ public function deleteTask(Task $task): void { $taskEntity = DbTask::fromPublicTask($task); $this->taskMapper->delete($taskEntity); @@ -253,4 +270,26 @@ class Manager implements IManager { throw new RuntimeException('Failure while trying to find tasks by appId and identifier: ' . $e->getMessage(), 0, $e); } } + + /** + * @param OCPTask $task + * @return IProvider[] + */ + public function getPreferredProviders(OCPTask $task): array { + $providers = $this->getProviders(); + $json = $this->config->getAppValue('core', 'ai.textprocessing_provider_preferences', ''); + if ($json !== '') { + $preferences = json_decode($json, true); + if (isset($preferences[$task->getType()])) { + // If a preference for this task type is set, move the preferred provider to the start + $provider = current(array_values(array_filter($providers, fn ($provider) => $provider::class === $preferences[$task->getType()]))); + if ($provider !== false) { + $providers = array_filter($providers, fn ($p) => $p !== $provider); + array_unshift($providers, $provider); + } + } + } + $providers = array_values(array_filter($providers, fn (IProvider $provider) => $task->canUseProvider($provider))); + return $providers; + } } diff --git a/lib/private/TextToImage/Db/Task.php b/lib/private/TextToImage/Db/Task.php new file mode 100644 index 00000000000..96dd6e4e165 --- /dev/null +++ b/lib/private/TextToImage/Db/Task.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage\Db; + +use DateTime; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\TextToImage\Task as OCPTask; + +/** + * @method setLastUpdated(DateTime $lastUpdated) + * @method DateTime getLastUpdated() + * @method setInput(string $type) + * @method string getInput() + * @method setResultPath(string $resultPath) + * @method string getResultPath() + * @method setStatus(int $type) + * @method int getStatus() + * @method setUserId(?string $userId) + * @method string|null getUserId() + * @method setAppId(string $type) + * @method string getAppId() + * @method setIdentifier(string $identifier) + * @method string|null getIdentifier() + * @method setNumberOfImages(int $numberOfImages) + * @method int getNumberOfImages() + * @method setCompletionExpectedAt(DateTime $at) + * @method DateTime getCompletionExpectedAt() + */ +class Task extends Entity { + protected $lastUpdated; + protected $type; + protected $input; + protected $status; + protected $userId; + protected $appId; + protected $identifier; + protected $numberOfImages; + protected $completionExpectedAt; + + /** + * @var string[] + */ + public static array $columns = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at']; + + /** + * @var string[] + */ + public static array $fields = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt']; + + + public function __construct() { + // add types in constructor + $this->addType('id', 'integer'); + $this->addType('lastUpdated', 'datetime'); + $this->addType('input', 'string'); + $this->addType('status', 'integer'); + $this->addType('userId', 'string'); + $this->addType('appId', 'string'); + $this->addType('identifier', 'string'); + $this->addType('numberOfImages', 'integer'); + $this->addType('completionExpectedAt', 'datetime'); + } + + public function toRow(): array { + return array_combine(self::$columns, array_map(function ($field) { + return $this->{'get'.ucfirst($field)}(); + }, self::$fields)); + } + + public static function fromPublicTask(OCPTask $task): Task { + /** @var Task $dbTask */ + $dbTask = Task::fromParams([ + 'id' => $task->getId(), + 'lastUpdated' => \OCP\Server::get(ITimeFactory::class)->getDateTime(), + 'status' => $task->getStatus(), + 'numberOfImages' => $task->getNumberOfImages(), + 'input' => $task->getInput(), + 'userId' => $task->getUserId(), + 'appId' => $task->getAppId(), + 'identifier' => $task->getIdentifier(), + 'completionExpectedAt' => $task->getCompletionExpectedAt(), + ]); + return $dbTask; + } + + public function toPublicTask(): OCPTask { + $task = new OCPTask($this->getInput(), $this->getAppId(), $this->getNumberOfImages(), $this->getuserId(), $this->getIdentifier()); + $task->setId($this->getId()); + $task->setStatus($this->getStatus()); + $task->setCompletionExpectedAt($this->getCompletionExpectedAt()); + return $task; + } +} diff --git a/lib/private/TextToImage/Db/TaskMapper.php b/lib/private/TextToImage/Db/TaskMapper.php new file mode 100644 index 00000000000..68fdd8f40de --- /dev/null +++ b/lib/private/TextToImage/Db/TaskMapper.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @extends QBMapper<Task> + */ +class TaskMapper extends QBMapper { + public function __construct( + IDBConnection $db, + private ITimeFactory $timeFactory, + ) { + parent::__construct($db, 'text2image_tasks', Task::class); + } + + /** + * @param int $id + * @return Task + * @throws Exception + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function find(int $id): Task { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); + return $this->findEntity($qb); + } + + /** + * @param int $id + * @param string|null $userId + * @return Task + * @throws DoesNotExistException + * @throws Exception + * @throws MultipleObjectsReturnedException + */ + public function findByIdAndUser(int $id, ?string $userId): Task { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); + if ($userId === null) { + $qb->andWhere($qb->expr()->isNull('user_id')); + } else { + $qb->andWhere($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))); + } + return $this->findEntity($qb); + } + + /** + * @param string $userId + * @param string $appId + * @param string|null $identifier + * @return array + * @throws Exception + */ + public function findUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) + ->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId))); + if ($identifier !== null) { + $qb->andWhere($qb->expr()->eq('identifier', $qb->createPositionalParameter($identifier))); + } + return $this->findEntities($qb); + } + + /** + * @param int $timeout + * @return Task[] the deleted tasks + * @throws Exception + */ + public function deleteOlderThan(int $timeout): array { + $datetime = $this->timeFactory->getDateTime(); + $datetime->sub(new \DateInterval('PT'.$timeout.'S')); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($datetime, IQueryBuilder::PARAM_DATE))); + $deletedTasks = $this->findEntities($qb); + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($datetime, IQueryBuilder::PARAM_DATE))); + $qb->executeStatement(); + return $deletedTasks; + } + + public function update(Entity $entity): Entity { + $entity->setLastUpdated($this->timeFactory->getDateTime()); + return parent::update($entity); + } +} diff --git a/lib/private/TextToImage/Manager.php b/lib/private/TextToImage/Manager.php new file mode 100644 index 00000000000..f68f657277f --- /dev/null +++ b/lib/private/TextToImage/Manager.php @@ -0,0 +1,335 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\TextToImage\Db\Task as DbTask; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IConfig; +use OCP\TextToImage\Exception\TaskFailureException; +use OCP\TextToImage\Exception\TaskNotFoundException; +use OCP\TextToImage\IManager; +use OCP\TextToImage\Task; +use OC\TextToImage\Db\TaskMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\BackgroundJob\IJobList; +use OCP\DB\Exception; +use OCP\IServerContainer; +use OCP\TextToImage\IProvider; +use OCP\PreConditionNotMetException; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class Manager implements IManager { + /** @var ?IProvider[] */ + private ?array $providers = null; + private IAppData $appData; + + public function __construct( + private IServerContainer $serverContainer, + private Coordinator $coordinator, + private LoggerInterface $logger, + private IJobList $jobList, + private TaskMapper $taskMapper, + private IConfig $config, + IAppDataFactory $appDataFactory, + ) { + $this->appData = $appDataFactory->get('core'); + } + + /** + * @inerhitDocs + */ + public function getProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return []; + } + + if ($this->providers !== null) { + return $this->providers; + } + + $this->providers = []; + + foreach ($context->getTextToImageProviders() as $providerServiceRegistration) { + $class = $providerServiceRegistration->getService(); + try { + $this->providers[$class] = $this->serverContainer->get($class); + } catch (Throwable $e) { + $this->logger->error('Failed to load Text to image provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return $this->providers; + } + + /** + * @inheritDoc + */ + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return false; + } + return count($context->getTextToImageProviders()) > 0; + } + + /** + * @inheritDoc + */ + public function runTask(Task $task): void { + $this->logger->debug('Running TextToImage Task'); + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $providers = $this->getPreferredProviders(); + + foreach ($providers as $provider) { + $this->logger->debug('Trying to run Text2Image provider '.$provider::class); + try { + $task->setStatus(Task::STATUS_RUNNING); + $completionExpectedAt = new \DateTime('now'); + $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S')); + $task->setCompletionExpectedAt($completionExpectedAt); + if ($task->getId() === null) { + $this->logger->debug('Inserting Text2Image task into DB'); + $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); + $task->setId($taskEntity->getId()); + } else { + $this->logger->debug('Updating Text2Image task in DB'); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } + try { + $folder = $this->appData->getFolder('text2image'); + } catch(NotFoundException) { + $this->logger->debug('Creating folder in appdata for Text2Image results'); + $folder = $this->appData->newFolder('text2image'); + } + try { + $folder = $folder->getFolder((string) $task->getId()); + } catch(NotFoundException) { + $this->logger->debug('Creating new folder in appdata Text2Image results folder'); + $folder = $folder->newFolder((string) $task->getId()); + } + $this->logger->debug('Creating result files for Text2Image task'); + $resources = []; + $files = []; + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + $file = $folder->newFile((string) $i); + $files[] = $file; + $resource = $file->write(); + if ($resource !== false && $resource !== true && is_resource($resource)) { + $resources[] = $resource; + } else { + throw new RuntimeException('Text2Image generation using provider "' . $provider->getName() . '" failed: Couldn\'t open file to write.'); + } + } + $this->logger->debug('Calling Text2Image provider\'s generate method'); + $provider->generate($task->getInput(), $resources); + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + if (is_resource($resources[$i])) { + // If $resource hasn't been closed yet, we'll do that here + fclose($resources[$i]); + } + } + $task->setStatus(Task::STATUS_SUCCESSFUL); + $this->logger->debug('Updating Text2Image task in DB'); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + return; + } catch (\RuntimeException|\Throwable $e) { + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + if (isset($files, $files[$i])) { + try { + $files[$i]->delete(); + } catch(NotPermittedException $e) { + $this->logger->warning('Failed to clean up Text2Image result file after error', ['exception' => $e]); + } + } + } + + $this->logger->info('Text2Image generation using provider "' . $provider->getName() . '" failed', ['exception' => $e]); + $task->setStatus(Task::STATUS_FAILED); + try { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } catch (Exception $e) { + $this->logger->warning('Failed to update database after Text2Image error', ['exception' => $e]); + } + throw new TaskFailureException('Text2Image generation using provider "' . $provider->getName() . '" failed: ' . $e->getMessage(), 0, $e); + } + } + + $task->setStatus(Task::STATUS_FAILED); + try { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } catch (Exception $e) { + $this->logger->warning('Failed to update database after Text2Image error', ['exception' => $e]); + } + throw new TaskFailureException('Could not run task'); + } + + /** + * @inheritDoc + */ + public function scheduleTask(Task $task): void { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $this->logger->debug('Scheduling Text2Image Task'); + $task->setStatus(Task::STATUS_SCHEDULED); + $completionExpectedAt = new \DateTime('now'); + $completionExpectedAt->add(new \DateInterval('PT'.$this->getPreferredProviders()[0]->getExpectedRuntime().'S')); + $task->setCompletionExpectedAt($completionExpectedAt); + $taskEntity = DbTask::fromPublicTask($task); + $this->taskMapper->insert($taskEntity); + $task->setId($taskEntity->getId()); + $this->jobList->add(TaskBackgroundJob::class, [ + 'taskId' => $task->getId() + ]); + } + + /** + * @inheritDoc + */ + public function runOrScheduleTask(Task $task) : void { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $providers = $this->getPreferredProviders(); + $maxExecutionTime = (int) ini_get('max_execution_time'); + // Offload the task to a background job if the expected runtime of the likely provider is longer than 80% of our max execution time + if ($providers[0]->getExpectedRuntime() > $maxExecutionTime * 0.8) { + $this->scheduleTask($task); + return; + } + $this->runTask($task); + } + + /** + * @inheritDoc + */ + public function deleteTask(Task $task): void { + $taskEntity = DbTask::fromPublicTask($task); + $this->taskMapper->delete($taskEntity); + $this->jobList->remove(TaskBackgroundJob::class, [ + 'taskId' => $task->getId() + ]); + } + + /** + * Get a task from its id + * + * @param int $id The id of the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + */ + public function getTask(int $id): Task { + try { + $taskEntity = $this->taskMapper->find($id); + return $taskEntity->toPublicTask(); + } catch (DoesNotExistException $e) { + throw new TaskNotFoundException('Could not find task with the provided id'); + } catch (MultipleObjectsReturnedException $e) { + throw new RuntimeException('Could not uniquely identify task with given id', 0, $e); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find task by id: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get a task from its user id and task id + * If userId is null, this can only get a task that was scheduled anonymously + * + * @param int $id The id of the task + * @param string|null $userId The user id that scheduled the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + */ + public function getUserTask(int $id, ?string $userId): Task { + try { + $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId); + return $taskEntity->toPublicTask(); + } catch (DoesNotExistException $e) { + throw new TaskNotFoundException('Could not find task with the provided id and user id'); + } catch (MultipleObjectsReturnedException $e) { + throw new RuntimeException('Could not uniquely identify task with given id and user id', 0, $e); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find task by id and user id: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get a list of tasks scheduled by a specific user for a specific app + * and optionally with a specific identifier. + * This cannot be used to get anonymously scheduled tasks + * + * @param string $userId + * @param string $appId + * @param string|null $identifier + * @return Task[] + * @throws RuntimeException + */ + public function getUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { + try { + $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier); + return array_map(static function (DbTask $taskEntity) { + return $taskEntity->toPublicTask(); + }, $taskEntities); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find tasks by appId and identifier: ' . $e->getMessage(), 0, $e); + } + } + + /** + * @return IProvider[] + */ + private function getPreferredProviders() { + $providers = $this->getProviders(); + $json = $this->config->getAppValue('core', 'ai.text2image_provider', ''); + if ($json !== '') { + try { + $id = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + $provider = current(array_filter($providers, fn ($provider) => $provider->getId() === $id)); + if ($provider !== false && $provider !== null) { + $providers = [$provider]; + } + } catch (\JsonException $e) { + $this->logger->warning('Failed to decode Text2Image setting `ai.text2image_provider`', ['exception' => $e]); + } + } + + return $providers; + } +} diff --git a/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php b/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php new file mode 100644 index 00000000000..2ecebc241bf --- /dev/null +++ b/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +namespace OC\TextToImage; + +use OC\TextToImage\Db\TaskMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\Exception; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use Psr\Log\LoggerInterface; + +class RemoveOldTasksBackgroundJob extends TimedJob { + public const MAX_TASK_AGE_SECONDS = 60 * 50 * 24 * 7; // 1 week + + private IAppData $appData; + + public function __construct( + ITimeFactory $timeFactory, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + IAppDataFactory $appDataFactory, + ) { + parent::__construct($timeFactory); + $this->appData = $appDataFactory->get('core'); + $this->setInterval(60 * 60 * 24); + } + + /** + * @param mixed $argument + * @inheritDoc + */ + protected function run($argument) { + try { + $deletedTasks = $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); + $folder = $this->appData->getFolder('text2image'); + foreach ($deletedTasks as $deletedTask) { + try { + $folder->getFolder((string)$deletedTask->getId())->delete(); + } catch (NotFoundException) { + // noop + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete stale text to image task files', ['exception' => $e]); + } + } + } catch (Exception $e) { + $this->logger->warning('Failed to delete stale text to image tasks', ['exception' => $e]); + } catch(NotFoundException) { + // noop + } + } +} diff --git a/lib/private/TextToImage/TaskBackgroundJob.php b/lib/private/TextToImage/TaskBackgroundJob.php new file mode 100644 index 00000000000..ac5cd6b59b5 --- /dev/null +++ b/lib/private/TextToImage/TaskBackgroundJob.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +namespace OC\TextToImage; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\TextToImage\Events\TaskFailedEvent; +use OCP\TextToImage\Events\TaskSuccessfulEvent; +use OCP\TextToImage\IManager; + +class TaskBackgroundJob extends QueuedJob { + public function __construct( + ITimeFactory $timeFactory, + private IManager $text2imageManager, + private IEventDispatcher $eventDispatcher, + ) { + parent::__construct($timeFactory); + // We want to avoid overloading the machine with these jobs + // so we only allow running one job at a time + $this->setAllowParallelRuns(false); + } + + /** + * @param array{taskId: int} $argument + * @inheritDoc + */ + protected function run($argument) { + $taskId = $argument['taskId']; + $task = $this->text2imageManager->getTask($taskId); + try { + $this->text2imageManager->runTask($task); + $event = new TaskSuccessfulEvent($task); + } catch (\Throwable $e) { + $event = new TaskFailedEvent($task, $e->getMessage()); + } + $this->eventDispatcher->dispatchTyped($event); + } +} diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 3a52b99889c..4e9a23b3eae 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -116,16 +116,25 @@ class URLGenerator implements IURLGenerator { } public function linkToOCSRouteAbsolute(string $routeName, array $arguments = []): string { + // Returns `/subfolder/index.php/ocsapp/…` with `'htaccess.IgnoreFrontController' => false` in config.php + // And `/subfolder/ocsapp/…` with `'htaccess.IgnoreFrontController' => true` in config.php $route = $this->router->generate('ocs.'.$routeName, $arguments, false); - $indexPhpPos = strpos($route, '/index.php/'); - if ($indexPhpPos !== false) { - $route = substr($route, $indexPhpPos + 10); + // Cut off `/subfolder` + if (\OC::$WEBROOT !== '' && str_starts_with($route, \OC::$WEBROOT)) { + $route = substr($route, \strlen(\OC::$WEBROOT)); } + if (str_starts_with($route, '/index.php/')) { + $route = substr($route, 10); + } + + // Remove `ocsapp/` bit $route = substr($route, 7); + // Prefix with ocs/v2.php endpoint $route = '/ocs/v2.php' . $route; + // Turn into an absolute URL return $this->getAbsoluteURL($route); } @@ -147,7 +156,7 @@ class URLGenerator implements IURLGenerator { $app_path = $this->getAppManager()->getAppPath($appName); // Check if the app is in the app folder if (file_exists($app_path . '/' . $file)) { - if (substr($file, -3) === 'php') { + if (str_ends_with($file, 'php')) { $urlLinkTo = \OC::$WEBROOT . '/index.php/apps/' . $appName; if ($frontControllerActive) { $urlLinkTo = \OC::$WEBROOT . '/apps/' . $appName; diff --git a/lib/private/Updater/VersionCheck.php b/lib/private/Updater/VersionCheck.php index 2aab260716a..97f770b6998 100644 --- a/lib/private/Updater/VersionCheck.php +++ b/lib/private/Updater/VersionCheck.php @@ -31,6 +31,7 @@ use OCP\IConfig; use OCP\IUserManager; use OCP\Support\Subscription\IRegistry; use OCP\Util; +use Psr\Log\LoggerInterface; class VersionCheck { public function __construct( @@ -38,6 +39,7 @@ class VersionCheck { private IConfig $config, private IUserManager $userManager, private IRegistry $registry, + private LoggerInterface $logger, ) { } @@ -86,6 +88,8 @@ class VersionCheck { try { $xml = $this->getUrlContent($url); } catch (\Exception $e) { + $this->logger->info('Version could not be fetched from updater server: ' . $url, ['exception' => $e]); + return false; } diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php new file mode 100644 index 00000000000..8e6b73bd56d --- /dev/null +++ b/lib/private/User/AvailabilityCoordinator.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Richard Steinmetz <richard@steinmetz.cloud> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\User; + +use JsonException; +use OCA\DAV\AppInfo\Application; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IUser; +use OCP\User\IAvailabilityCoordinator; +use OCP\User\IOutOfOfficeData; +use Psr\Log\LoggerInterface; + +class AvailabilityCoordinator implements IAvailabilityCoordinator { + private ICache $cache; + + public function __construct( + ICacheFactory $cacheFactory, + private AbsenceMapper $absenceMapper, + private IConfig $config, + private LoggerInterface $logger, + ) { + $this->cache = $cacheFactory->createLocal('OutOfOfficeData'); + } + + public function isEnabled(): bool { + return $this->config->getAppValue( + Application::APP_ID, + 'hide_absence_settings', + 'no', + ) === 'no'; + } + + private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData { + $cachedString = $this->cache->get($user->getUID()); + if ($cachedString === null) { + return null; + } + + try { + $cachedData = json_decode($cachedString, true, 10, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->logger->error('Failed to deserialize cached out-of-office data: ' . $e->getMessage(), [ + 'exception' => $e, + 'json' => $cachedString, + ]); + return null; + } + + return new OutOfOfficeData( + $cachedData['id'], + $user, + $cachedData['startDate'], + $cachedData['endDate'], + $cachedData['shortMessage'], + $cachedData['message'], + ); + } + + private function setCachedOutOfOfficeData(IOutOfOfficeData $data): void { + try { + $cachedString = json_encode([ + 'id' => $data->getId(), + 'startDate' => $data->getStartDate(), + 'endDate' => $data->getEndDate(), + 'shortMessage' => $data->getShortMessage(), + 'message' => $data->getMessage(), + ], JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->logger->error('Failed to serialize out-of-office data: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + return; + } + + $this->cache->set($data->getUser()->getUID(), $cachedString, 300); + } + + public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData { + $cachedData = $this->getCachedOutOfOfficeData($user); + if ($cachedData !== null) { + return $cachedData; + } + + try { + $absenceData = $this->absenceMapper->findByUserId($user->getUID()); + } catch (DoesNotExistException $e) { + return null; + } + + $data = $absenceData->toOutOufOfficeData($user); + $this->setCachedOutOfOfficeData($data); + return $data; + } +} diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index fb1afb65825..8ec8ef0c4be 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -52,6 +52,7 @@ use OCP\User\Backend\IGetRealUIDBackend; use OCP\User\Backend\ISearchKnownUsersBackend; use OCP\User\Backend\ICheckPasswordBackend; use OCP\User\Backend\ICountUsersBackend; +use OCP\User\Backend\IProvideEnabledStateBackend; use OCP\User\Events\BeforeUserCreatedEvent; use OCP\User\Events\UserCreatedEvent; use OCP\UserInterface; @@ -338,6 +339,35 @@ class Manager extends PublicEmitter implements IUserManager { } /** + * @return IUser[] + */ + public function getDisabledUsers(?int $limit = null, int $offset = 0): array { + $users = $this->config->getUsersForUserValue('core', 'enabled', 'false'); + $users = array_combine( + $users, + array_map( + fn (string $uid): IUser => new LazyUser($uid, $this), + $users + ) + ); + + $tempLimit = ($limit === null ? null : $limit + $offset); + foreach ($this->backends as $backend) { + if (($tempLimit !== null) && (count($users) >= $tempLimit)) { + break; + } + if ($backend instanceof IProvideEnabledStateBackend) { + $backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users))); + foreach ($backendUsers as $uid) { + $users[$uid] = new LazyUser($uid, $this, null, $backend); + } + } + } + + return array_slice($users, $offset, $limit); + } + + /** * Search known users (from phonebook sync) by displayName * * @param string $searcher diff --git a/lib/private/User/OutOfOfficeData.php b/lib/private/User/OutOfOfficeData.php new file mode 100644 index 00000000000..12b7e03a0ae --- /dev/null +++ b/lib/private/User/OutOfOfficeData.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\User; + +use OCP\IUser; +use OCP\User\IOutOfOfficeData; + +class OutOfOfficeData implements IOutOfOfficeData { + public function __construct(private string $id, + private IUser $user, + private int $startDate, + private int $endDate, + private string $shortMessage, + private string $message) { + } + + public function getId(): string { + return $this->id; + } + + public function getUser(): IUser { + return $this->user; + } + + public function getStartDate(): int { + return $this->startDate; + } + + public function getEndDate(): int { + return $this->endDate; + } + + public function getShortMessage(): string { + return $this->shortMessage; + } + + public function getMessage(): string { + return $this->message; + } +} diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 82887f8d029..f8578ee616c 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -783,6 +783,11 @@ class Session implements IUserSession, Emitter { try { $dbToken = $this->tokenProvider->getToken($token); } catch (InvalidTokenException $ex) { + $this->logger->debug('Session token is invalid because it does not exist', [ + 'app' => 'core', + 'user' => $user, + 'exception' => $ex, + ]); return false; } @@ -794,12 +799,18 @@ class Session implements IUserSession, Emitter { $this->logger->error('App token login name does not match', [ 'tokenLoginName' => $dbToken->getLoginName(), 'sessionLoginName' => $user, + 'app' => 'core', + 'user' => $dbToken->getUID(), ]); return false; } if (!$this->checkTokenCredentials($dbToken, $token)) { + $this->logger->warning('Session token credentials are invalid', [ + 'app' => 'core', + 'user' => $user, + ]); return false; } @@ -875,9 +886,9 @@ class Session implements IUserSession, Emitter { $tokens = $this->config->getUserKeys($uid, 'login_token'); // test cookies token against stored tokens if (!in_array($currentToken, $tokens, true)) { - $this->logger->info('Tried to log in {uid} but could not verify token', [ + $this->logger->info('Tried to log in but could not verify token', [ 'app' => 'core', - 'uid' => $uid, + 'user' => $uid, ]); return false; } @@ -885,18 +896,31 @@ class Session implements IUserSession, Emitter { $this->config->deleteUserValue($uid, 'login_token', $currentToken); $newToken = $this->random->generate(32); $this->config->setUserValue($uid, 'login_token', $newToken, (string)$this->timeFactory->getTime()); + $this->logger->debug('Remember-me token replaced', [ + 'app' => 'core', + 'user' => $uid, + ]); try { $sessionId = $this->session->getId(); $token = $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId); + $this->logger->debug('Session token replaced', [ + 'app' => 'core', + 'user' => $uid, + ]); } catch (SessionNotAvailableException $ex) { - $this->logger->warning('Could not renew session token for {uid} because the session is unavailable', [ + $this->logger->critical('Could not renew session token for {uid} because the session is unavailable', [ 'app' => 'core', 'uid' => $uid, + 'user' => $uid, ]); return false; } catch (InvalidTokenException $ex) { - $this->logger->warning('Renewing session token failed', ['app' => 'core']); + $this->logger->error('Renewing session token failed: ' . $ex->getMessage(), [ + 'app' => 'core', + 'user' => $uid, + 'exception' => $ex, + ]); return false; } @@ -935,10 +959,17 @@ class Session implements IUserSession, Emitter { $this->manager->emit('\OC\User', 'logout', [$user]); if ($user !== null) { try { - $this->tokenProvider->invalidateToken($this->session->getId()); + $token = $this->session->getId(); + $this->tokenProvider->invalidateToken($token); + $this->logger->debug('Session token invalidated before logout', [ + 'user' => $user->getUID(), + ]); } catch (SessionNotAvailableException $ex) { } } + $this->logger->debug('Logging out', [ + 'user' => $user === null ? null : $user->getUID(), + ]); $this->setUser(null); $this->setLoginName(null); $this->setToken(null); diff --git a/lib/private/User/User.php b/lib/private/User/User.php index 3362367fff1..c62d14d9450 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -597,7 +597,7 @@ class User implements IUser { public function getCloudId() { $uid = $this->getUID(); $server = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/'); - if (substr($server, -10) === '/index.php') { + if (str_ends_with($server, '/index.php')) { $server = substr($server, 0, -10); } $server = $this->removeProtocolFromUrl($server); diff --git a/lib/private/legacy/OC_API.php b/lib/private/legacy/OC_API.php index 275e02986c4..862e73e6edd 100644 --- a/lib/private/legacy/OC_API.php +++ b/lib/private/legacy/OC_API.php @@ -130,7 +130,7 @@ class OC_API { protected static function isV2(\OCP\IRequest $request) { $script = $request->getScriptName(); - return substr($script, -11) === '/ocs/v2.php'; + return str_ends_with($script, '/ocs/v2.php'); } /** diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index ac449a62a4f..23e0b099e91 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -56,7 +56,6 @@ use OCP\App\IAppManager; use OCP\App\ManagerEvent; use OCP\Authentication\IAlternativeLogin; use OCP\EventDispatcher\IEventDispatcher; -use OCP\ILogger; use OC\AppFramework\Bootstrap\Coordinator; use OC\App\DependencyAnalyzer; use OC\App\Platform; @@ -292,7 +291,7 @@ class OC_App { } } - \OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('No application directories are marked as writable.', ['app' => 'core']); return null; } @@ -391,7 +390,7 @@ class OC_App { public static function getAppVersionByPath(string $path): string { $infoFile = $path . '/appinfo/info.xml'; $appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true); - return isset($appData['version']) ? $appData['version'] : ''; + return $appData['version'] ?? ''; } /** @@ -517,7 +516,7 @@ class OC_App { foreach (OC::$APPSROOTS as $apps_dir) { if (!is_readable($apps_dir['path'])) { - \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN); + \OCP\Server::get(LoggerInterface::class)->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']); continue; } $dh = opendir($apps_dir['path']); @@ -568,12 +567,12 @@ class OC_App { if (array_search($app, $blacklist) === false) { $info = $appManager->getAppInfo($app, false, $langCode); if (!is_array($info)) { - \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('Could not read app info file for app "' . $app . '"', ['app' => 'core']); continue; } if (!isset($info['name'])) { - \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('App id "' . $app . '" has no name in appinfo', ['app' => 'core']); continue; } @@ -870,11 +869,11 @@ class OC_App { } return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId); } else { - \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('Can\'t get app storage, app ' . $appId . ', user not logged in', ['app' => 'core']); return false; } } else { - \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('Can\'t get app storage, app ' . $appId . ' not enabled', ['app' => 'core']); return false; } } diff --git a/lib/private/legacy/OC_Files.php b/lib/private/legacy/OC_Files.php index 7bc1fab94b6..ac0a2bbd0e9 100644 --- a/lib/private/legacy/OC_Files.php +++ b/lib/private/legacy/OC_Files.php @@ -76,7 +76,6 @@ class OC_Files { private static function sendHeaders($filename, $name, array $rangeArray): void { OC_Response::setContentDispositionHeader($name, 'attachment'); header('Content-Transfer-Encoding: binary', true); - header('Pragma: public');// enable caching in IE header('Expires: 0'); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); $fileSize = \OC\Files\Filesystem::filesize($filename); @@ -230,6 +229,10 @@ class OC_Files { OC::$server->getLogger()->logException($ex); $l = \OC::$server->getL10N('lib'); \OC_Template::printErrorPage($l->t('Cannot download file'), $ex->getMessage(), 200); + } catch (\OCP\Files\ConnectionLostException $ex) { + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + OC::$server->getLogger()->logException($ex, ['level' => \OCP\ILogger::DEBUG]); + \OC_Template::printErrorPage('Connection lost', $ex->getMessage(), 200); } catch (\Exception $ex) { self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); OC::$server->getLogger()->logException($ex); diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index caa4f5dca65..5751b813f2c 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -38,10 +38,10 @@ use OC\User\LoginException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\ILogger; use OCP\IUserManager; use OCP\User\Events\BeforeUserLoggedInEvent; use OCP\User\Events\UserLoggedInEvent; +use Psr\Log\LoggerInterface; /** * This class provides wrapper methods for user management. Multiple backends are @@ -93,7 +93,7 @@ class OC_User { case 'database': case 'mysql': case 'sqlite': - \OCP\Util::writeLog('core', 'Adding user backend ' . $backend . '.', ILogger::DEBUG); + \OCP\Server::get(LoggerInterface::class)->debug('Adding user backend ' . $backend . '.', ['app' => 'core']); self::$_usedBackends[$backend] = new \OC\User\Database(); \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); break; @@ -102,7 +102,7 @@ class OC_User { \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); break; default: - \OCP\Util::writeLog('core', 'Adding default user backend ' . $backend . '.', ILogger::DEBUG); + \OCP\Server::get(LoggerInterface::class)->debug('Adding default user backend ' . $backend . '.', ['app' => 'core']); $className = 'OC_USER_' . strtoupper($backend); self::$_usedBackends[$backend] = new $className(); \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); @@ -147,10 +147,10 @@ class OC_User { self::useBackend($backend); self::$_setupedBackends[] = $i; } else { - \OCP\Util::writeLog('core', 'User backend ' . $class . ' already initialized.', ILogger::DEBUG); + \OCP\Server::get(LoggerInterface::class)->debug('User backend ' . $class . ' already initialized.', ['app' => 'core']); } } else { - \OCP\Util::writeLog('core', 'User backend ' . $class . ' not found.', ILogger::ERROR); + \OCP\Server::get(LoggerInterface::class)->error('User backend ' . $class . ' not found.', ['app' => 'core']); } } } diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index 9d62c46137e..f82ddcc78ee 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -325,9 +325,10 @@ class OC_Util { return; } + $timestamp = filemtime(OC::$SERVERROOT . '/version.php'); require OC::$SERVERROOT . '/version.php'; /** @var int $timestamp */ - self::$versionCache['OC_Version_Timestamp'] = \OC::$VERSION_MTIME; + self::$versionCache['OC_Version_Timestamp'] = $timestamp; /** @var string $OC_Version */ self::$versionCache['OC_Version'] = $OC_Version; /** @var string $OC_VersionString */ |