diff options
Diffstat (limited to 'lib/private')
236 files changed, 4404 insertions, 1402 deletions
diff --git a/lib/private/Activity/Manager.php b/lib/private/Activity/Manager.php index 8b60dc49ec9..54d50a47dcb 100644 --- a/lib/private/Activity/Manager.php +++ b/lib/private/Activity/Manager.php @@ -291,7 +291,7 @@ class Manager implements IManager { public function isFormattingFilteredObject(): bool { return $this->formattingObjectType !== null && $this->formattingObjectId !== null && $this->formattingObjectType === $this->request->getParam('object_type') - && $this->formattingObjectId === (int) $this->request->getParam('object_id'); + && $this->formattingObjectId === (int)$this->request->getParam('object_id'); } /** @@ -344,7 +344,7 @@ class Manager implements IManager { * @throws \UnexpectedValueException If the token is invalid, does not exist or is not unique */ protected function getUserFromToken(): string { - $token = (string) $this->request->getParam('token', ''); + $token = (string)$this->request->getParam('token', ''); if (strlen($token) !== 30) { throw new \UnexpectedValueException('The token is invalid'); } diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php index 58eee772fbf..263b5283133 100644 --- a/lib/private/AllConfig.php +++ b/lib/private/AllConfig.php @@ -107,7 +107,7 @@ class AllConfig implements IConfig { * @since 16.0.0 */ public function getSystemValueBool(string $key, bool $default = false): bool { - return (bool) $this->getSystemValue($key, $default); + return (bool)$this->getSystemValue($key, $default); } /** @@ -121,7 +121,7 @@ class AllConfig implements IConfig { * @since 16.0.0 */ public function getSystemValueInt(string $key, int $default = 0): int { - return (int) $this->getSystemValue($key, $default); + return (int)$this->getSystemValue($key, $default); } /** @@ -135,7 +135,7 @@ class AllConfig implements IConfig { * @since 16.0.0 */ public function getSystemValueString(string $key, string $default = ''): string { - return (string) $this->getSystemValue($key, $default); + return (string)$this->getSystemValue($key, $default); } /** @@ -236,7 +236,7 @@ class AllConfig implements IConfig { $this->fixDIInit(); if ($appName === 'settings' && $key === 'email') { - $value = strtolower((string) $value); + $value = strtolower((string)$value); } $prevValue = $this->getUserValue($userId, $appName, $key, null); @@ -382,9 +382,9 @@ class AllConfig implements IConfig { * @param ?string $userId the user ID to get the app configs from * @psalm-return array<string, array<string, string>> * @return array[] - 2 dimensional array with the following structure: - * [ $appId => - * [ $key => $value ] - * ] + * [ $appId => + * [ $key => $value ] + * ] */ public function getAllUserValues(?string $userId): array { if (isset($this->userCache[$userId])) { @@ -462,7 +462,7 @@ class AllConfig implements IConfig { * @param string $appName the app to get the user for * @param string $key the key to get the user for * @param string $value the value to get the user for - * @return array of user IDs + * @return list<string> of user IDs */ public function getUsersForUserValue($appName, $key, $value) { // TODO - FIXME @@ -496,7 +496,7 @@ class AllConfig implements IConfig { * @param string $appName the app to get the user for * @param string $key the key to get the user for * @param string $value the value to get the user for - * @return array of user IDs + * @return list<string> of user IDs */ public function getUsersForUserValueCaseInsensitive($appName, $key, $value) { // TODO - FIXME diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index ad76befc5fa..28252f264c3 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -86,7 +86,8 @@ abstract class Fetcher { $response = $client->get($this->getEndpoint(), $options); } catch (ConnectException $e) { $this->config->setAppValue('settings', 'appstore-fetcher-lastFailure', (string)time()); - throw $e; + $this->logger->error('Failed to connect to the app store', ['exception' => $e]); + return []; } $responseJson = []; diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php index d963c74de79..72b38ca12c7 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -66,7 +66,7 @@ class DependencyAnalyzer { * @param string $first * @param string $second * @return string[] first element is the first version, second element is the - * second version + * second version */ private function normalizeVersions($first, $second) { $first = explode('.', $first); diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php index 54afd0069fb..d0e67b82f21 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -223,7 +223,7 @@ class InfoParser { $totalElement = count($xml->{$element}); if (!isset($array[$element])) { - $array[$element] = $totalElement > 1 ? [] : ""; + $array[$element] = $totalElement > 1 ? [] : ''; } /** @var \SimpleXMLElement $node */ // Has attributes diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index d046557e42c..c27150a67dc 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -748,11 +748,11 @@ class AppConfig implements IAppConfig { try { $insert = $this->connection->getQueryBuilder(); $insert->insert('appconfig') - ->setValue('appid', $insert->createNamedParameter($app)) - ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT)) - ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT)) - ->setValue('configkey', $insert->createNamedParameter($key)) - ->setValue('configvalue', $insert->createNamedParameter($value)); + ->setValue('appid', $insert->createNamedParameter($app)) + ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT)) + ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT)) + ->setValue('configkey', $insert->createNamedParameter($key)) + ->setValue('configvalue', $insert->createNamedParameter($value)); $insert->executeStatement(); $inserted = true; } catch (DBException $e) { @@ -807,11 +807,11 @@ class AppConfig implements IAppConfig { $update = $this->connection->getQueryBuilder(); $update->update('appconfig') - ->set('configvalue', $update->createNamedParameter($value)) - ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT)) - ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) - ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) - ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); + ->set('configvalue', $update->createNamedParameter($value)) + ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT)) + ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) + ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) + ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); } @@ -869,9 +869,9 @@ class AppConfig implements IAppConfig { $update = $this->connection->getQueryBuilder(); $update->update('appconfig') - ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) - ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) - ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); + ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) + ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) + ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); $this->valueTypes[$app][$key] = $type; @@ -927,10 +927,10 @@ class AppConfig implements IAppConfig { $update = $this->connection->getQueryBuilder(); $update->update('appconfig') - ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) - ->set('configvalue', $update->createNamedParameter($value)) - ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) - ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); + ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT)) + ->set('configvalue', $update->createNamedParameter($value)) + ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) + ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); $this->valueTypes[$app][$key] = $type; @@ -962,9 +962,9 @@ class AppConfig implements IAppConfig { $update = $this->connection->getQueryBuilder(); $update->update('appconfig') - ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)) - ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) - ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); + ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)) + ->where($update->expr()->eq('appid', $update->createNamedParameter($app))) + ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); // At this point, it is a lot safer to clean cache @@ -1075,8 +1075,8 @@ class AppConfig implements IAppConfig { $this->assertParams($app, $key); $qb = $this->connection->getQueryBuilder(); $qb->delete('appconfig') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))) - ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key))); + ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))) + ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key))); $qb->executeStatement(); unset($this->lazyCache[$app][$key]); @@ -1094,7 +1094,7 @@ class AppConfig implements IAppConfig { $this->assertParams($app); $qb = $this->connection->getQueryBuilder(); $qb->delete('appconfig') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))); + ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))); $qb->executeStatement(); $this->clearCache(); diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index 9f9fb32dbcb..c5f61d7e938 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -37,7 +37,7 @@ class App { * namespace tag or uppercasing the appid's first letter * @param string $appId the app id * @param string $topNamespace the namespace which should be prepended to - * the transformed app id, defaults to OCA\ + * the transformed app id, defaults to OCA\ * @return string the starting namespace for the app */ public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string { diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index f59d5b55706..d7a380f9e1d 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -538,10 +538,10 @@ class RegistrationContext { public function registerTalkBackend(string $appId, string $backend) { // Some safeguards for invalid registrations if ($appId !== 'spreed') { - throw new RuntimeException("Only the Talk app is allowed to register a Talk backend"); + throw new RuntimeException('Only the Talk app is allowed to register a Talk backend'); } if ($this->talkBackendRegistration !== null) { - throw new RuntimeException("There can only be one Talk backend"); + throw new RuntimeException('There can only be one Talk backend'); } $this->talkBackendRegistration = new ServiceRegistration($appId, $backend); diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 208031b942f..a96e050c0e6 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -36,6 +36,7 @@ use OCP\Files\IAppData; use OCP\Group\ISubAdmin; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IGroupManager; use OCP\IInitialStateService; use OCP\IL10N; use OCP\ILogger; @@ -228,8 +229,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { $server->get(LoggerInterface::class), $c->get('AppName'), $server->getUserSession()->isLoggedIn(), - $this->getUserId() !== null && $server->getGroupManager()->isAdmin($this->getUserId()), - $server->getUserSession()->getUser() !== null && $server->query(ISubAdmin::class)->isSubAdmin($server->getUserSession()->getUser()), + $c->get(IGroupManager::class), + $c->get(ISubAdmin::class), $server->getAppManager(), $server->getL10N('lib'), $c->get(AuthorizedGroupMapper::class), @@ -241,7 +242,6 @@ class DIContainer extends SimpleContainer implements IAppContainer { new OC\AppFramework\Middleware\Security\CSPMiddleware( $server->query(OC\Security\CSP\ContentSecurityPolicyManager::class), $server->query(OC\Security\CSP\ContentSecurityPolicyNonceManager::class), - $server->query(OC\Security\CSRF\CsrfTokenManager::class) ) ); $dispatcher->registerMiddleware( diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index bbb68972a41..b7952df8d19 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -55,9 +55,9 @@ class Dispatcher { /** * @param Http $protocol the http protocol with contains all status headers * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which - * runs the middleware + * runs the middleware * @param ControllerMethodReflector $reflector the reflector that is used to inject - * the arguments for the controller + * the arguments for the controller * @param IRequest $request the incoming request * @param IConfig $config * @param ConnectionAdapter $connection @@ -89,10 +89,10 @@ class Dispatcher { * Handles a request and calls the dispatcher on the controller * @param Controller $controller the controller which will be called * @param string $methodName the method name which will be called on - * the controller + * the controller * @return array $array[0] contains a string with the http main header, - * $array[1] contains headers in the form: $key => value, $array[2] contains - * the response output + * $array[1] contains headers in the form: $key => value, $array[2] contains + * the response output * @throws \Exception */ public function dispatch(Controller $controller, string $methodName): array { diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index f790dae226c..4bbeabb7aae 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -38,6 +38,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { public const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+( (Vivaldi|Brave|OPR)\/[0-9.]+|)$/'; // Safari User Agent from http://www.useragentstring.com/pages/Safari/ public const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/'; + public const USER_AGENT_SAFARI_MOBILE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ (Mobile\/[0-9.A-Z]+) Safari\/[0-9.A-Z]+$/'; // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; @@ -66,15 +67,15 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * @param array $vars An associative array with the following optional values: - * - array 'urlParams' the parameters which were matched from the URL - * - array 'get' the $_GET array - * - array|string 'post' the $_POST array or JSON string - * - array 'files' the $_FILES array - * - array 'server' the $_SERVER array - * - array 'env' the $_ENV array - * - array 'cookies' the $_COOKIE array - * - string 'method' the request method (GET, POST etc) - * - string|false 'requesttoken' the requesttoken or false when not available + * - array 'urlParams' the parameters which were matched from the URL + * - array 'get' the $_GET array + * - array|string 'post' the $_POST array or JSON string + * - array 'files' the $_FILES array + * - array 'server' the $_SERVER array + * - array 'env' the $_ENV array + * - array 'cookies' the $_COOKIE array + * - string 'method' the request method (GET, POST etc) + * - string|false 'requesttoken' the requesttoken or false when not available * @param IRequestId $requestId * @param IConfig $config * @param CsrfTokenManager|null $csrfTokenManager @@ -283,11 +284,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { * In case of json requests the encoded json body is accessed * * @param string $key the key which you want to access in the URL Parameter - * placeholder, $_POST or $_GET array. - * The priority how they're returned is the following: - * 1. URL parameters - * 2. POST parameters - * 3. GET parameters + * placeholder, $_POST or $_GET array. + * The priority how they're returned is the following: + * 1. URL parameters + * 2. POST parameters + * 3. GET parameters * @param mixed $default If the key is not found, this value will be returned * @return mixed the content of the array */ @@ -836,7 +837,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * Returns the overwritehost setting from the config if set and * if the overwrite condition is met * @return string|null overwritehost value or null if not defined or the defined condition - * isn't met + * isn't met */ private function getOverwriteHost() { if ($this->config->getSystemValueString('overwritehost') !== '' && $this->isOverwriteCondition()) { diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php index 2b5acc8b75f..c9b51f26f34 100644 --- a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php +++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php @@ -23,7 +23,7 @@ class MiddlewareDispatcher { /** * @var int counter which tells us what middleware was executed once an - * exception occurs + * exception occurs */ private int $middlewareCounter; @@ -84,10 +84,10 @@ class MiddlewareDispatcher { * * @param Controller $controller the controller that is being called * @param string $methodName the name of the method that will be called on - * the controller + * the controller * @param \Exception $exception the thrown exception * @return Response a Response object if the middleware can handle the - * exception + * exception * @throws \Exception the passed in exception if it can't handle it */ public function afterException(Controller $controller, string $methodName, \Exception $exception): Response { @@ -109,7 +109,7 @@ class MiddlewareDispatcher { * * @param Controller $controller the controller that is being called * @param string $methodName the name of the method that will be called on - * the controller + * the controller * @param Response $response the generated response from the controller * @return Response a Response object */ diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 3f0755b1b91..10c8f8c7aee 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -31,7 +31,7 @@ use ReflectionMethod; * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ class CORSMiddleware extends Middleware { - /** @var IRequest */ + /** @var IRequest */ private $request; /** @var ControllerMethodReflector */ private $reflector; diff --git a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php index 2115c07c0fc..e88c9563c00 100644 --- a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php @@ -10,7 +10,6 @@ namespace OC\AppFramework\Middleware\Security; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager; -use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; @@ -18,19 +17,11 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; class CSPMiddleware extends Middleware { - /** @var ContentSecurityPolicyManager */ - private $contentSecurityPolicyManager; - /** @var ContentSecurityPolicyNonceManager */ - private $cspNonceManager; - /** @var CsrfTokenManager */ - private $csrfTokenManager; - - public function __construct(ContentSecurityPolicyManager $policyManager, - ContentSecurityPolicyNonceManager $cspNonceManager, - CsrfTokenManager $csrfTokenManager) { - $this->contentSecurityPolicyManager = $policyManager; - $this->cspNonceManager = $cspNonceManager; - $this->csrfTokenManager = $csrfTokenManager; + + public function __construct( + private ContentSecurityPolicyManager $policyManager, + private ContentSecurityPolicyNonceManager $cspNonceManager, + ) { } /** @@ -49,11 +40,11 @@ class CSPMiddleware extends Middleware { return $response; } - $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); - $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + $defaultPolicy = $this->policyManager->getDefaultPolicy(); + $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy); if ($this->cspNonceManager->browserSupportsCspV3()) { - $defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue()); + $defaultPolicy->useJsNonce($this->cspNonceManager->getNonce()); } $response->setContentSecurityPolicy($defaultPolicy); diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index a983de23597..34933e13ecd 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -93,7 +93,7 @@ class PasswordConfirmationMiddleware extends Middleware { return; } - $lastConfirm = (int) $this->session->get('last-password-confirm'); + $lastConfirm = (int)$this->session->get('last-password-confirm'); // TODO: confirm excludedUserBackEnds can go away and remove it if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay throw new NotConfirmedException(); diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index 511ee3fc28a..f4d120ebc30 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -112,8 +112,8 @@ class RateLimitingMiddleware extends Middleware { if ($annotationLimit !== '' && $annotationPeriod !== '') { return new $attributeClass( - (int) $annotationLimit, - (int) $annotationPeriod, + (int)$annotationLimit, + (int)$annotationPeriod, ); } diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php index e0bb96f132b..efe56e0b124 100644 --- a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php @@ -46,13 +46,13 @@ class SameSiteCookieMiddleware extends Middleware { public function afterException($controller, $methodName, \Exception $exception) { if ($exception instanceof LaxSameSiteCookieFailedException) { - $respone = new Response(); - $respone->setStatus(Http::STATUS_FOUND); - $respone->addHeader('Location', $this->request->getRequestUri()); + $response = new Response(); + $response->setStatus(Http::STATUS_FOUND); + $response->addHeader('Location', $this->request->getRequestUri()); $this->setSameSiteCookie(); - return $respone; + return $response; } throw $exception; diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index b8de09072ce..88987290244 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -36,6 +36,8 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\OCSController; +use OCP\Group\ISubAdmin; +use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; @@ -53,6 +55,9 @@ use ReflectionMethod; * check fails */ class SecurityMiddleware extends Middleware { + private ?bool $isAdminUser = null; + private ?bool $isSubAdmin = null; + public function __construct( private IRequest $request, private ControllerMethodReflector $reflector, @@ -61,8 +66,8 @@ class SecurityMiddleware extends Middleware { private LoggerInterface $logger, private string $appName, private bool $isLoggedIn, - private bool $isAdminUser, - private bool $isSubAdmin, + private IGroupManager $groupManager, + private ISubAdmin $subAdminManager, private IAppManager $appManager, private IL10N $l10n, private AuthorizedGroupMapper $groupAuthorizationMapper, @@ -71,6 +76,22 @@ class SecurityMiddleware extends Middleware { ) { } + private function isAdminUser(): bool { + if ($this->isAdminUser === null) { + $user = $this->userSession->getUser(); + $this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID()); + } + return $this->isAdminUser; + } + + private function isSubAdmin(): bool { + if ($this->isSubAdmin === null) { + $user = $this->userSession->getUser(); + $this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user); + } + return $this->isSubAdmin; + } + /** * This runs all the security checks before a method call. The * security checks are determined by inspecting the controller method @@ -114,10 +135,10 @@ class SecurityMiddleware extends Middleware { } if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { - $authorized = $this->isAdminUser; + $authorized = $this->isAdminUser(); if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) { - $authorized = $this->isSubAdmin; + $authorized = $this->isSubAdmin(); } if (!$authorized) { @@ -139,14 +160,14 @@ class SecurityMiddleware extends Middleware { } } if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) - && !$this->isSubAdmin - && !$this->isAdminUser + && !$this->isSubAdmin() + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin')); } if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class) - && !$this->isAdminUser + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin')); } diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php index 9c08f58b384..2031327dfae 100644 --- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -82,9 +82,9 @@ class ControllerMethodReflector implements IControllerMethodReflector { /** * Inspects the PHPDoc parameters for types * @param string $parameter the parameter whose type comments should be - * parsed + * parsed * @return string|null type in the type parameters (@param int $something) - * would return int or null if not existing + * would return int or null if not existing */ public function getType(string $parameter) { if (array_key_exists($parameter, $this->types)) { diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index bf0ef36d13c..56de4a34cf6 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -89,7 +89,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { } // don't lose the error we got while trying to query by type - throw new QueryException($e->getMessage(), (int) $e->getCode(), $e); + throw new QueryException($e->getMessage(), (int)$e->getCode(), $e); } } diff --git a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php index d95bcd98cf9..5781c1edf16 100644 --- a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php +++ b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php @@ -45,7 +45,7 @@ class RemoteWipeNotificationsListener implements IEventListener { $notification->setApp('auth') ->setUser($token->getUID()) ->setDateTime($this->timeFactory->getDateTime()) - ->setObject('token', (string) $token->getId()) + ->setObject('token', (string)$token->getId()) ->setSubject($event, [ 'name' => $token->getName(), ]); diff --git a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php index 697aea71c6d..8523fb6abc7 100644 --- a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php +++ b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php @@ -39,7 +39,7 @@ class UserDeletedFilesCleanupListener implements IEventListener { $userHome = $this->mountProviderCollection->getHomeMountForUser($event->getUser()); $storage = $userHome->getStorage(); if (!$storage) { - throw new \Exception("Account has no home storage"); + throw new \Exception('Account has no home storage'); } // remove all wrappers, so we do the delete directly on the home storage bypassing any wrapper @@ -52,7 +52,7 @@ class UserDeletedFilesCleanupListener implements IEventListener { } if ($event instanceof UserDeletedEvent) { if (!isset($this->homeStorageCache[$event->getUser()->getUID()])) { - throw new \Exception("UserDeletedEvent fired without matching BeforeUserDeletedEvent"); + throw new \Exception('UserDeletedEvent fired without matching BeforeUserDeletedEvent'); } $storage = $this->homeStorageCache[$event->getUser()->getUID()]; $cache = $storage->getCache(); @@ -60,7 +60,7 @@ class UserDeletedFilesCleanupListener implements IEventListener { if ($cache instanceof Cache) { $cache->clear(); } else { - throw new \Exception("Home storage has invalid cache"); + throw new \Exception('Home storage has invalid cache'); } } } diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 0db5c4f53e7..b1341fe1898 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -36,17 +36,20 @@ class PublicKeyTokenMapper extends QBMapper { /** * @param int $olderThan - * @param int $remember + * @param int $type + * @param int|null $remember */ - public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + public function invalidateOld(int $olderThan, int $type = IToken::TEMPORARY_TOKEN, ?int $remember = null) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) + $delete = $qb->delete($this->tableName) ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + if ($remember !== null) { + $delete->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))); + } + $delete->executeStatement(); } public function invalidateLastUsedBefore(string $uid, int $before): int { diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index a3bfa3147a5..4eddd5c80f7 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -162,7 +162,7 @@ class PublicKeyTokenProvider implements IProvider { $this->rotate($token, $tokenId, $tokenId); } catch (DoesNotExistException) { $this->cacheInvalidHash($tokenHash); - throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); + throw new InvalidTokenException('Token does not exist: ' . $ex->getMessage(), 0, $ex); } } @@ -232,7 +232,7 @@ class PublicKeyTokenProvider implements IProvider { $token = $this->getToken($oldSessionId); if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $password = null; @@ -281,10 +281,19 @@ class PublicKeyTokenProvider implements IProvider { public function invalidateOldTokens() { $olderThan = $this->time->getTime() - $this->config->getSystemValueInt('session_lifetime', 60 * 60 * 24); $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); - $this->mapper->invalidateOld($olderThan, OCPIToken::DO_NOT_REMEMBER); + $this->mapper->invalidateOld($olderThan, OCPIToken::TEMPORARY_TOKEN, OCPIToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); - $this->mapper->invalidateOld($rememberThreshold, OCPIToken::REMEMBER); + $this->mapper->invalidateOld($rememberThreshold, OCPIToken::TEMPORARY_TOKEN, OCPIToken::REMEMBER); + + $wipeThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_wipe_token_retention', 60 * 60 * 24 * 60); + $this->logger->debug('Invalidating auth tokens marked for remote wipe older than ' . date('c', $wipeThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($wipeThreshold, OCPIToken::WIPE_TOKEN); + + $authTokenThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_token_retention', 60 * 60 * 24 * 365); + $this->logger->debug('Invalidating auth tokens older than ' . date('c', $authTokenThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($authTokenThreshold, OCPIToken::PERMANENT_TOKEN); } public function invalidateLastUsedBefore(string $uid, int $before): void { @@ -293,7 +302,7 @@ class PublicKeyTokenProvider implements IProvider { public function updateToken(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $this->mapper->update($token); $this->cacheToken($token); @@ -301,7 +310,7 @@ class PublicKeyTokenProvider implements IProvider { public function updateTokenActivity(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60); @@ -322,7 +331,7 @@ class PublicKeyTokenProvider implements IProvider { public function getPassword(OCPIToken $savedToken, string $tokenId): string { if (!($savedToken instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } if ($savedToken->getPassword() === null) { @@ -338,7 +347,7 @@ class PublicKeyTokenProvider implements IProvider { public function setPassword(OCPIToken $token, string $tokenId, string $password) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $this->atomic(function () use ($password, $token) { @@ -363,7 +372,7 @@ class PublicKeyTokenProvider implements IProvider { public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } // Decrypt private key with oldTokenId @@ -396,7 +405,7 @@ class PublicKeyTokenProvider implements IProvider { } catch (\Exception $ex2) { // Delete the invalid token $this->invalidateToken($token); - throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2); + throw new InvalidTokenException('Could not decrypt token password: ' . $ex->getMessage(), 0, $ex2); } } } @@ -486,7 +495,7 @@ class PublicKeyTokenProvider implements IProvider { public function markPasswordInvalid(OCPIToken $token, string $tokenId) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $token->setPasswordInvalid(true); diff --git a/lib/private/Authentication/Token/RemoteWipe.php b/lib/private/Authentication/Token/RemoteWipe.php index 43c2bd060d1..80ba330b66d 100644 --- a/lib/private/Authentication/Token/RemoteWipe.php +++ b/lib/private/Authentication/Token/RemoteWipe.php @@ -98,7 +98,7 @@ class RemoteWipe { $dbToken = $e->getToken(); - $this->logger->info("user " . $dbToken->getUID() . " started a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' started a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeStarted::class, new RemoteWipeStarted($dbToken)); @@ -126,7 +126,7 @@ class RemoteWipe { $this->tokenProvider->invalidateToken($token); - $this->logger->info("user " . $dbToken->getUID() . " finished a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' finished a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeFinished::class, new RemoteWipeFinished($dbToken)); return true; diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php index c84b7f1af20..cdbd8b48cf2 100644 --- a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php +++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php @@ -29,7 +29,7 @@ class ProviderUserAssignmentDao { * Get all assigned provider IDs for the given user ID * * @return array<string, bool> where the array key is the provider ID (string) and the - * value is the enabled state (bool) + * value is the enabled state (bool) */ public function getState(string $uid): array { $qb = $this->conn->getQueryBuilder(); @@ -95,7 +95,7 @@ class ProviderUserAssignmentDao { return [ 'provider_id' => (string)$row['provider_id'], 'uid' => (string)$row['uid'], - 'enabled' => ((int) $row['enabled']) === 1, + 'enabled' => ((int)$row['enabled']) === 1, ]; }, $rows)); } diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 2585646c998..072ffc4f86f 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -192,7 +192,7 @@ class Manager { if (!empty($missing)) { // There was at least one provider missing - $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']); + $this->logger->alert(count($missing) . ' two-factor auth providers failed to load', ['app' => 'core']); return true; } @@ -322,7 +322,7 @@ class Manager { $tokenId = $token->getId(); $tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa'); - if (!\in_array((string) $tokenId, $tokensNeeding2FA, true)) { + if (!\in_array((string)$tokenId, $tokensNeeding2FA, true)) { $this->session->set(self::SESSION_UID_DONE, $user->getUID()); return false; } @@ -359,7 +359,7 @@ class Manager { $id = $this->session->getId(); $token = $this->tokenProvider->getToken($id); - $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string) $token->getId(), (string)$this->timeFactory->getTime()); + $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string)$token->getId(), (string)$this->timeFactory->getTime()); } public function clearTwoFactorPending(string $userId) { diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php index f32136f9594..203f2ef9020 100644 --- a/lib/private/Authentication/WebAuthn/CredentialRepository.php +++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php @@ -44,7 +44,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository { }, $entities); } - public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity { + public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity { $oldEntity = null; try { @@ -58,13 +58,18 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository { $name = 'default'; } - $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource); + $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification); if ($oldEntity) { $entity->setId($oldEntity->getId()); if ($defaultName) { $entity->setName($oldEntity->getName()); } + + // Don't downgrade UV just because it was skipped during a login due to another key + if ($oldEntity->getUserVerification()) { + $entity->setUserVerification(true); + } } return $this->credentialMapper->insertOrUpdate($entity); diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php index 443a7985cae..6c4bc3ca81b 100644 --- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php +++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php @@ -23,6 +23,10 @@ use Webauthn\PublicKeyCredentialSource; * @method void setPublicKeyCredentialId(string $id); * @method string getData(); * @method void setData(string $data); + * + * @since 30.0.0 Add userVerification attribute + * @method bool|null getUserVerification(); + * @method void setUserVerification(bool $userVerification); */ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ @@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ protected $data; + /** @var bool|null */ + protected $userVerification; + public function __construct() { $this->addType('name', 'string'); $this->addType('uid', 'string'); $this->addType('publicKeyCredentialId', 'string'); $this->addType('data', 'string'); + $this->addType('userVerification', 'boolean'); } - public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity { + public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity { $publicKeyCredentialEntity = new self(); $publicKeyCredentialEntity->setName($name); $publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle()); $publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId())); $publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource)); + $publicKeyCredentialEntity->setUserVerification($userVerification); return $publicKeyCredentialEntity; } diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php index 007be245992..7aa7a3c8f3a 100644 --- a/lib/private/Authentication/WebAuthn/Manager.php +++ b/lib/private/Authentication/WebAuthn/Manager.php @@ -88,8 +88,8 @@ class Manager { ]; $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria( - null, - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, null, false, ); @@ -151,7 +151,8 @@ class Manager { } // Persist the data - return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name); + $userVerification = $response->attestationObject->authData->isUserVerified(); + return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification); } private function stripPort(string $serverHost): string { @@ -160,7 +161,11 @@ class Manager { public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions { // List of registered PublicKeyCredentialDescriptor classes associated to the user - $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED; + $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) { + if ($entity->getUserVerification() !== true) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } $credential = $entity->toPublicKeyCredentialSource(); return new PublicKeyCredentialDescriptor( $credential->type, @@ -173,7 +178,7 @@ class Manager { random_bytes(32), // Challenge $this->stripPort($serverHost), // Relying Party ID $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + $userVerificationRequirement, 60000, // Timeout ); } diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index 1ad70001f13..bf29d57b88d 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -88,8 +88,8 @@ abstract class Avatar implements IAvatar { $userDisplayName = $this->getDisplayName(); $fgRGB = $this->avatarBackgroundColor($userDisplayName); $bgRGB = $fgRGB->alphaBlending(0.1, $darkTheme ? new Color(0, 0, 0) : new Color(255, 255, 255)); - $fill = sprintf("%02x%02x%02x", $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); - $fgFill = sprintf("%02x%02x%02x", $fgRGB->red(), $fgRGB->green(), $fgRGB->blue()); + $fill = sprintf('%02x%02x%02x', $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); + $fgFill = sprintf('%02x%02x%02x', $fgRGB->red(), $fgRGB->green(), $fgRGB->blue()); $text = $this->getAvatarText(); $toReplace = ['{size}', '{fill}', '{fgFill}', '{letter}']; return str_replace($toReplace, [$size, $fill, $fgFill, $text], $this->svgTemplate); @@ -104,7 +104,7 @@ abstract class Avatar implements IAvatar { } $formats = Imagick::queryFormats(); // Avatar generation breaks if RSVG format is enabled. Fall back to gd in that case - if (in_array("RSVG", $formats, true)) { + if (in_array('RSVG', $formats, true)) { return null; } try { diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php index f8ce4d5b656..60a3d358bf4 100644 --- a/lib/private/Avatar/AvatarManager.php +++ b/lib/private/Avatar/AvatarManager.php @@ -115,7 +115,7 @@ class AvatarManager implements IAvatarManager { $folder->delete(); } catch (NotFoundException $e) { $this->logger->debug("No cache for the user $userId. Ignoring avatar deletion"); - } catch (NotPermittedException | StorageNotAvailableException $e) { + } catch (NotPermittedException|StorageNotAvailableException $e) { $this->logger->error("Unable to delete user avatars for $userId. gnoring avatar deletion"); } catch (NoUserException $e) { $this->logger->debug("Account $userId not found. Ignoring avatar deletion"); diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php index 3978ae635f7..5059199a182 100644 --- a/lib/private/BackgroundJob/JobList.php +++ b/lib/private/BackgroundJob/JobList.php @@ -129,7 +129,7 @@ class JobList implements IJobList { $row = $result->fetch(); $result->closeCursor(); - return (bool) $row; + return (bool)$row; } public function getJobs($job, ?int $limit, int $offset): array { @@ -302,8 +302,8 @@ class JobList implements IJobList { // This most likely means an invalid job was enqueued. We can ignore it. return null; } - $job->setId((int) $row['id']); - $job->setLastRun((int) $row['last_run']); + $job->setId((int)$row['id']); + $job->setLastRun((int)$row['last_run']); $job->setArgument(json_decode($row['argument'], true)); return $job; } catch (AutoloadNotAllowedException $e) { diff --git a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php index bb5bc3cadb2..1d0a502df6d 100644 --- a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php +++ b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php @@ -70,7 +70,7 @@ class GenerateBlurhashMetadata implements IEventListener { } $metadata->setString('blurhash', $this->generateBlurHash($image)) - ->setEtag('blurhash', $currentEtag); + ->setEtag('blurhash', $currentEtag); } /** diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index 7ae577c9d7f..fa324273f5c 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -53,7 +53,7 @@ class Manager implements IManager { * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match * @param array $options - optional parameters: - * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]] + * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]] * @param integer|null $limit - limit number of search results * @param integer|null $offset - offset for paging of search results * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs @@ -193,6 +193,7 @@ class Manager implements IManager { foreach ($r as $o) { $o['calendar-key'] = $calendar->getKey(); + $o['calendar-uri'] = $calendar->getUri(); $results[] = $o; } } diff --git a/lib/private/Calendar/ResourcesRoomsUpdater.php b/lib/private/Calendar/ResourcesRoomsUpdater.php index ae2a2f3a650..eacdaf0aeb4 100644 --- a/lib/private/Calendar/ResourcesRoomsUpdater.php +++ b/lib/private/Calendar/ResourcesRoomsUpdater.php @@ -406,7 +406,7 @@ class ResourcesRoomsUpdater { ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); $result = $query->executeQuery(); - $id = (int) $result->fetchOne(); + $id = (int)$result->fetchOne(); $result->closeCursor(); return $id; } diff --git a/lib/private/Collaboration/AutoComplete/Manager.php b/lib/private/Collaboration/AutoComplete/Manager.php index d7298d9deef..382b9188535 100644 --- a/lib/private/Collaboration/AutoComplete/Manager.php +++ b/lib/private/Collaboration/AutoComplete/Manager.php @@ -13,7 +13,7 @@ class Manager implements IManager { /** @var string[] */ protected array $sorters = []; - /** @var ISorter[] */ + /** @var ISorter[] */ protected array $sorterInstances = []; public function __construct( diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index b4cb77ad5b8..d196abae042 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -73,7 +73,7 @@ class UserPlugin implements ISearchPlugin { foreach ($currentUserGroups as $userGroupId) { $usersInGroup = $this->groupManager->displayNamesInGroup($userGroupId, $search, $limit, $offset); foreach ($usersInGroup as $userId => $displayName) { - $userId = (string) $userId; + $userId = (string)$userId; $user = $this->userManager->get($userId); if (!$user->isEnabled()) { // Ignore disabled users @@ -130,7 +130,7 @@ class UserPlugin implements ISearchPlugin { foreach ($users as $uid => $user) { $userDisplayName = $user->getDisplayName(); $userEmail = $user->getSystemEMailAddress(); - $uid = (string) $uid; + $uid = (string)$uid; $status = []; if (array_key_exists($uid, $userStatuses)) { diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php index 5a1b39d9dff..9287b66b2a2 100644 --- a/lib/private/Collaboration/Reference/ReferenceManager.php +++ b/lib/private/Collaboration/Reference/ReferenceManager.php @@ -231,7 +231,7 @@ class ReferenceManager implements IReferenceManager { } $configKey = 'provider-last-use_' . $providerId; - $this->config->setUserValue($userId, 'references', $configKey, (string) $timestamp); + $this->config->setUserValue($userId, 'references', $configKey, (string)$timestamp); return true; } return false; @@ -254,7 +254,7 @@ class ReferenceManager implements IReferenceManager { $timestamps = []; foreach ($keys as $key) { $providerId = substr($key, strlen($prefix)); - $timestamp = (int) $this->config->getUserValue($userId, 'references', $key); + $timestamp = (int)$this->config->getUserValue($userId, 'references', $key); $timestamps[$providerId] = $timestamp; } return $timestamps; diff --git a/lib/private/Collaboration/Resources/Manager.php b/lib/private/Collaboration/Resources/Manager.php index 6c9b77d41c5..8d1e4b13287 100644 --- a/lib/private/Collaboration/Resources/Manager.php +++ b/lib/private/Collaboration/Resources/Manager.php @@ -53,7 +53,7 @@ class Manager implements IManager { throw new CollectionException('Collection not found'); } - return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name']); + return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name']); } /** @@ -82,12 +82,12 @@ class Manager implements IManager { throw new CollectionException('Collection not found'); } - $access = $row['access'] === null ? null : (bool) $row['access']; + $access = $row['access'] === null ? null : (bool)$row['access']; if ($user instanceof IUser) { - return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access); + return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access); } - return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access); + return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access); } /** @@ -122,7 +122,7 @@ class Manager implements IManager { $foundResults = 0; while ($row = $result->fetch()) { $foundResults++; - $access = $row['access'] === null ? null : (bool) $row['access']; + $access = $row['access'] === null ? null : (bool)$row['access']; $collection = new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access); if ($collection->canAccess($user)) { $collections[] = $collection; @@ -186,7 +186,7 @@ class Manager implements IManager { throw new ResourceException('Resource not found'); } - $access = $row['access'] === null ? null : (bool) $row['access']; + $access = $row['access'] === null ? null : (bool)$row['access']; if ($user instanceof IUser) { return new Resource($this, $this->connection, $type, $id, $user, $access); } @@ -217,7 +217,7 @@ class Manager implements IManager { $resources = []; $result = $query->execute(); while ($row = $result->fetch()) { - $access = $row['access'] === null ? null : (bool) $row['access']; + $access = $row['access'] === null ? null : (bool)$row['access']; $resources[] = new Resource($this, $this->connection, $row['resource_type'], $row['resource_id'], $user, $access); } $result->closeCursor(); @@ -311,7 +311,7 @@ class Manager implements IManager { $hasAccess = null; $result = $query->execute(); if ($row = $result->fetch()) { - $hasAccess = (bool) $row['access']; + $hasAccess = (bool)$row['access']; } $result->closeCursor(); @@ -331,7 +331,7 @@ class Manager implements IManager { $hasAccess = null; $result = $query->execute(); if ($row = $result->fetch()) { - $hasAccess = (bool) $row['access']; + $hasAccess = (bool)$row['access']; } $result->closeCursor(); diff --git a/lib/private/Collaboration/Resources/Resource.php b/lib/private/Collaboration/Resources/Resource.php index ae011e319de..34f68aeee11 100644 --- a/lib/private/Collaboration/Resources/Resource.php +++ b/lib/private/Collaboration/Resources/Resource.php @@ -104,7 +104,7 @@ class Resource implements IResource { $result = $query->execute(); while ($row = $result->fetch()) { - $collections[] = $this->manager->getCollection((int) $row['collection_id']); + $collections[] = $this->manager->getCollection((int)$row['collection_id']); } $result->closeCursor(); diff --git a/lib/private/Comments/Comment.php b/lib/private/Comments/Comment.php index 422e29c084d..3c44c02fe2e 100644 --- a/lib/private/Comments/Comment.php +++ b/lib/private/Comments/Comment.php @@ -34,8 +34,8 @@ class Comment implements IComment { /** * Comment constructor. * - * @param array $data optional, array with keys according to column names from - * the comments database scheme + * @param array $data optional, array with keys according to column names from + * the comments database scheme */ public function __construct(?array $data = null) { if (is_array($data)) { diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index 21ff5322f23..9237c884691 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -33,10 +33,10 @@ class Manager implements ICommentsManager { /** @var IComment[] */ protected array $commentsCache = []; - /** @var \Closure[] */ + /** @var \Closure[] */ protected array $eventHandlerClosures = []; - /** @var ICommentsEventHandler[] */ + /** @var ICommentsEventHandler[] */ protected array $eventHandlers = []; /** @var \Closure[] */ @@ -308,10 +308,10 @@ class Manager implements ICommentsManager { * @param string $objectType the object type, e.g. 'files' * @param string $objectId the id of the object * @param int $limit optional, number of maximum comments to be returned. if - * not specified, all comments are returned. + * not specified, all comments are returned. * @param int $offset optional, starting point * @param \DateTime $notOlderThan optional, timestamp of the oldest comments - * that may be returned + * that may be returned * @return list<IComment> * @since 9.0.0 */ @@ -362,7 +362,7 @@ class Manager implements ICommentsManager { * @param int $lastKnownCommentId the last known comment (will be used as offset) * @param string $sortDirection direction of the comments (`asc` or `desc`) * @param int $limit optional, number of maximum comments to be returned. if - * set to 0, all comments are returned. + * set to 0, all comments are returned. * @param bool $includeLastKnown * @return list<IComment> */ @@ -392,7 +392,7 @@ class Manager implements ICommentsManager { * @param int $lastKnownCommentId the last known comment (will be used as offset) * @param string $sortDirection direction of the comments (`asc` or `desc`) * @param int $limit optional, number of maximum comments to be returned. if - * set to 0, all comments are returned. + * set to 0, all comments are returned. * @param bool $includeLastKnown * @return list<IComment> */ @@ -608,7 +608,7 @@ class Manager implements ICommentsManager { * @param $objectType string the object type, e.g. 'files' * @param $objectId string the id of the object * @param \DateTime $notOlderThan optional, timestamp of the oldest comments - * that may be returned + * that may be returned * @param string $verb Limit the verb of the comment - Added in 14.0.0 * @return Int * @since 9.0.0 @@ -675,7 +675,7 @@ class Manager implements ICommentsManager { $result = $query->executeQuery(); while ($row = $result->fetch()) { - $unreadComments[$row['object_id']] = (int) $row['num_comments']; + $unreadComments[$row['object_id']] = (int)$row['num_comments']; } $result->closeCursor(); } @@ -723,7 +723,7 @@ class Manager implements ICommentsManager { $data = $result->fetch(); $result->closeCursor(); - return (int) ($data['num_messages'] ?? 0); + return (int)($data['num_messages'] ?? 0); } /** @@ -751,7 +751,7 @@ class Manager implements ICommentsManager { $data = $result->fetch(); $result->closeCursor(); - return (int) ($data['id'] ?? 0); + return (int)($data['id'] ?? 0); } /** @@ -808,9 +808,9 @@ class Manager implements ICommentsManager { return []; } $children = $directory->getDirectoryListing(); - $ids = array_map(fn (FileInfo $child) => (string) $child->getId(), $children); + $ids = array_map(fn (FileInfo $child) => (string)$child->getId(), $children); - $ids[] = (string) $directory->getId(); + $ids[] = (string)$directory->getId(); $counts = $this->getNumberOfUnreadCommentsForObjects('files', $ids, $user); return array_filter($counts, function (int $count) { return $count > 0; @@ -1079,7 +1079,7 @@ class Manager implements ICommentsManager { $result = $this->update($comment); } - if ($result && !!$comment->getParentId()) { + if ($result && (bool)$comment->getParentId()) { $this->updateChildrenInformation( $comment->getParentId(), $comment->getCreationDateTime() @@ -1141,7 +1141,7 @@ class Manager implements ICommentsManager { ->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($reaction->getActorId()))) ->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction->getMessage()))); $result = $qb->executeQuery(); - $exists = (int) $result->fetchOne(); + $exists = (int)$result->fetchOne(); if (!$exists) { $qb = $this->dbConn->getQueryBuilder(); try { diff --git a/lib/private/Config.php b/lib/private/Config.php index ee30b8efc5e..6823ab7027f 100644 --- a/lib/private/Config.php +++ b/lib/private/Config.php @@ -184,7 +184,7 @@ class Config { // Invalidate opcache (only if the timestamp changed) if (function_exists('opcache_invalidate')) { - opcache_invalidate($file, false); + @opcache_invalidate($file, false); } $filePointer = @fopen($file, 'r'); @@ -268,7 +268,7 @@ class Config { $df = disk_free_space($this->configDir); $size = strlen($content) + 10240; if ($df !== false && $df < (float)$size) { - throw new \Exception($this->configDir . " does not have enough space for writing the config file! Not writing it back!"); + throw new \Exception($this->configDir . ' does not have enough space for writing the config file! Not writing it back!'); } } diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php index 14baa528940..16ed8894386 100644 --- a/lib/private/Console/Application.php +++ b/lib/private/Console/Application.php @@ -94,7 +94,7 @@ class Application { try { $this->loadCommandsFromInfoXml($info['commands']); } catch (\Throwable $e) { - $output->writeln("<error>" . $e->getMessage() . "</error>"); + $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->error($e->getMessage(), [ 'exception' => $e, ]); @@ -116,13 +116,13 @@ class Application { } } elseif ($input->getArgument('command') !== '_completion' && $input->getArgument('command') !== 'maintenance:install') { $errorOutput = $output->getErrorOutput(); - $errorOutput->writeln("Nextcloud is not installed - only a limited number of commands are available"); + $errorOutput->writeln('Nextcloud is not installed - only a limited number of commands are available'); } } catch (NeedsUpdateException $e) { if ($input->getArgument('command') !== '_completion') { $errorOutput = $output->getErrorOutput(); - $errorOutput->writeln("Nextcloud or one of the apps require upgrade - only a limited number of commands are available"); - $errorOutput->writeln("You may use your browser or the occ upgrade command to do the upgrade"); + $errorOutput->writeln('Nextcloud or one of the apps require upgrade - only a limited number of commands are available'); + $errorOutput->writeln('You may use your browser or the occ upgrade command to do the upgrade'); } } @@ -134,7 +134,7 @@ class Application { $output->writeln((string)$error['hint']); $output->writeln(''); } - throw new \Exception("Environment not properly prepared."); + throw new \Exception('Environment not properly prepared.'); } } } @@ -145,7 +145,7 @@ class Application { * * @param InputInterface $input The input implementation for reading inputs. * @param ConsoleOutputInterface $output The output implementation - * for writing outputs. + * for writing outputs. * @return void */ private function writeMaintenanceModeInfo(InputInterface $input, ConsoleOutputInterface $output): void { diff --git a/lib/private/Console/TimestampFormatter.php b/lib/private/Console/TimestampFormatter.php index de0675cc5df..da1b7ba48dd 100644 --- a/lib/private/Console/TimestampFormatter.php +++ b/lib/private/Console/TimestampFormatter.php @@ -81,7 +81,7 @@ class TimestampFormatter implements OutputFormatterInterface { * * @param string|null $message The message to style * @return string|null The styled message, prepended with a timestamp using the - * log timezone and dateformat, e.g. "2015-06-23T17:24:37+02:00" + * log timezone and dateformat, e.g. "2015-06-23T17:24:37+02:00" */ public function format(?string $message): ?string { if (!$this->formatter->isDecorated()) { diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index d7cdb5efebf..d15e6e35706 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -348,7 +348,7 @@ class ContactsStore implements IContactsStore { $entry->setFullName($contact['FN']); } - $avatarPrefix = "VALUE=uri:"; + $avatarPrefix = 'VALUE=uri:'; if (!empty($contact['PHOTO']) && str_starts_with($contact['PHOTO'], $avatarPrefix)) { $entry->setAvatar(substr($contact['PHOTO'], strlen($avatarPrefix))); } diff --git a/lib/private/ContactsManager.php b/lib/private/ContactsManager.php index f67cb196eef..7dd2bf33124 100644 --- a/lib/private/ContactsManager.php +++ b/lib/private/ContactsManager.php @@ -19,14 +19,14 @@ class ContactsManager implements IManager { * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match * @param array $options = array() to define the search behavior - * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array - * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']] - * - 'escape_like_param' - If set to false wildcards _ and % are not escaped - * - 'limit' - Set a numeric limit for the search results - * - 'offset' - Set the offset for the limited search results - * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed - * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system address book is allowed - * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search + * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array + * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']] + * - 'escape_like_param' - If set to false wildcards _ and % are not escaped + * - 'limit' - Set a numeric limit for the search results + * - 'offset' - Set the offset for the limited search results + * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed + * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system address book is allowed + * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, enumeration?: bool, fullmatch?: bool, strict_search?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs */ diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php index b5be14e5dc6..71824bda9e8 100644 --- a/lib/private/DB/Adapter.php +++ b/lib/private/DB/Adapter.php @@ -32,7 +32,7 @@ class Adapter { * @throws Exception */ public function lastInsertId($table) { - return (int) $this->conn->realLastInsertId($table); + return (int)$this->conn->realLastInsertId($table); } /** @@ -46,11 +46,10 @@ class Adapter { /** * Create an exclusive read+write lock on a table * - * @param string $tableName * @throws Exception * @since 9.1.0 */ - public function lockTable($tableName) { + public function lockTable(string $tableName) { $this->conn->beginTransaction(); $this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE'); } @@ -73,19 +72,21 @@ class Adapter { * @param string $table The table name (will replace *PREFIX* with the actual prefix) * @param array $input data that should be inserted into the table (column name => value) * @param array|null $compare List of values that should be checked for "if not exists" - * If this is null or an empty array, all keys of $input will be compared - * Please note: text fields (clob) must not be used in the compare array + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array * @return int number of inserted rows * @throws Exception * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371 */ public function insertIfNotExist($table, $input, ?array $compare = null) { - if (empty($compare)) { - $compare = array_keys($input); - } - $query = 'INSERT INTO `' .$table . '` (`' - . implode('`,`', array_keys($input)) . '`) SELECT ' - . str_repeat('?,', count($input) - 1).'? ' // Is there a prettier alternative? + $compare = $compare ?: array_keys($input); + + // Prepare column names and generate placeholders + $columns = '`' . implode('`,`', array_keys($input)) . '`'; + $placeholders = implode(', ', array_fill(0, count($input), '?')); + + $query = 'INSERT INTO `' . $table . '` (' . $columns . ') ' + . 'SELECT ' . $placeholders . ' ' . 'FROM `' . $table . '` WHERE '; $inserts = array_values($input); @@ -104,10 +105,9 @@ class Adapter { try { return $this->conn->executeUpdate($query, $inserts); } catch (UniqueConstraintViolationException $e) { - // if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it - // it's fine to ignore this then - // - // more discussions about this can be found at https://github.com/nextcloud/server/pull/12315 + // This exception indicates a concurrent insert happened between + // the insert and the sub-select in the insert, which is safe to ignore. + // More details: https://github.com/nextcloud/server/pull/12315 return 0; } } diff --git a/lib/private/DB/AdapterSqlite.php b/lib/private/DB/AdapterSqlite.php index 24274cbcda6..0023ee15364 100644 --- a/lib/private/DB/AdapterSqlite.php +++ b/lib/private/DB/AdapterSqlite.php @@ -38,8 +38,8 @@ class AdapterSqlite extends Adapter { * @param string $table The table name (will replace *PREFIX* with the actual prefix) * @param array $input data that should be inserted into the table (column name => value) * @param array|null $compare List of values that should be checked for "if not exists" - * If this is null or an empty array, all keys of $input will be compared - * Please note: text fields (clob) must not be used in the compare array + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array * @return int number of inserted rows * @throws \Doctrine\DBAL\Exception * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371 diff --git a/lib/private/DB/ArrayResult.php b/lib/private/DB/ArrayResult.php new file mode 100644 index 00000000000..b567ad23d57 --- /dev/null +++ b/lib/private/DB/ArrayResult.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB; + +use OCP\DB\IResult; +use PDO; + +/** + * Wrap an array or rows into a result interface + */ +class ArrayResult implements IResult { + protected int $count; + + public function __construct( + protected array $rows, + ) { + $this->count = count($this->rows); + } + + public function closeCursor(): bool { + // noop + return true; + } + + public function fetch(int $fetchMode = PDO::FETCH_ASSOC) { + $row = array_shift($this->rows); + if (!$row) { + return false; + } + return match ($fetchMode) { + PDO::FETCH_ASSOC => $row, + PDO::FETCH_NUM => array_values($row), + PDO::FETCH_COLUMN => current($row), + default => throw new \InvalidArgumentException('Fetch mode not supported for array result'), + }; + + } + + public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array { + return match ($fetchMode) { + PDO::FETCH_ASSOC => $this->rows, + PDO::FETCH_NUM => array_map(function ($row) { + return array_values($row); + }, $this->rows), + PDO::FETCH_COLUMN => array_map(function ($row) { + return current($row); + }, $this->rows), + default => throw new \InvalidArgumentException('Fetch mode not supported for array result'), + }; + } + + public function fetchColumn() { + return $this->fetchOne(); + } + + public function fetchOne() { + $row = $this->fetch(); + if ($row) { + return current($row); + } else { + return false; + } + } + + public function rowCount(): int { + return $this->count; + } +} diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 3cdd5fd06c0..ecc6d09bc95 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -23,11 +23,21 @@ use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Statement; +use OC\DB\QueryBuilder\Partitioned\PartitionedQueryBuilder; +use OC\DB\QueryBuilder\Partitioned\PartitionSplit; use OC\DB\QueryBuilder\QueryBuilder; +use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler; +use OC\DB\QueryBuilder\Sharded\CrossShardMoveHelper; +use OC\DB\QueryBuilder\Sharded\RoundRobinShardMapper; +use OC\DB\QueryBuilder\Sharded\ShardConnectionManager; +use OC\DB\QueryBuilder\Sharded\ShardDefinition; use OC\SystemConfig; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\Sharded\IShardMapper; use OCP\Diagnostics\IEventLogger; +use OCP\ICacheFactory; use OCP\IDBConnection; +use OCP\ILogger; use OCP\IRequestId; use OCP\PreConditionNotMetException; use OCP\Profiler\IProfiler; @@ -75,6 +85,29 @@ class Connection extends PrimaryReadReplicaConnection { protected bool $logRequestId; protected string $requestId; + /** @var array<string, list<string>> */ + protected array $partitions; + /** @var ShardDefinition[] */ + protected array $shards = []; + protected ShardConnectionManager $shardConnectionManager; + protected AutoIncrementHandler $autoIncrementHandler; + protected bool $isShardingEnabled; + + public const SHARD_PRESETS = [ + 'filecache' => [ + 'companion_keys' => [ + 'file_id', + ], + 'companion_tables' => [ + 'filecache_extended', + 'files_metadata', + ], + 'primary_key' => 'fileid', + 'shard_key' => 'storage', + 'table' => 'filecache', + ], + ]; + /** * Initializes a new instance of the Connection class. * @@ -98,7 +131,17 @@ class Connection extends PrimaryReadReplicaConnection { parent::__construct($params, $driver, $config, $eventManager); $this->adapter = new $params['adapter']($this); $this->tablePrefix = $params['tablePrefix']; - + $this->isShardingEnabled = isset($this->params['sharding']) && !empty($this->params['sharding']); + + if ($this->isShardingEnabled) { + /** @psalm-suppress InvalidArrayOffset */ + $this->shardConnectionManager = $this->params['shard_connection_manager'] ?? Server::get(ShardConnectionManager::class); + /** @psalm-suppress InvalidArrayOffset */ + $this->autoIncrementHandler = $this->params['auto_increment_handler'] ?? new AutoIncrementHandler( + Server::get(ICacheFactory::class), + $this->shardConnectionManager, + ); + } $this->systemConfig = \OC::$server->getSystemConfig(); $this->clock = Server::get(ClockInterface::class); $this->logger = Server::get(LoggerInterface::class); @@ -117,10 +160,54 @@ class Connection extends PrimaryReadReplicaConnection { $this->_config->setSQLLogger($debugStack); } + /** @var array<string, array{shards: array[], mapper: ?string}> $shardConfig */ + $shardConfig = $this->params['sharding'] ?? []; + $shardNames = array_keys($shardConfig); + $this->shards = array_map(function (array $config, string $name) { + if (!isset(self::SHARD_PRESETS[$name])) { + throw new \Exception("Shard preset $name not found"); + } + + $shardMapperClass = $config['mapper'] ?? RoundRobinShardMapper::class; + $shardMapper = Server::get($shardMapperClass); + if (!$shardMapper instanceof IShardMapper) { + throw new \Exception("Invalid shard mapper: $shardMapperClass"); + } + return new ShardDefinition( + self::SHARD_PRESETS[$name]['table'], + self::SHARD_PRESETS[$name]['primary_key'], + self::SHARD_PRESETS[$name]['companion_keys'], + self::SHARD_PRESETS[$name]['shard_key'], + $shardMapper, + self::SHARD_PRESETS[$name]['companion_tables'], + $config['shards'] + ); + }, $shardConfig, $shardNames); + $this->shards = array_combine($shardNames, $this->shards); + $this->partitions = array_map(function (ShardDefinition $shard) { + return array_merge([$shard->table], $shard->companionTables); + }, $this->shards); + $this->setNestTransactionsWithSavepoints(true); } /** + * @return IDBConnection[] + */ + public function getShardConnections(): array { + $connections = []; + if ($this->isShardingEnabled) { + foreach ($this->shards as $shardDefinition) { + foreach ($shardDefinition->getAllShards() as $shard) { + /** @var ConnectionAdapter $connection */ + $connections[] = $this->shardConnectionManager->getConnection($shardDefinition, $shard); + } + } + } + return $connections; + } + + /** * @throws Exception */ public function connect($connectionName = null) { @@ -168,11 +255,27 @@ class Connection extends PrimaryReadReplicaConnection { */ public function getQueryBuilder(): IQueryBuilder { $this->queriesBuilt++; - return new QueryBuilder( + + $builder = new QueryBuilder( new ConnectionAdapter($this), $this->systemConfig, $this->logger ); + if ($this->isShardingEnabled && count($this->partitions) > 0) { + $builder = new PartitionedQueryBuilder( + $builder, + $this->shards, + $this->shardConnectionManager, + $this->autoIncrementHandler, + ); + foreach ($this->partitions as $name => $tables) { + $partition = new PartitionSplit($name, $tables); + $builder->addPartition($partition); + } + return $builder; + } else { + return $builder; + } } /** @@ -239,10 +342,10 @@ class Connection extends PrimaryReadReplicaConnection { if ($limit === -1 || $limit === null) { $limit = null; } else { - $limit = (int) $limit; + $limit = (int)$limit; } if ($offset !== null) { - $offset = (int) $offset; + $offset = (int)$offset; } if (!is_null($limit)) { $platform = $this->getDatabasePlatform(); @@ -259,10 +362,10 @@ class Connection extends PrimaryReadReplicaConnection { * If the query is parametrized, a prepared statement is used. * If an SQLLogger is configured, the execution is logged. * - * @param string $sql The SQL query to execute. - * @param array $params The parameters to bind to the query, if any. - * @param array $types The types the previous parameters are in. - * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional. + * @param string $sql The SQL query to execute. + * @param array $params The parameters to bind to the query, if any. + * @param array $types The types the previous parameters are in. + * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional. * * @return Result The executed statement. * @@ -291,7 +394,7 @@ class Connection extends PrimaryReadReplicaConnection { // Read to a table that has been written to previously // While this might not necessarily mean that we did a read after write it is an indication for a code path to check $this->logger->log( - (int) ($this->systemConfig->getValue('loglevel_dirty_database_queries', null) ?? 0), + (int)($this->systemConfig->getValue('loglevel_dirty_database_queries', null) ?? 0), 'dirty table reads: ' . $sql, [ 'tables' => array_keys($this->tableDirtyWrites), @@ -339,9 +442,9 @@ class Connection extends PrimaryReadReplicaConnection { * * This method supports PDO binding types as well as DBAL mapping types. * - * @param string $sql The SQL query. - * @param array $params The query parameters. - * @param array $types The parameter types. + * @param string $sql The SQL query. + * @param array $params The query parameters. + * @param array $types The parameter types. * * @return int The number of affected rows. * @@ -426,8 +529,8 @@ class Connection extends PrimaryReadReplicaConnection { * @param string $table The table name (will replace *PREFIX* with the actual prefix) * @param array $input data that should be inserted into the table (column name => value) * @param array|null $compare List of values that should be checked for "if not exists" - * If this is null or an empty array, all keys of $input will be compared - * Please note: text fields (clob) must not be used in the compare array + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array * @return int number of inserted rows * @throws \Doctrine\DBAL\Exception * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371 @@ -609,7 +712,7 @@ class Connection extends PrimaryReadReplicaConnection { $statement = $this->replaceTablePrefix($statement); $statement = $this->adapter->fixupStatement($statement); if ($this->logRequestId) { - return $statement . " /* reqid: " . $this->requestId . " */"; + return $statement . ' /* reqid: ' . $this->requestId . ' */'; } else { return $statement; } @@ -686,6 +789,9 @@ class Connection extends PrimaryReadReplicaConnection { return $migrator->generateChangeScript($toSchema); } else { $migrator->migrate($toSchema); + foreach ($this->getShardConnections() as $shardConnection) { + $shardConnection->migrateToSchema($toSchema); + } } } @@ -719,7 +825,20 @@ class Connection extends PrimaryReadReplicaConnection { $this->transactionBacktrace = null; $this->transactionActiveSince = null; if ($timeTook > 1) { - $this->logger->debug('Transaction took ' . $timeTook . 's', ['exception' => new \Exception('Transaction took ' . $timeTook . 's')]); + $logLevel = match (true) { + $timeTook > 20 * 60 => ILogger::ERROR, + $timeTook > 5 * 60 => ILogger::WARN, + $timeTook > 10 => ILogger::INFO, + default => ILogger::DEBUG, + }; + $this->logger->log( + $logLevel, + 'Transaction took ' . $timeTook . 's', + [ + 'exception' => new \Exception('Transaction took ' . $timeTook . 's'), + 'timeSpent' => $timeTook, + ] + ); } } return $result; @@ -732,7 +851,20 @@ class Connection extends PrimaryReadReplicaConnection { $this->transactionBacktrace = null; $this->transactionActiveSince = null; if ($timeTook > 1) { - $this->logger->debug('Transaction rollback took longer than 1s: ' . $timeTook, ['exception' => new \Exception('Long running transaction rollback')]); + $logLevel = match (true) { + $timeTook > 20 * 60 => ILogger::ERROR, + $timeTook > 5 * 60 => ILogger::WARN, + $timeTook > 10 => ILogger::INFO, + default => ILogger::DEBUG, + }; + $this->logger->log( + $logLevel, + 'Transaction rollback took longer than 1s: ' . $timeTook, + [ + 'exception' => new \Exception('Long running transaction rollback'), + 'timeSpent' => $timeTook, + ] + ); } } return $result; @@ -802,4 +934,12 @@ class Connection extends PrimaryReadReplicaConnection { } } } + + public function getShardDefinition(string $name): ?ShardDefinition { + return $this->shards[$name] ?? null; + } + + public function getCrossShardMoveHelper(): CrossShardMoveHelper { + return new CrossShardMoveHelper($this->shardConnectionManager); + } } diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php index 88083711195..2baeda9cfb7 100644 --- a/lib/private/DB/ConnectionAdapter.php +++ b/lib/private/DB/ConnectionAdapter.php @@ -12,6 +12,8 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Schema\Schema; use OC\DB\Exceptions\DbalException; +use OC\DB\QueryBuilder\Sharded\CrossShardMoveHelper; +use OC\DB\QueryBuilder\Sharded\ShardDefinition; use OCP\DB\IPreparedStatement; use OCP\DB\IResult; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -244,4 +246,12 @@ class ConnectionAdapter implements IDBConnection { public function logDatabaseException(\Exception $exception) { $this->inner->logDatabaseException($exception); } + + public function getShardDefinition(string $name): ?ShardDefinition { + return $this->inner->getShardDefinition($name); + } + + public function getCrossShardMoveHelper(): CrossShardMoveHelper { + return $this->inner->getCrossShardMoveHelper(); + } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index dd041f1e41d..8d662b0508c 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -11,7 +11,11 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Event\Listeners\OracleSessionInit; +use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler; +use OC\DB\QueryBuilder\Sharded\ShardConnectionManager; use OC\SystemConfig; +use OCP\ICacheFactory; +use OCP\Server; /** * Takes care of creating and configuring Doctrine connections. @@ -54,9 +58,12 @@ class ConnectionFactory { ], ]; + private ShardConnectionManager $shardConnectionManager; + private ICacheFactory $cacheFactory; public function __construct( - private SystemConfig $config + private SystemConfig $config, + ?ICacheFactory $cacheFactory = null, ) { if ($this->config->getValue('mysql.utf8mb4', false)) { $this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4'; @@ -65,6 +72,8 @@ class ConnectionFactory { if ($collationOverride) { $this->defaultConnectionParams['mysql']['collation'] = $collationOverride; } + $this->shardConnectionManager = new ShardConnectionManager($this->config, $this); + $this->cacheFactory = $cacheFactory ?? Server::get(ICacheFactory::class); } /** @@ -124,7 +133,7 @@ class ConnectionFactory { if ($host === '') { $connectionParams['dbname'] = $dbName; // use dbname as easy connect name } else { - $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : "") . '/' . $dbName; + $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : '') . '/' . $dbName; } unset($connectionParams['host']); break; @@ -180,7 +189,7 @@ class ConnectionFactory { $name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME)); if ($this->normalizeType($type) === 'sqlite3') { - $dataDir = $this->config->getValue("datadirectory", \OC::$SERVERROOT . '/data'); + $dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data'); $connectionParams['path'] = $dataDir . '/' . $name . '.db'; } else { $host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', '')); @@ -214,6 +223,19 @@ class ConnectionFactory { if ($this->config->getValue('dbpersistent', false)) { $connectionParams['persistent'] = true; } + + $connectionParams['sharding'] = $this->config->getValue('dbsharding', []); + if (!empty($connectionParams['sharding'])) { + $connectionParams['shard_connection_manager'] = $this->shardConnectionManager; + $connectionParams['auto_increment_handler'] = new AutoIncrementHandler( + $this->cacheFactory, + $this->shardConnectionManager, + ); + } else { + // just in case only the presence could lead to funny behaviour + unset($connectionParams['sharding']); + } + $connectionParams = array_merge($connectionParams, $additionalConnectionParams); $replica = $this->config->getValue($configPrefix . 'dbreplica', $this->config->getValue('dbreplica', [])) ?: [$connectionParams]; @@ -237,7 +259,7 @@ class ConnectionFactory { // Host variable carries a port or socket. $params['host'] = $matches[1]; if (is_numeric($matches[2])) { - $params['port'] = (int) $matches[2]; + $params['port'] = (int)$matches[2]; } else { $params['unix_socket'] = $matches[2]; } diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php index 0a3b0d1dcc7..61a6d2baf16 100644 --- a/lib/private/DB/MigrationService.php +++ b/lib/private/DB/MigrationService.php @@ -158,7 +158,7 @@ class MigrationService { /** * Returns all versions which have already been applied * - * @return string[] + * @return list<string> * @codeCoverageIgnore - no need to test this */ public function getMigratedVersions() { @@ -174,6 +174,8 @@ class MigrationService { $rows = $result->fetchAll(\PDO::FETCH_COLUMN); $result->closeCursor(); + usort($rows, $this->sortMigrations(...)); + return $rows; } @@ -183,7 +185,23 @@ class MigrationService { */ public function getAvailableVersions(): array { $this->ensureMigrationsAreLoaded(); - return array_map('strval', array_keys($this->migrations)); + $versions = array_map('strval', array_keys($this->migrations)); + usort($versions, $this->sortMigrations(...)); + return $versions; + } + + protected function sortMigrations(string $a, string $b): int { + preg_match('/(\d+)Date(\d+)/', basename($a), $matchA); + preg_match('/(\d+)Date(\d+)/', basename($b), $matchB); + if (!empty($matchA) && !empty($matchB)) { + $versionA = (int)$matchA[1]; + $versionB = (int)$matchB[1]; + if ($versionA !== $versionB) { + return ($versionA < $versionB) ? -1 : 1; + } + return ($matchA[2] < $matchB[2]) ? -1 : 1; + } + return (basename($a) < basename($b)) ? -1 : 1; } /** @@ -204,23 +222,13 @@ class MigrationService { \RegexIterator::GET_MATCH); $files = array_keys(iterator_to_array($iterator)); - uasort($files, function ($a, $b) { - preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA); - preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB); - if (!empty($matchA) && !empty($matchB)) { - if ($matchA[1] !== $matchB[1]) { - return ($matchA[1] < $matchB[1]) ? -1 : 1; - } - return ($matchA[2] < $matchB[2]) ? -1 : 1; - } - return (basename($a) < basename($b)) ? -1 : 1; - }); + usort($files, $this->sortMigrations(...)); $migrations = []; foreach ($files as $file) { $className = basename($file, '.php'); - $version = (string) substr($className, 7); + $version = (string)substr($className, 7); if ($version === '0') { throw new \InvalidArgumentException( "Cannot load a migrations with the name '$version' because it is a reserved number" diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php index 4fd457b4bc6..40f8ad9676a 100644 --- a/lib/private/DB/Migrator.php +++ b/lib/private/DB/Migrator.php @@ -139,7 +139,7 @@ class Migrator { $step = 0; foreach ($sqls as $sql) { $this->emit($sql, $step++, count($sqls)); - $connection->executeQuery($sql); + $connection->executeStatement($sql); } if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) { $connection->commit(); diff --git a/lib/private/DB/PreparedStatement.php b/lib/private/DB/PreparedStatement.php index 5fdfa2b03e8..54561ed96cd 100644 --- a/lib/private/DB/PreparedStatement.php +++ b/lib/private/DB/PreparedStatement.php @@ -78,6 +78,6 @@ class PreparedStatement implements IPreparedStatement { return $this->result; } - throw new Exception("You have to execute the prepared statement before accessing the results"); + throw new Exception('You have to execute the prepared statement before accessing the results'); } } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php index b70e20e4d0d..b922c861630 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php @@ -57,7 +57,7 @@ class ExpressionBuilder implements IExpressionBuilder { * $expr->andX('u.type = ?', 'u.role = ?')); * * @param mixed ...$x Optional clause. Defaults = null, but requires - * at least one defined when converting to string. + * at least one defined when converting to string. * * @return \OCP\DB\QueryBuilder\ICompositeExpression */ @@ -78,7 +78,7 @@ class ExpressionBuilder implements IExpressionBuilder { * $qb->where($qb->expr()->orX('u.type = ?', 'u.role = ?')); * * @param mixed ...$x Optional clause. Defaults = null, but requires - * at least one defined when converting to string. + * at least one defined when converting to string. * * @return \OCP\DB\QueryBuilder\ICompositeExpression */ @@ -96,7 +96,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param string $operator One of the IExpressionBuilder::* constants. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -119,7 +119,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -141,7 +141,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -163,7 +163,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -185,7 +185,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -207,7 +207,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -229,7 +229,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed $x The left expression. * @param mixed $y The right expression. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -269,7 +269,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param ILiteral|IParameter|IQueryFunction|string $x Field in string format to be inspected by LIKE() comparison. * @param mixed $y Argument to be used in LIKE() comparison. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -285,7 +285,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param string $x Field in string format to be inspected by ILIKE() comparison. * @param mixed $y Argument to be used in ILIKE() comparison. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string * @since 9.0.0 @@ -300,7 +300,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param ILiteral|IParameter|IQueryFunction|string $x Field in string format to be inspected by NOT LIKE() comparison. * @param mixed $y Argument to be used in NOT LIKE() comparison. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -316,7 +316,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by IN() comparison. * @param ILiteral|IParameter|IQueryFunction|string|array $y The placeholder or the array of values to be used by IN() comparison. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ @@ -332,7 +332,7 @@ class ExpressionBuilder implements IExpressionBuilder { * @param ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by NOT IN() comparison. * @param ILiteral|IParameter|IQueryFunction|string|array $y The placeholder or the array of values to be used by NOT IN() comparison. * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants - * required when comparing text fields for oci compatibility + * required when comparing text fields for oci compatibility * * @return string */ diff --git a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php index bde6523567f..c40cadfbdb5 100644 --- a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php +++ b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php @@ -288,4 +288,22 @@ abstract class ExtendedQueryBuilder implements IQueryBuilder { public function executeStatement(?IDBConnection $connection = null): int { return $this->builder->executeStatement($connection); } + + public function hintShardKey(string $column, mixed $value, bool $overwrite = false) { + $this->builder->hintShardKey($column, $value, $overwrite); + return $this; + } + + public function runAcrossAllShards() { + $this->builder->runAcrossAllShards(); + return $this; + } + + public function getOutputColumns(): array { + return $this->builder->getOutputColumns(); + } + + public function prefixTableName(string $table): string { + return $this->builder->prefixTableName($table); + } } diff --git a/lib/private/DB/QueryBuilder/Literal.php b/lib/private/DB/QueryBuilder/Literal.php index f0accce1d93..3fb897328e5 100644 --- a/lib/private/DB/QueryBuilder/Literal.php +++ b/lib/private/DB/QueryBuilder/Literal.php @@ -18,6 +18,6 @@ class Literal implements ILiteral { } public function __toString(): string { - return (string) $this->literal; + return (string)$this->literal; } } diff --git a/lib/private/DB/QueryBuilder/Parameter.php b/lib/private/DB/QueryBuilder/Parameter.php index dbd723639fc..a272c744d62 100644 --- a/lib/private/DB/QueryBuilder/Parameter.php +++ b/lib/private/DB/QueryBuilder/Parameter.php @@ -18,6 +18,6 @@ class Parameter implements IParameter { } public function __toString(): string { - return (string) $this->name; + return (string)$this->name; } } diff --git a/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php b/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php new file mode 100644 index 00000000000..3a5aa2f3e0e --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +/** + * Partitioned queries impose limitations that queries have to follow: + * + * 1. Any reference to columns not in the "main table" (the table referenced by "FROM"), needs to explicitly include the + * table or alias the column belongs to. + * + * For example: + * ``` + * $query->select("mount_point", "mimetype") + * ->from("mounts", "m") + * ->innerJoin("m", "filecache", "f", $query->expr()->eq("root_id", "fileid")); + * ``` + * will not work, as the query builder doesn't know that the `mimetype` column belongs to the "filecache partition". + * Instead, you need to do + * ``` + * $query->select("mount_point", "f.mimetype") + * ->from("mounts", "m") + * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid")); + * ``` + * + * 2. The "ON" condition for the join can only perform a comparison between both sides of the join once. + * + * For example: + * ``` + * $query->select("mount_point", "mimetype") + * ->from("mounts", "m") + * ->innerJoin("m", "filecache", "f", $query->expr()->andX($query->expr()->eq("m.root_id", "f.fileid"), $query->expr()->eq("m.storage_id", "f.storage"))); + * ``` + * will not work. + * + * 3. An "OR" expression in the "WHERE" cannot mention both sides of the join, this does not apply to "AND" expressions. + * + * For example: + * ``` + * $query->select("mount_point", "mimetype") + * ->from("mounts", "m") + * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid"))) + * ->where($query->expr()->orX( + * $query->expr()-eq("m.user_id", $query->createNamedParameter("test"))), + * $query->expr()-eq("f.name", $query->createNamedParameter("test"))), + * )); + * ``` + * will not work, but. + * ``` + * $query->select("mount_point", "mimetype") + * ->from("mounts", "m") + * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid"))) + * ->where($query->expr()->andX( + * $query->expr()-eq("m.user_id", $query->createNamedParameter("test"))), + * $query->expr()-eq("f.name", $query->createNamedParameter("test"))), + * )); + * ``` + * will. + * + * 4. Queries that join cross-partition cannot use position parameters, only named parameters are allowed + * 5. The "ON" condition of a join cannot contain and "OR" expression. + * 6. Right-joins are not allowed. + * 7. Update, delete and insert statements aren't allowed to contain cross-partition joins. + * 8. Queries that "GROUP BY" a column from the joined partition are not allowed. + * 9. Any `join` call needs to be made before any `where` call. + * 10. Queries that join cross-partition with an "INNER JOIN" or "LEFT JOIN" with a condition on the left side + * cannot use "LIMIT" or "OFFSET" in queries. + * + * The part of the query running on the sharded table has some additional limitations, + * see the `InvalidShardedQueryException` documentation for more information. + */ +class InvalidPartitionedQueryException extends \Exception { + +} diff --git a/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php b/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php new file mode 100644 index 00000000000..a08858d1d6b --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +use OC\DB\QueryBuilder\CompositeExpression; +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\IQueryFunction; + +/** + * Utility class for working with join conditions + */ +class JoinCondition { + public function __construct( + public string|IQueryFunction $fromColumn, + public ?string $fromAlias, + public string|IQueryFunction $toColumn, + public ?string $toAlias, + public array $fromConditions, + public array $toConditions, + ) { + if (is_string($this->fromColumn) && str_starts_with($this->fromColumn, '(')) { + $this->fromColumn = new QueryFunction($this->fromColumn); + } + if (is_string($this->toColumn) && str_starts_with($this->toColumn, '(')) { + $this->toColumn = new QueryFunction($this->toColumn); + } + } + + /** + * @param JoinCondition[] $conditions + * @return JoinCondition + */ + public static function merge(array $conditions): JoinCondition { + $fromColumn = ''; + $toColumn = ''; + $fromAlias = null; + $toAlias = null; + $fromConditions = []; + $toConditions = []; + foreach ($conditions as $condition) { + if (($condition->fromColumn && $fromColumn) || ($condition->toColumn && $toColumn)) { + throw new InvalidPartitionedQueryException("Can't join from {$condition->fromColumn} to {$condition->toColumn} as it already join froms {$fromColumn} to {$toColumn}"); + } + if ($condition->fromColumn) { + $fromColumn = $condition->fromColumn; + } + if ($condition->toColumn) { + $toColumn = $condition->toColumn; + } + if ($condition->fromAlias) { + $fromAlias = $condition->fromAlias; + } + if ($condition->toAlias) { + $toAlias = $condition->toAlias; + } + $fromConditions = array_merge($fromConditions, $condition->fromConditions); + $toConditions = array_merge($toConditions, $condition->toConditions); + } + return new JoinCondition($fromColumn, $fromAlias, $toColumn, $toAlias, $fromConditions, $toConditions); + } + + /** + * @param null|string|CompositeExpression $condition + * @param string $join + * @param string $alias + * @param string $fromAlias + * @return JoinCondition + * @throws InvalidPartitionedQueryException + */ + public static function parse($condition, string $join, string $alias, string $fromAlias): JoinCondition { + if ($condition === null) { + throw new InvalidPartitionedQueryException("Can't join on $join without a condition"); + } + + $result = self::parseSubCondition($condition, $join, $alias, $fromAlias); + if (!$result->fromColumn || !$result->toColumn) { + throw new InvalidPartitionedQueryException("No join condition found from $fromAlias to $alias"); + } + return $result; + } + + private static function parseSubCondition($condition, string $join, string $alias, string $fromAlias): JoinCondition { + if ($condition instanceof CompositeExpression) { + if ($condition->getType() === CompositeExpression::TYPE_OR) { + throw new InvalidPartitionedQueryException("Cannot join on $join with an OR expression"); + } + return self::merge(array_map(function ($subCondition) use ($join, $alias, $fromAlias) { + return self::parseSubCondition($subCondition, $join, $alias, $fromAlias); + }, $condition->getParts())); + } + + $condition = (string)$condition; + $isSubCondition = self::isExtraCondition($condition); + if ($isSubCondition) { + if (self::mentionsAlias($condition, $fromAlias)) { + return new JoinCondition('', null, '', null, [$condition], []); + } else { + return new JoinCondition('', null, '', null, [], [$condition]); + } + } + + $condition = str_replace('`', '', $condition); + + // expect a condition in the form of 'alias1.column1 = alias2.column2' + if (!str_contains($condition, ' = ')) { + throw new InvalidPartitionedQueryException("Can only join on $join with an `eq` condition"); + } + $parts = explode(' = ', $condition, 2); + $parts = array_map(function (string $part) { + return self::clearConditionPart($part); + }, $parts); + + if (!self::isSingleCondition($parts[0]) || !self::isSingleCondition($parts[1])) { + throw new InvalidPartitionedQueryException("Can only join on $join with a single condition"); + } + + + if (self::mentionsAlias($parts[0], $fromAlias)) { + return new JoinCondition($parts[0], self::getAliasForPart($parts[0]), $parts[1], self::getAliasForPart($parts[1]), [], []); + } elseif (self::mentionsAlias($parts[1], $fromAlias)) { + return new JoinCondition($parts[1], self::getAliasForPart($parts[1]), $parts[0], self::getAliasForPart($parts[0]), [], []); + } else { + throw new InvalidPartitionedQueryException("join condition for $join needs to explicitly refer to the table by alias"); + } + } + + private static function isSingleCondition(string $condition): bool { + return !(str_contains($condition, ' OR ') || str_contains($condition, ' AND ')); + } + + private static function getAliasForPart(string $part): ?string { + if (str_contains($part, ' ')) { + return uniqid('join_alias_'); + } else { + return null; + } + } + + private static function clearConditionPart(string $part): string { + if (str_starts_with($part, 'CAST(')) { + // pgsql/mysql cast + $part = substr($part, strlen('CAST(')); + [$part] = explode(' AS ', $part); + } elseif (str_starts_with($part, 'to_number(to_char(')) { + // oracle cast to int + $part = substr($part, strlen('to_number(to_char('), -2); + } elseif (str_starts_with($part, 'to_number(to_char(')) { + // oracle cast to string + $part = substr($part, strlen('to_char('), -1); + } + return $part; + } + + /** + * Check that a condition is an extra limit on the from/to part, and not the join condition + * + * This is done by checking that only one of the halves of the condition references a column + */ + private static function isExtraCondition(string $condition): bool { + $parts = explode(' ', $condition, 2); + return str_contains($parts[0], '`') xor str_contains($parts[1], '`'); + } + + private static function mentionsAlias(string $condition, string $alias): bool { + return str_contains($condition, "$alias."); + } +} diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php new file mode 100644 index 00000000000..a5024b478d3 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +use OCP\DB\QueryBuilder\IQueryBuilder; + +/** + * A sub-query from a partitioned join + */ +class PartitionQuery { + public const JOIN_MODE_INNER = 'inner'; + public const JOIN_MODE_LEFT = 'left'; + // left-join where the left side IS NULL + public const JOIN_MODE_LEFT_NULL = 'left_null'; + + public const JOIN_MODE_RIGHT = 'right'; + + public function __construct( + public IQueryBuilder $query, + public string $joinFromColumn, + public string $joinToColumn, + public string $joinMode, + ) { + if ($joinMode !== self::JOIN_MODE_LEFT && $joinMode !== self::JOIN_MODE_INNER) { + throw new InvalidPartitionedQueryException("$joinMode joins aren't allowed in partitioned queries"); + } + } + + public function mergeWith(array $rows): array { + if (empty($rows)) { + return []; + } + // strip table/alias from column names + $joinFromColumn = preg_replace('/\w+\./', '', $this->joinFromColumn); + $joinToColumn = preg_replace('/\w+\./', '', $this->joinToColumn); + + $joinFromValues = array_map(function (array $row) use ($joinFromColumn) { + return $row[$joinFromColumn]; + }, $rows); + $joinFromValues = array_filter($joinFromValues, function ($value) { + return $value !== null; + }); + $this->query->andWhere($this->query->expr()->in($this->joinToColumn, $this->query->createNamedParameter($joinFromValues, IQueryBuilder::PARAM_STR_ARRAY, ':' . uniqid()))); + + $s = $this->query->getSQL(); + $partitionedRows = $this->query->executeQuery()->fetchAll(); + + $columns = $this->query->getOutputColumns(); + $nullResult = array_combine($columns, array_fill(0, count($columns), null)); + + $partitionedRowsByKey = []; + foreach ($partitionedRows as $partitionedRow) { + $partitionedRowsByKey[$partitionedRow[$joinToColumn]][] = $partitionedRow; + } + $result = []; + foreach ($rows as $row) { + if (isset($partitionedRowsByKey[$row[$joinFromColumn]])) { + if ($this->joinMode !== self::JOIN_MODE_LEFT_NULL) { + foreach ($partitionedRowsByKey[$row[$joinFromColumn]] as $partitionedRow) { + $result[] = array_merge($row, $partitionedRow); + } + } + } elseif ($this->joinMode === self::JOIN_MODE_LEFT || $this->joinMode === self::JOIN_MODE_LEFT_NULL) { + $result[] = array_merge($nullResult, $row); + } + } + return $result; + } +} diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php new file mode 100644 index 00000000000..ad4c0fab055 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +/** + * Information about a database partition, containing the tables in the partition and any active alias + */ +class PartitionSplit { + /** @var array<string, string> */ + public array $aliases = []; + + /** + * @param string[] $tables + */ + public function __construct( + public string $name, + public array $tables, + ) { + } + + public function addAlias(string $table, string $alias): void { + if ($this->containsTable($table)) { + $this->aliases[$alias] = $table; + } + } + + public function addTable(string $table): void { + if (!$this->containsTable($table)) { + $this->tables[] = $table; + } + } + + public function containsTable(string $table): bool { + return in_array($table, $this->tables); + } + + public function containsAlias(string $alias): bool { + return array_key_exists($alias, $this->aliases); + } + + private function getTablesAndAliases(): array { + return array_keys($this->aliases) + $this->tables; + } + + /** + * Check if a query predicate mentions a table or alias from this partition + * + * @param string $predicate + * @return bool + */ + public function checkPredicateForTable(string $predicate): bool { + foreach ($this->getTablesAndAliases() as $name) { + if (str_contains($predicate, "`$name`.`")) { + return true; + } + } + return false; + } + + public function isColumnInPartition(string $column): bool { + foreach ($this->getTablesAndAliases() as $name) { + if (str_starts_with($column, "$name.")) { + return true; + } + } + return false; + } +} diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php new file mode 100644 index 00000000000..175b7c1a42e --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php @@ -0,0 +1,426 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +use OC\DB\QueryBuilder\CompositeExpression; +use OC\DB\QueryBuilder\QuoteHelper; +use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler; +use OC\DB\QueryBuilder\Sharded\ShardConnectionManager; +use OC\DB\QueryBuilder\Sharded\ShardedQueryBuilder; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\IQueryFunction; +use OCP\IDBConnection; + +/** + * A special query builder that automatically splits queries that span across multiple database partitions[1]. + * + * This is done by inspecting the query as it's being built, and when a cross-partition join is detected, + * the part of the query that touches the partition is split of into a different sub-query. + * Then, when the query is executed, the results from the sub-queries are automatically merged. + * + * This whole process is intended to be transparent to any code using the query builder, however it does impose some extra + * limitation for queries that work cross-partition. See the documentation from `InvalidPartitionedQueryException` for more details. + * + * When a join is created in the query, this builder checks if it belongs to the same partition as the table from the + * original FROM/UPDATE/DELETE/INSERT and if not, creates a new "sub query" for the partition. + * Then for every part that is added the query, the part is analyzed to determine which partition the query part is referencing + * and the query part is added to the sub query for that partition. + * + * [1]: A set of tables which can't be queried together with the rest of the tables, such as when sharding is used. + */ +class PartitionedQueryBuilder extends ShardedQueryBuilder { + /** @var array<string, PartitionQuery> $splitQueries */ + private array $splitQueries = []; + /** @var list<PartitionSplit> */ + private array $partitions = []; + + /** @var array{'select': string|array, 'alias': ?string}[] */ + private array $selects = []; + private ?PartitionSplit $mainPartition = null; + private bool $hasPositionalParameter = false; + private QuoteHelper $quoteHelper; + private ?int $limit = null; + private ?int $offset = null; + + public function __construct( + IQueryBuilder $builder, + array $shardDefinitions, + ShardConnectionManager $shardConnectionManager, + AutoIncrementHandler $autoIncrementHandler, + ) { + parent::__construct($builder, $shardDefinitions, $shardConnectionManager, $autoIncrementHandler); + $this->quoteHelper = new QuoteHelper(); + } + + private function newQuery(): IQueryBuilder { + // get a fresh, non-partitioning query builder + $builder = $this->builder->getConnection()->getQueryBuilder(); + if ($builder instanceof PartitionedQueryBuilder) { + $builder = $builder->builder; + } + + return new ShardedQueryBuilder( + $builder, + $this->shardDefinitions, + $this->shardConnectionManager, + $this->autoIncrementHandler, + ); + } + + // we need to save selects until we know all the table aliases + public function select(...$selects) { + $this->selects = []; + $this->addSelect(...$selects); + return $this; + } + + public function addSelect(...$select) { + $select = array_map(function ($select) { + return ['select' => $select, 'alias' => null]; + }, $select); + $this->selects = array_merge($this->selects, $select); + return $this; + } + + public function selectAlias($select, $alias) { + $this->selects[] = ['select' => $select, 'alias' => $alias]; + return $this; + } + + /** + * Ensure that a column is being selected by the query + * + * This is mainly used to ensure that the returned rows from both sides of a partition contains the columns of the join predicate + * + * @param string $column + * @return void + */ + private function ensureSelect(string|IQueryFunction $column, ?string $alias = null): void { + $checkColumn = $alias ?: $column; + if (str_contains($checkColumn, '.')) { + [, $checkColumn] = explode('.', $checkColumn); + } + foreach ($this->selects as $select) { + if ($select['select'] === $checkColumn || $select['select'] === '*' || str_ends_with($select['select'], '.' . $checkColumn)) { + return; + } + } + if ($alias) { + $this->selectAlias($column, $alias); + } else { + $this->addSelect($column); + } + } + + /** + * Distribute the select statements to the correct partition + * + * This is done at the end instead of when the `select` call is made, because the `select` calls are generally done + * before we know what tables are involved in the query + * + * @return void + */ + private function applySelects(): void { + foreach ($this->selects as $select) { + foreach ($this->partitions as $partition) { + if (is_string($select['select']) && ( + $select['select'] === '*' || + $partition->isColumnInPartition($select['select'])) + ) { + if (isset($this->splitQueries[$partition->name])) { + if ($select['alias']) { + $this->splitQueries[$partition->name]->query->selectAlias($select['select'], $select['alias']); + } else { + $this->splitQueries[$partition->name]->query->addSelect($select['select']); + } + if ($select['select'] !== '*') { + continue 2; + } + } + } + } + + if ($select['alias']) { + parent::selectAlias($select['select'], $select['alias']); + } else { + parent::addSelect($select['select']); + } + } + $this->selects = []; + } + + + public function addPartition(PartitionSplit $partition): void { + $this->partitions[] = $partition; + } + + private function getPartition(string $table): ?PartitionSplit { + foreach ($this->partitions as $partition) { + if ($partition->containsTable($table) || $partition->containsAlias($table)) { + return $partition; + } + } + return null; + } + + public function from($from, $alias = null) { + if (is_string($from) && $partition = $this->getPartition($from)) { + $this->mainPartition = $partition; + if ($alias) { + $this->mainPartition->addAlias($from, $alias); + } + } + return parent::from($from, $alias); + } + + public function innerJoin($fromAlias, $join, $alias, $condition = null): self { + return $this->join($fromAlias, $join, $alias, $condition); + } + + public function leftJoin($fromAlias, $join, $alias, $condition = null): self { + return $this->join($fromAlias, $join, $alias, $condition, PartitionQuery::JOIN_MODE_LEFT); + } + + public function join($fromAlias, $join, $alias, $condition = null, $joinMode = PartitionQuery::JOIN_MODE_INNER): self { + $partition = $this->getPartition($join); + $fromPartition = $this->getPartition($fromAlias); + if ($partition && $partition !== $this->mainPartition) { + // join from the main db to a partition + + $joinCondition = JoinCondition::parse($condition, $join, $alias, $fromAlias); + $partition->addAlias($join, $alias); + + if (!isset($this->splitQueries[$partition->name])) { + $this->splitQueries[$partition->name] = new PartitionQuery( + $this->newQuery(), + $joinCondition->fromAlias ?? $joinCondition->fromColumn, $joinCondition->toAlias ?? $joinCondition->toColumn, + $joinMode + ); + $this->splitQueries[$partition->name]->query->from($join, $alias); + $this->ensureSelect($joinCondition->fromColumn, $joinCondition->fromAlias); + $this->ensureSelect($joinCondition->toColumn, $joinCondition->toAlias); + } else { + $query = $this->splitQueries[$partition->name]->query; + if ($partition->containsAlias($fromAlias)) { + $query->innerJoin($fromAlias, $join, $alias, $condition); + } else { + throw new InvalidPartitionedQueryException("Can't join across partition boundaries more than once"); + } + } + $this->splitQueries[$partition->name]->query->andWhere(...$joinCondition->toConditions); + parent::andWhere(...$joinCondition->fromConditions); + return $this; + } elseif ($fromPartition && $fromPartition !== $partition) { + // join from partition, to the main db + + $joinCondition = JoinCondition::parse($condition, $join, $alias, $fromAlias); + if (str_starts_with($fromPartition->name, 'from_')) { + $partitionName = $fromPartition->name; + } else { + $partitionName = 'from_' . $fromPartition->name; + } + + if (!isset($this->splitQueries[$partitionName])) { + $newPartition = new PartitionSplit($partitionName, [$join]); + $newPartition->addAlias($join, $alias); + $this->partitions[] = $newPartition; + + $this->splitQueries[$partitionName] = new PartitionQuery( + $this->newQuery(), + $joinCondition->fromAlias ?? $joinCondition->fromColumn, $joinCondition->toAlias ?? $joinCondition->toColumn, + $joinMode + ); + $this->ensureSelect($joinCondition->fromColumn, $joinCondition->fromAlias); + $this->ensureSelect($joinCondition->toColumn, $joinCondition->toAlias); + $this->splitQueries[$partitionName]->query->from($join, $alias); + $this->splitQueries[$partitionName]->query->andWhere(...$joinCondition->toConditions); + parent::andWhere(...$joinCondition->fromConditions); + } else { + $fromPartition->addTable($join); + $fromPartition->addAlias($join, $alias); + + $query = $this->splitQueries[$partitionName]->query; + $query->innerJoin($fromAlias, $join, $alias, $condition); + } + return $this; + } else { + // join within the main db or a partition + if ($joinMode === PartitionQuery::JOIN_MODE_INNER) { + return parent::innerJoin($fromAlias, $join, $alias, $condition); + } elseif ($joinMode === PartitionQuery::JOIN_MODE_LEFT) { + return parent::leftJoin($fromAlias, $join, $alias, $condition); + } elseif ($joinMode === PartitionQuery::JOIN_MODE_RIGHT) { + return parent::rightJoin($fromAlias, $join, $alias, $condition); + } else { + throw new \InvalidArgumentException("Invalid join mode: $joinMode"); + } + } + } + + /** + * Flatten a list of predicates by merging the parts of any "AND" expression into the list of predicates + * + * @param array $predicates + * @return array + */ + private function flattenPredicates(array $predicates): array { + $result = []; + foreach ($predicates as $predicate) { + if ($predicate instanceof CompositeExpression && $predicate->getType() === CompositeExpression::TYPE_AND) { + $result = array_merge($result, $this->flattenPredicates($predicate->getParts())); + } else { + $result[] = $predicate; + } + } + return $result; + } + + /** + * Split an array of predicates (WHERE query parts) by the partition they reference + * @param array $predicates + * @return array<string, array> + */ + private function splitPredicatesByParts(array $predicates): array { + $predicates = $this->flattenPredicates($predicates); + + $partitionPredicates = []; + foreach ($predicates as $predicate) { + $partition = $this->getPartitionForPredicate((string)$predicate); + if ($this->mainPartition === $partition) { + $partitionPredicates[''][] = $predicate; + } elseif ($partition) { + $partitionPredicates[$partition->name][] = $predicate; + } else { + $partitionPredicates[''][] = $predicate; + } + } + return $partitionPredicates; + } + + public function where(...$predicates) { + return $this->andWhere(...$predicates); + } + + public function andWhere(...$where) { + if ($where) { + foreach ($this->splitPredicatesByParts($where) as $alias => $predicates) { + if (isset($this->splitQueries[$alias])) { + // when there is a condition on a table being left-joined it starts to behave as if it's an inner join + // since any joined column that doesn't have the left part will not match the condition + // when there the condition is `$joinToColumn IS NULL` we instead mark the query as excluding the left half + if ($this->splitQueries[$alias]->joinMode === PartitionQuery::JOIN_MODE_LEFT) { + $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_INNER; + + $column = $this->quoteHelper->quoteColumnName($this->splitQueries[$alias]->joinToColumn); + foreach ($predicates as $predicate) { + if ((string)$predicate === "$column IS NULL") { + $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_LEFT_NULL; + } else { + $this->splitQueries[$alias]->query->andWhere($predicate); + } + } + } else { + $this->splitQueries[$alias]->query->andWhere(...$predicates); + } + } else { + parent::andWhere(...$predicates); + } + } + } + return $this; + } + + + private function getPartitionForPredicate(string $predicate): ?PartitionSplit { + foreach ($this->partitions as $partition) { + + if (str_contains($predicate, '?')) { + $this->hasPositionalParameter = true; + } + if ($partition->checkPredicateForTable($predicate)) { + return $partition; + } + } + return null; + } + + public function update($update = null, $alias = null) { + return parent::update($update, $alias); + } + + public function insert($insert = null) { + return parent::insert($insert); + } + + public function delete($delete = null, $alias = null) { + return parent::delete($delete, $alias); + } + + public function setMaxResults($maxResults) { + if ($maxResults > 0) { + $this->limit = (int)$maxResults; + } + return parent::setMaxResults($maxResults); + } + + public function setFirstResult($firstResult) { + if ($firstResult > 0) { + $this->offset = (int)$firstResult; + } + return parent::setFirstResult($firstResult); + } + + public function executeQuery(?IDBConnection $connection = null): IResult { + $this->applySelects(); + if ($this->splitQueries && $this->hasPositionalParameter) { + throw new InvalidPartitionedQueryException("Partitioned queries aren't allowed to to positional arguments"); + } + foreach ($this->splitQueries as $split) { + $split->query->setParameters($this->getParameters(), $this->getParameterTypes()); + } + if (count($this->splitQueries) > 0) { + $hasNonLeftJoins = array_reduce($this->splitQueries, function (bool $hasNonLeftJoins, PartitionQuery $query) { + return $hasNonLeftJoins || $query->joinMode !== PartitionQuery::JOIN_MODE_LEFT; + }, false); + if ($hasNonLeftJoins) { + if (is_int($this->limit)) { + throw new InvalidPartitionedQueryException('Limit is not allowed in partitioned queries'); + } + if (is_int($this->offset)) { + throw new InvalidPartitionedQueryException('Offset is not allowed in partitioned queries'); + } + } + } + + $s = $this->getSQL(); + $result = parent::executeQuery($connection); + if (count($this->splitQueries) > 0) { + return new PartitionedResult($this->splitQueries, $result); + } else { + return $result; + } + } + + public function executeStatement(?IDBConnection $connection = null): int { + if (count($this->splitQueries)) { + throw new InvalidPartitionedQueryException("Partitioning write queries isn't supported"); + } + return parent::executeStatement($connection); + } + + public function getSQL() { + $this->applySelects(); + return parent::getSQL(); + } + + public function getPartitionCount(): int { + return count($this->splitQueries) + 1; + } +} diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php new file mode 100644 index 00000000000..aa9cc43b38b --- /dev/null +++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB\QueryBuilder\Partitioned; + +use OC\DB\ArrayResult; +use OCP\DB\IResult; +use PDO; + +/** + * Combine the results of multiple join parts into a single result + */ +class PartitionedResult extends ArrayResult { + private bool $fetched = false; + + /** + * @param PartitionQuery[] $splitOfParts + * @param IResult $result + */ + public function __construct( + private array $splitOfParts, + private IResult $result + ) { + parent::__construct([]); + } + + public function closeCursor(): bool { + return $this->result->closeCursor(); + } + + public function fetch(int $fetchMode = PDO::FETCH_ASSOC) { + $this->fetchRows(); + return parent::fetch($fetchMode); + } + + public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array { + $this->fetchRows(); + return parent::fetchAll($fetchMode); + } + + public function rowCount(): int { + $this->fetchRows(); + return parent::rowCount(); + } + + private function fetchRows(): void { + if (!$this->fetched) { + $this->fetched = true; + $this->rows = $this->result->fetchAll(); + foreach ($this->splitOfParts as $part) { + $this->rows = $part->mergeWith($this->rows); + } + $this->count = count($this->rows); + } + } +} diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 4c4786f02b6..5c7e273c9ec 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -49,6 +49,7 @@ class QueryBuilder implements IQueryBuilder { /** @var string */ protected $lastInsertedTable; + private array $selectedColumns = []; /** * Initializes a new QueryBuilder. @@ -68,11 +69,11 @@ class QueryBuilder implements IQueryBuilder { * Enable/disable automatic prefixing of table names with the oc_ prefix * * @param bool $enabled If set to true table names will be prefixed with the - * owncloud database prefix automatically. + * owncloud database prefix automatically. * @since 8.2.0 */ public function automaticTablePrefix($enabled) { - $this->automaticTablePrefix = (bool) $enabled; + $this->automaticTablePrefix = (bool)$enabled; } /** @@ -405,7 +406,7 @@ class QueryBuilder implements IQueryBuilder { * @return $this This QueryBuilder instance. */ public function setFirstResult($firstResult) { - $this->queryBuilder->setFirstResult((int) $firstResult); + $this->queryBuilder->setFirstResult((int)$firstResult); return $this; } @@ -435,7 +436,7 @@ class QueryBuilder implements IQueryBuilder { if ($maxResults === null) { $this->queryBuilder->setMaxResults($maxResults); } else { - $this->queryBuilder->setMaxResults((int) $maxResults); + $this->queryBuilder->setMaxResults((int)$maxResults); } return $this; @@ -470,6 +471,7 @@ class QueryBuilder implements IQueryBuilder { if (count($selects) === 1 && is_array($selects[0])) { $selects = $selects[0]; } + $this->addOutputColumns($selects); $this->queryBuilder->select( $this->helper->quoteColumnNames($selects) @@ -497,6 +499,7 @@ class QueryBuilder implements IQueryBuilder { $this->queryBuilder->addSelect( $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias) ); + $this->addOutputColumns([$alias]); return $this; } @@ -518,6 +521,7 @@ class QueryBuilder implements IQueryBuilder { if (!is_array($select)) { $select = [$select]; } + $this->addOutputColumns($select); $quotedSelect = $this->helper->quoteColumnNames($select); @@ -547,6 +551,7 @@ class QueryBuilder implements IQueryBuilder { if (count($selects) === 1 && is_array($selects[0])) { $selects = $selects[0]; } + $this->addOutputColumns($selects); $this->queryBuilder->addSelect( $this->helper->quoteColumnNames($selects) @@ -555,6 +560,30 @@ class QueryBuilder implements IQueryBuilder { return $this; } + private function addOutputColumns(array $columns) { + foreach ($columns as $column) { + if (is_array($column)) { + $this->addOutputColumns($column); + } elseif (is_string($column) && !str_contains($column, '*')) { + if (str_contains($column, '.')) { + [, $column] = explode('.', $column); + } + $this->selectedColumns[] = $column; + } + } + } + + public function getOutputColumns(): array { + return array_unique(array_map(function (string $column) { + if (str_contains($column, '.')) { + [, $column] = explode('.', $column); + return $column; + } else { + return $column; + } + }, $this->selectedColumns)); + } + /** * Turns the query being built into a bulk delete query that ranges over * a certain table. @@ -978,7 +1007,7 @@ class QueryBuilder implements IQueryBuilder { public function setValue($column, $value) { $this->queryBuilder->setValue( $this->helper->quoteColumnName($column), - (string) $value + (string)$value ); return $this; @@ -1287,7 +1316,7 @@ class QueryBuilder implements IQueryBuilder { */ public function getTableName($table) { if ($table instanceof IQueryFunction) { - return (string) $table; + return (string)$table; } $table = $this->prefixTableName($table); @@ -1300,7 +1329,7 @@ class QueryBuilder implements IQueryBuilder { * @param string $table * @return string */ - protected function prefixTableName($table) { + public function prefixTableName(string $table): string { if ($this->automaticTablePrefix === false || str_starts_with($table, '*PREFIX*')) { return $table; } @@ -1336,4 +1365,18 @@ class QueryBuilder implements IQueryBuilder { return $this->helper->quoteColumnName($alias); } + + public function escapeLikeParameter(string $parameter): string { + return $this->connection->escapeLikeParameter($parameter); + } + + public function hintShardKey(string $column, mixed $value, bool $overwrite = false) { + return $this; + } + + public function runAcrossAllShards() { + // noop + return $this; + } + } diff --git a/lib/private/DB/QueryBuilder/QueryFunction.php b/lib/private/DB/QueryBuilder/QueryFunction.php index 9cdd6c31c7b..7f2ab584a73 100644 --- a/lib/private/DB/QueryBuilder/QueryFunction.php +++ b/lib/private/DB/QueryBuilder/QueryFunction.php @@ -18,6 +18,6 @@ class QueryFunction implements IQueryFunction { } public function __toString(): string { - return (string) $this->function; + return (string)$this->function; } } diff --git a/lib/private/DB/QueryBuilder/QuoteHelper.php b/lib/private/DB/QueryBuilder/QuoteHelper.php index a60a9731aa2..7ce4b359638 100644 --- a/lib/private/DB/QueryBuilder/QuoteHelper.php +++ b/lib/private/DB/QueryBuilder/QuoteHelper.php @@ -35,7 +35,7 @@ class QuoteHelper { */ public function quoteColumnName($string) { if ($string instanceof IParameter || $string instanceof ILiteral || $string instanceof IQueryFunction) { - return (string) $string; + return (string)$string; } if ($string === null || $string === 'null' || $string === '*') { diff --git a/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php b/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php new file mode 100644 index 00000000000..d40934669d7 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OCP\ICacheFactory; +use OCP\IMemcache; +use OCP\IMemcacheTTL; + +/** + * A helper to atomically determine the next auto increment value for a sharded table + * + * Since we can't use the database's auto-increment (since each db doesn't know about the keys in the other shards) + * we need external logic for doing the auto increment + */ +class AutoIncrementHandler { + public const MIN_VALID_KEY = 1000; + public const TTL = 365 * 24 * 60 * 60; + + private ?IMemcache $cache = null; + + public function __construct( + private ICacheFactory $cacheFactory, + private ShardConnectionManager $shardConnectionManager, + ) { + if (PHP_INT_SIZE < 8) { + throw new \Exception('sharding is only supported with 64bit php'); + } + } + + private function getCache(): IMemcache { + if(is_null($this->cache)) { + $cache = $this->cacheFactory->createDistributed('shared_autoincrement'); + if ($cache instanceof IMemcache) { + $this->cache = $cache; + } else { + throw new \Exception('Distributed cache ' . get_class($cache) . ' is not suitable'); + } + } + return $this->cache; + } + + /** + * Get the next value for the given shard definition + * + * The returned key is unique and incrementing, but not sequential. + * The shard id is encoded in the first byte of the returned value + * + * @param ShardDefinition $shardDefinition + * @return int + * @throws \Exception + */ + public function getNextPrimaryKey(ShardDefinition $shardDefinition, int $shard): int { + $retries = 0; + while ($retries < 5) { + $next = $this->getNextInner($shardDefinition); + if ($next !== null) { + if ($next > ShardDefinition::MAX_PRIMARY_KEY) { + throw new \Exception('Max primary key of ' . ShardDefinition::MAX_PRIMARY_KEY . ' exceeded'); + } + // we encode the shard the primary key was originally inserted into to allow guessing the shard by primary key later on + return ($next << 8) | $shard; + } else { + $retries++; + } + } + throw new \Exception('Failed to get next primary key'); + } + + /** + * auto increment logic without retry + * + * @param ShardDefinition $shardDefinition + * @return int|null either the next primary key or null if the call needs to be retried + */ + private function getNextInner(ShardDefinition $shardDefinition): ?int { + $cache = $this->getCache(); + // because this function will likely be called concurrently from different requests + // the implementation needs to ensure that the cached value can be cleared, invalidated or re-calculated at any point between our cache calls + // care must be taken that the logic remains fully resilient against race conditions + + // in the ideal case, the last primary key is stored in the cache and we can just do an `inc` + // if that is not the case we find the highest used id in the database increment it, and save it in the cache + + // prevent inc from returning `1` if the key doesn't exist by setting it to a non-numeric value + $cache->add($shardDefinition->table, 'empty-placeholder', self::TTL); + $next = $cache->inc($shardDefinition->table); + + if ($cache instanceof IMemcacheTTL) { + $cache->setTTL($shardDefinition->table, self::TTL); + } + + // the "add + inc" trick above isn't strictly atomic, so as a safety we reject any result that to small + // to handle the edge case of the stored value disappearing between the add and inc + if (is_int($next) && $next >= self::MIN_VALID_KEY) { + return $next; + } elseif (is_int($next)) { + // we hit the edge case, so invalidate the cached value + if (!$cache->cas($shardDefinition->table, $next, 'empty-placeholder')) { + // someone else is changing the value concurrently, give up and retry + return null; + } + } + + // discard the encoded initial shard + $current = $this->getMaxFromDb($shardDefinition) >> 8; + $next = max($current, self::MIN_VALID_KEY) + 1; + if ($cache->cas($shardDefinition->table, 'empty-placeholder', $next)) { + return $next; + } + + // another request set the cached value before us, so we should just be able to inc + $next = $cache->inc($shardDefinition->table); + if (is_int($next) && $next >= self::MIN_VALID_KEY) { + return $next; + } elseif(is_int($next)) { + // key got cleared, invalidate and retry + $cache->cas($shardDefinition->table, $next, 'empty-placeholder'); + return null; + } else { + // cleanup any non-numeric value other than the placeholder if that got stored somehow + $cache->ncad($shardDefinition->table, 'empty-placeholder'); + // retry + return null; + } + } + + /** + * Get the maximum primary key value from the shards + */ + private function getMaxFromDb(ShardDefinition $shardDefinition): int { + $max = 0; + foreach ($shardDefinition->getAllShards() as $shard) { + $connection = $this->shardConnectionManager->getConnection($shardDefinition, $shard); + $query = $connection->getQueryBuilder(); + $query->select($shardDefinition->primaryKey) + ->from($shardDefinition->table) + ->orderBy($shardDefinition->primaryKey, 'DESC') + ->setMaxResults(1); + $result = $query->executeQuery()->fetchOne(); + if ($result) { + $max = max($max, $result); + } + } + return $max; + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php b/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php new file mode 100644 index 00000000000..45f24e32685 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php @@ -0,0 +1,162 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Utility methods for implementing logic that moves data across shards + */ +class CrossShardMoveHelper { + public function __construct( + private ShardConnectionManager $connectionManager + ) { + } + + public function getConnection(ShardDefinition $shardDefinition, int $shardKey): IDBConnection { + return $this->connectionManager->getConnection($shardDefinition, $shardDefinition->getShardForKey($shardKey)); + } + + /** + * Update the shard key of a set of rows, moving them to a different shard if needed + * + * @param ShardDefinition $shardDefinition + * @param string $table + * @param string $shardColumn + * @param int $sourceShardKey + * @param int $targetShardKey + * @param string $primaryColumn + * @param int[] $primaryKeys + * @return void + */ + public function moveCrossShards(ShardDefinition $shardDefinition, string $table, string $shardColumn, int $sourceShardKey, int $targetShardKey, string $primaryColumn, array $primaryKeys): void { + $sourceShard = $shardDefinition->getShardForKey($sourceShardKey); + $targetShard = $shardDefinition->getShardForKey($targetShardKey); + $sourceConnection = $this->connectionManager->getConnection($shardDefinition, $sourceShard); + if ($sourceShard === $targetShard) { + $this->updateItems($sourceConnection, $table, $shardColumn, $targetShardKey, $primaryColumn, $primaryKeys); + + return; + } + $targetConnection = $this->connectionManager->getConnection($shardDefinition, $targetShard); + + $sourceItems = $this->loadItems($sourceConnection, $table, $primaryColumn, $primaryKeys); + foreach ($sourceItems as &$sourceItem) { + $sourceItem[$shardColumn] = $targetShardKey; + } + if (!$sourceItems) { + return; + } + + $sourceConnection->beginTransaction(); + $targetConnection->beginTransaction(); + try { + $this->saveItems($targetConnection, $table, $sourceItems); + $this->deleteItems($sourceConnection, $table, $primaryColumn, $primaryKeys); + + $targetConnection->commit(); + $sourceConnection->commit(); + } catch (\Exception $e) { + $sourceConnection->rollback(); + $targetConnection->rollback(); + throw $e; + } + } + + /** + * Load rows from a table to move + * + * @param IDBConnection $connection + * @param string $table + * @param string $primaryColumn + * @param int[] $primaryKeys + * @return array[] + */ + public function loadItems(IDBConnection $connection, string $table, string $primaryColumn, array $primaryKeys): array { + $query = $connection->getQueryBuilder(); + $query->select('*') + ->from($table) + ->where($query->expr()->in($primaryColumn, $query->createParameter('keys'))); + + $chunks = array_chunk($primaryKeys, 1000); + + $results = []; + foreach ($chunks as $chunk) { + $query->setParameter('keys', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $results = array_merge($results, $query->execute()->fetchAll()); + } + + return $results; + } + + /** + * Save modified rows + * + * @param IDBConnection $connection + * @param string $table + * @param array[] $items + * @return void + */ + public function saveItems(IDBConnection $connection, string $table, array $items): void { + if (count($items) === 0) { + return; + } + $query = $connection->getQueryBuilder(); + $query->insert($table); + foreach ($items[0] as $column => $value) { + $query->setValue($column, $query->createParameter($column)); + } + + foreach ($items as $item) { + foreach ($item as $column => $value) { + if (is_int($column)) { + $query->setParameter($column, $value, IQueryBuilder::PARAM_INT); + } else { + $query->setParameter($column, $value); + } + } + $query->executeStatement(); + } + } + + /** + * @param IDBConnection $connection + * @param string $table + * @param string $primaryColumn + * @param int[] $primaryKeys + * @return void + */ + public function updateItems(IDBConnection $connection, string $table, string $shardColumn, int $targetShardKey, string $primaryColumn, array $primaryKeys): void { + $query = $connection->getQueryBuilder(); + $query->update($table) + ->set($shardColumn, $query->createNamedParameter($targetShardKey, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->in($primaryColumn, $query->createNamedParameter($primaryKeys, IQueryBuilder::PARAM_INT_ARRAY))); + $query->executeQuery()->fetchAll(); + } + + /** + * @param IDBConnection $connection + * @param string $table + * @param string $primaryColumn + * @param int[] $primaryKeys + * @return void + */ + public function deleteItems(IDBConnection $connection, string $table, string $primaryColumn, array $primaryKeys): void { + $query = $connection->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->in($primaryColumn, $query->createParameter('keys'))); + $chunks = array_chunk($primaryKeys, 1000); + + foreach ($chunks as $chunk) { + $query->setParameter('keys', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $query->executeStatement(); + } + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php b/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php new file mode 100644 index 00000000000..af778489a2d --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OCP\DB\QueryBuilder\Sharded\IShardMapper; + +/** + * Map string key to an int-range by hashing the key + */ +class HashShardMapper implements IShardMapper { + public function getShardForKey(int $key, int $count): int { + $int = unpack('L', substr(md5((string)$key, true), 0, 4))[1]; + return $int % $count; + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php b/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php new file mode 100644 index 00000000000..733a6acaf9d --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\DB\QueryBuilder\Sharded; + +/** + * Queries on sharded table has the following limitations: + * + * 1. Either the shard key (e.g. "storage") or primary key (e.g. "fileid") must be mentioned in the query. + * Or the query must be explicitly marked as running across all shards. + * + * For queries where it isn't possible to set one of these keys in the query normally, you can set it using `hintShardKey` + * + * 2. Insert statements must always explicitly set the shard key + * 3. A query on a sharded table is not allowed to join on the same table + * 4. Right joins are not allowed on sharded tables + * 5. Updating the shard key where the new shard key maps to a different shard is not allowed + * + * Moving rows to a different shard needs to be implemented manually. `CrossShardMoveHelper` provides + * some tools to help make this easier. + */ +class InvalidShardedQueryException extends \Exception { + +} diff --git a/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php b/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php new file mode 100644 index 00000000000..a5694b06507 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OCP\DB\QueryBuilder\Sharded\IShardMapper; + +/** + * Map string key to an int-range by hashing the key + */ +class RoundRobinShardMapper implements IShardMapper { + public function getShardForKey(int $key, int $count): int { + return $key % $count; + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php b/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php new file mode 100644 index 00000000000..87cac58bc57 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OC\DB\ConnectionAdapter; +use OC\DB\ConnectionFactory; +use OC\SystemConfig; +use OCP\IDBConnection; + +/** + * Keeps track of the db connections to the various shards + */ +class ShardConnectionManager { + /** @var array<string, IDBConnection> */ + private array $connections = []; + + public function __construct( + private SystemConfig $config, + private ConnectionFactory $factory, + ) { + } + + public function getConnection(ShardDefinition $shardDefinition, int $shard): IDBConnection { + $connectionKey = $shardDefinition->table . '_' . $shard; + if (!isset($this->connections[$connectionKey])) { + $this->connections[$connectionKey] = $this->createConnection($shardDefinition->shards[$shard]); + } + + return $this->connections[$connectionKey]; + } + + private function createConnection(array $shardConfig): IDBConnection { + $shardConfig['sharding'] = []; + $type = $this->config->getValue('dbtype', 'sqlite'); + return new ConnectionAdapter($this->factory->getConnection($type, $shardConfig)); + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php b/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php new file mode 100644 index 00000000000..ebccbb639a6 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OCP\DB\QueryBuilder\Sharded\IShardMapper; + +/** + * Configuration for a shard setup + */ +class ShardDefinition { + // we reserve the bottom byte of the primary key for the initial shard, so the total shard count is limited to what we can fit there + public const MAX_SHARDS = 256; + + public const PRIMARY_KEY_MASK = 0x7F_FF_FF_FF_FF_FF_FF_00; + public const PRIMARY_KEY_SHARD_MASK = 0x00_00_00_00_00_00_00_FF; + // since we reserve 1 byte for the shard index, we only have 56 bits of primary key space + public const MAX_PRIMARY_KEY = PHP_INT_MAX >> 8; + + /** + * @param string $table + * @param string $primaryKey + * @param string $shardKey + * @param string[] $companionKeys + * @param IShardMapper $shardMapper + * @param string[] $companionTables + * @param array $shards + */ + public function __construct( + public string $table, + public string $primaryKey, + public array $companionKeys, + public string $shardKey, + public IShardMapper $shardMapper, + public array $companionTables = [], + public array $shards = [], + ) { + if (count($this->shards) >= self::MAX_SHARDS) { + throw new \Exception('Only allowed maximum of ' . self::MAX_SHARDS . ' shards allowed'); + } + } + + public function hasTable(string $table): bool { + if ($this->table === $table) { + return true; + } + return in_array($table, $this->companionTables); + } + + public function getShardForKey(int $key): int { + return $this->shardMapper->getShardForKey($key, count($this->shards)); + } + + public function getAllShards(): array { + return array_keys($this->shards); + } + + public function isKey(string $column): bool { + return $column === $this->primaryKey || in_array($column, $this->companionKeys); + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php b/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php new file mode 100644 index 00000000000..51cd055e801 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php @@ -0,0 +1,197 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OC\DB\ArrayResult; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IQueryBuilder; + +/** + * Logic for running a query across a number of shards, combining the results + */ +class ShardQueryRunner { + public function __construct( + private ShardConnectionManager $shardConnectionManager, + private ShardDefinition $shardDefinition, + ) { + } + + /** + * Get the shards for a specific query or null if the shards aren't known in advance + * + * @param bool $allShards + * @param int[] $shardKeys + * @return null|int[] + */ + public function getShards(bool $allShards, array $shardKeys): ?array { + if ($allShards) { + return $this->shardDefinition->getAllShards(); + } + $allConfiguredShards = $this->shardDefinition->getAllShards(); + if (count($allConfiguredShards) === 1) { + return $allConfiguredShards; + } + if (empty($shardKeys)) { + return null; + } + $shards = array_map(function ($shardKey) { + return $this->shardDefinition->getShardForKey((int)$shardKey); + }, $shardKeys); + return array_values(array_unique($shards)); + } + + /** + * Try to get the shards that the keys are likely to be in, based on the shard the row was created + * + * @param int[] $primaryKeys + * @return int[] + */ + private function getLikelyShards(array $primaryKeys): array { + $shards = []; + foreach ($primaryKeys as $primaryKey) { + $encodedShard = $primaryKey & ShardDefinition::PRIMARY_KEY_SHARD_MASK; + if ($encodedShard < count($this->shardDefinition->shards) && !in_array($encodedShard, $shards)) { + $shards[] = $encodedShard; + } + } + return $shards; + } + + /** + * Execute a SELECT statement across the configured shards + * + * @param IQueryBuilder $query + * @param bool $allShards + * @param int[] $shardKeys + * @param int[] $primaryKeys + * @param array{column: string, order: string}[] $sortList + * @param int|null $limit + * @param int|null $offset + * @return IResult + */ + public function executeQuery( + IQueryBuilder $query, + bool $allShards, + array $shardKeys, + array $primaryKeys, + ?array $sortList = null, + ?int $limit = null, + ?int $offset = null, + ): IResult { + $shards = $this->getShards($allShards, $shardKeys); + $results = []; + if ($shards && count($shards) === 1) { + // trivial case + return $query->executeQuery($this->shardConnectionManager->getConnection($this->shardDefinition, $shards[0])); + } + // we have to emulate limit and offset, so we select offset+limit from all shards to ensure we have enough rows + // and then filter them down after we merged the results + if ($limit !== null && $offset !== null) { + $query->setMaxResults($limit + $offset); + } + + if ($shards) { + // we know exactly what shards we need to query + foreach ($shards as $shard) { + $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard); + $subResult = $query->executeQuery($shardConnection); + $results = array_merge($results, $subResult->fetchAll()); + $subResult->closeCursor(); + } + } else { + // we don't know for sure what shards we need to query, + // we first try the shards that are "likely" to have the rows we want, based on the shard that the row was + // originally created in. If we then still haven't found all rows we try the rest of the shards + $likelyShards = $this->getLikelyShards($primaryKeys); + $unlikelyShards = array_diff($this->shardDefinition->getAllShards(), $likelyShards); + $shards = array_merge($likelyShards, $unlikelyShards); + + foreach ($shards as $shard) { + $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard); + $subResult = $query->executeQuery($shardConnection); + $rows = $subResult->fetchAll(); + $results = array_merge($results, $rows); + $subResult->closeCursor(); + + if (count($rows) >= count($primaryKeys)) { + // we have all the rows we're looking for + break; + } + } + } + + if ($sortList) { + usort($results, function ($a, $b) use ($sortList) { + foreach ($sortList as $sort) { + $valueA = $a[$sort['column']] ?? null; + $valueB = $b[$sort['column']] ?? null; + $cmp = $valueA <=> $valueB; + if ($cmp === 0) { + continue; + } + if ($sort['order'] === 'DESC') { + $cmp = -$cmp; + } + return $cmp; + } + }); + } + + if ($limit !== null && $offset !== null) { + $results = array_slice($results, $offset, $limit); + } elseif ($limit !== null) { + $results = array_slice($results, 0, $limit); + } elseif ($offset !== null) { + $results = array_slice($results, $offset); + } + + return new ArrayResult($results); + } + + /** + * Execute an UPDATE or DELETE statement + * + * @param IQueryBuilder $query + * @param bool $allShards + * @param int[] $shardKeys + * @param int[] $primaryKeys + * @return int + * @throws \OCP\DB\Exception + */ + public function executeStatement(IQueryBuilder $query, bool $allShards, array $shardKeys, array $primaryKeys): int { + if ($query->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT) { + throw new \Exception('insert queries need special handling'); + } + + $shards = $this->getShards($allShards, $shardKeys); + $maxCount = count($primaryKeys); + if ($shards && count($shards) === 1) { + return $query->executeStatement($this->shardConnectionManager->getConnection($this->shardDefinition, $shards[0])); + } elseif ($shards) { + $maxCount = PHP_INT_MAX; + } else { + // sort the likely shards before the rest, similar logic to `self::executeQuery` + $likelyShards = $this->getLikelyShards($primaryKeys); + $unlikelyShards = array_diff($this->shardDefinition->getAllShards(), $likelyShards); + $shards = array_merge($likelyShards, $unlikelyShards); + } + + $count = 0; + + foreach ($shards as $shard) { + $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard); + $count += $query->executeStatement($shardConnection); + + if ($count >= $maxCount) { + break; + } + } + return $count; + } +} diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php new file mode 100644 index 00000000000..e7bc70ce440 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php @@ -0,0 +1,407 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\DB\QueryBuilder\Sharded; + +use OC\DB\QueryBuilder\CompositeExpression; +use OC\DB\QueryBuilder\ExtendedQueryBuilder; +use OC\DB\QueryBuilder\Parameter; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * A special query builder that automatically distributes queries over multiple database shards. + * + * This relies on `PartitionedQueryBuilder` to handle splitting of parts of the query that touch the sharded tables + * from the non-sharded tables. So the query build here should only either touch only sharded table or only non-sharded tables. + * + * Most of the logic in this class is concerned with extracting either the shard key (e.g. "storage") or primary key (e.g. "fileid") + * from the query. The logic for actually running the query across the shards is mostly delegated to `ShardQueryRunner`. + */ +class ShardedQueryBuilder extends ExtendedQueryBuilder { + private array $shardKeys = []; + private array $primaryKeys = []; + private ?ShardDefinition $shardDefinition = null; + /** @var bool Run the query across all shards */ + private bool $allShards = false; + private ?string $insertTable = null; + private mixed $lastInsertId = null; + private ?IDBConnection $lastInsertConnection = null; + private ?int $updateShardKey = null; + private ?int $limit = null; + private ?int $offset = null; + /** @var array{column: string, order: string}[] */ + private array $sortList = []; + private string $mainTable = ''; + + public function __construct( + IQueryBuilder $builder, + protected array $shardDefinitions, + protected ShardConnectionManager $shardConnectionManager, + protected AutoIncrementHandler $autoIncrementHandler, + ) { + parent::__construct($builder); + } + + public function getShardKeys(): array { + return $this->getKeyValues($this->shardKeys); + } + + public function getPrimaryKeys(): array { + return $this->getKeyValues($this->primaryKeys); + } + + private function getKeyValues(array $keys): array { + $values = []; + foreach ($keys as $key) { + $values = array_merge($values, $this->getKeyValue($key)); + } + return array_values(array_unique($values)); + } + + private function getKeyValue($value): array { + if ($value instanceof Parameter) { + $value = (string)$value; + } + if (is_string($value) && str_starts_with($value, ':')) { + $param = $this->getParameter(substr($value, 1)); + if (is_array($param)) { + return $param; + } else { + return [$param]; + } + } elseif ($value !== null) { + return [$value]; + } else { + return []; + } + } + + public function where(...$predicates) { + return $this->andWhere(...$predicates); + } + + public function andWhere(...$where) { + if ($where) { + foreach ($where as $predicate) { + $this->tryLoadShardKey($predicate); + } + parent::andWhere(...$where); + } + return $this; + } + + private function tryLoadShardKey($predicate): void { + if (!$this->shardDefinition) { + return; + } + if ($keys = $this->tryExtractShardKeys($predicate, $this->shardDefinition->shardKey)) { + $this->shardKeys += $keys; + } + if ($keys = $this->tryExtractShardKeys($predicate, $this->shardDefinition->primaryKey)) { + $this->primaryKeys += $keys; + } + foreach ($this->shardDefinition->companionKeys as $companionKey) { + if ($keys = $this->tryExtractShardKeys($predicate, $companionKey)) { + $this->primaryKeys += $keys; + } + } + } + + /** + * @param $predicate + * @param string $column + * @return string[] + */ + private function tryExtractShardKeys($predicate, string $column): array { + if ($predicate instanceof CompositeExpression) { + $values = []; + foreach ($predicate->getParts() as $part) { + $partValues = $this->tryExtractShardKeys($part, $column); + // for OR expressions, we can only rely on the predicate if all parts contain the comparison + if ($predicate->getType() === CompositeExpression::TYPE_OR && !$partValues) { + return []; + } + $values = array_merge($values, $partValues); + } + return $values; + } + $predicate = (string)$predicate; + // expect a condition in the form of 'alias1.column1 = placeholder' or 'alias1.column1 in placeholder' + if (substr_count($predicate, ' ') > 2) { + return []; + } + if (str_contains($predicate, ' = ')) { + $parts = explode(' = ', $predicate); + if ($parts[0] === "`{$column}`" || str_ends_with($parts[0], "`.`{$column}`")) { + return [$parts[1]]; + } else { + return []; + } + } + + if (str_contains($predicate, ' IN ')) { + $parts = explode(' IN ', $predicate); + if ($parts[0] === "`{$column}`" || str_ends_with($parts[0], "`.`{$column}`")) { + return [trim(trim($parts[1], '('), ')')]; + } else { + return []; + } + } + + return []; + } + + public function set($key, $value) { + if ($this->shardDefinition && $key === $this->shardDefinition->shardKey) { + $updateShardKey = $value; + } + return parent::set($key, $value); + } + + public function setValue($column, $value) { + if ($this->shardDefinition) { + if ($this->shardDefinition->isKey($column)) { + $this->primaryKeys[] = $value; + } + if ($column === $this->shardDefinition->shardKey) { + $this->shardKeys[] = $value; + } + } + return parent::setValue($column, $value); + } + + public function values(array $values) { + foreach ($values as $column => $value) { + $this->setValue($column, $value); + } + return $this; + } + + private function actOnTable(string $table): void { + $this->mainTable = $table; + foreach ($this->shardDefinitions as $shardDefinition) { + if ($shardDefinition->hasTable($table)) { + $this->shardDefinition = $shardDefinition; + } + } + } + + public function from($from, $alias = null) { + if (is_string($from) && $from) { + $this->actOnTable($from); + } + return parent::from($from, $alias); + } + + public function update($update = null, $alias = null) { + if (is_string($update) && $update) { + $this->actOnTable($update); + } + return parent::update($update, $alias); + } + + public function insert($insert = null) { + if (is_string($insert) && $insert) { + $this->insertTable = $insert; + $this->actOnTable($insert); + } + return parent::insert($insert); + } + + public function delete($delete = null, $alias = null) { + if (is_string($delete) && $delete) { + $this->actOnTable($delete); + } + return parent::delete($delete, $alias); + } + + private function checkJoin(string $table): void { + if ($this->shardDefinition) { + if ($table === $this->mainTable) { + throw new InvalidShardedQueryException("Sharded query on {$this->mainTable} isn't allowed to join on itself"); + } + if (!$this->shardDefinition->hasTable($table)) { + // this generally shouldn't happen as the partitioning logic should prevent this + // but the check is here just in case + throw new InvalidShardedQueryException("Sharded query on {$this->shardDefinition->table} isn't allowed to join on $table"); + } + } + } + + public function innerJoin($fromAlias, $join, $alias, $condition = null) { + $this->checkJoin($join); + return parent::innerJoin($fromAlias, $join, $alias, $condition); + } + + public function leftJoin($fromAlias, $join, $alias, $condition = null) { + $this->checkJoin($join); + return parent::leftJoin($fromAlias, $join, $alias, $condition); + } + + public function rightJoin($fromAlias, $join, $alias, $condition = null) { + if ($this->shardDefinition) { + throw new InvalidShardedQueryException("Sharded query on {$this->shardDefinition->table} isn't allowed to right join"); + } + return parent::rightJoin($fromAlias, $join, $alias, $condition); + } + + public function join($fromAlias, $join, $alias, $condition = null) { + return $this->innerJoin($fromAlias, $join, $alias, $condition); + } + + public function setMaxResults($maxResults) { + if ($maxResults > 0) { + $this->limit = (int)$maxResults; + } + return parent::setMaxResults($maxResults); + } + + public function setFirstResult($firstResult) { + if ($firstResult > 0) { + $this->offset = (int)$firstResult; + } + if ($this->shardDefinition && count($this->shardDefinition->shards) > 1) { + // we have to emulate offset + return $this; + } else { + return parent::setFirstResult($firstResult); + } + } + + public function addOrderBy($sort, $order = null) { + $this->registerOrder((string)$sort, (string)$order ?? 'ASC'); + return parent::orderBy($sort, $order); + } + + public function orderBy($sort, $order = null) { + $this->sortList = []; + $this->registerOrder((string)$sort, (string)$order ?? 'ASC'); + return parent::orderBy($sort, $order); + } + + private function registerOrder(string $column, string $order): void { + // handle `mime + 0` and similar by just sorting on the first part of the expression + [$column] = explode(' ', $column); + $column = trim($column, '`'); + $this->sortList[] = [ + 'column' => $column, + 'order' => strtoupper($order), + ]; + } + + public function hintShardKey(string $column, mixed $value, bool $overwrite = false) { + if ($overwrite) { + $this->primaryKeys = []; + $this->shardKeys = []; + } + if ($this->shardDefinition?->isKey($column)) { + $this->primaryKeys[] = $value; + } + if ($column === $this->shardDefinition?->shardKey) { + $this->shardKeys[] = $value; + } + return $this; + } + + public function runAcrossAllShards() { + $this->allShards = true; + return $this; + } + + /** + * @throws InvalidShardedQueryException + */ + public function validate(): void { + if ($this->shardDefinition && $this->insertTable) { + if ($this->allShards) { + throw new InvalidShardedQueryException("Can't insert across all shards"); + } + if (empty($this->getShardKeys())) { + throw new InvalidShardedQueryException("Can't insert without shard key"); + } + } + if ($this->shardDefinition && !$this->allShards) { + if (empty($this->getShardKeys()) && empty($this->getPrimaryKeys())) { + throw new InvalidShardedQueryException('No shard key or primary key set for query'); + } + } + if ($this->shardDefinition && $this->updateShardKey) { + $newShardKey = $this->getKeyValue($this->updateShardKey); + $oldShardKeys = $this->getShardKeys(); + if (count($newShardKey) !== 1) { + throw new InvalidShardedQueryException("Can't set shard key to an array"); + } + $newShardKey = current($newShardKey); + if (empty($oldShardKeys)) { + throw new InvalidShardedQueryException("Can't update without shard key"); + } + $oldShards = array_values(array_unique(array_map(function ($shardKey) { + return $this->shardDefinition->getShardForKey((int)$shardKey); + }, $oldShardKeys))); + $newShard = $this->shardDefinition->getShardForKey((int)$newShardKey); + if ($oldShards === [$newShard]) { + throw new InvalidShardedQueryException('Update statement would move rows to a different shard'); + } + } + } + + public function executeQuery(?IDBConnection $connection = null): IResult { + $this->validate(); + if ($this->shardDefinition) { + $runner = new ShardQueryRunner($this->shardConnectionManager, $this->shardDefinition); + return $runner->executeQuery($this->builder, $this->allShards, $this->getShardKeys(), $this->getPrimaryKeys(), $this->sortList, $this->limit, $this->offset); + } + return parent::executeQuery($connection); + } + + public function executeStatement(?IDBConnection $connection = null): int { + $this->validate(); + if ($this->shardDefinition) { + $runner = new ShardQueryRunner($this->shardConnectionManager, $this->shardDefinition); + if ($this->insertTable) { + $shards = $runner->getShards($this->allShards, $this->getShardKeys()); + if (!$shards) { + throw new InvalidShardedQueryException("Can't insert without shard key"); + } + $count = 0; + foreach ($shards as $shard) { + $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard); + if (!$this->primaryKeys && $this->shardDefinition->table === $this->insertTable) { + $id = $this->autoIncrementHandler->getNextPrimaryKey($this->shardDefinition, $shard); + parent::setValue($this->shardDefinition->primaryKey, $this->createParameter('__generated_primary_key')); + $this->setParameter('__generated_primary_key', $id, self::PARAM_INT); + $this->lastInsertId = $id; + } + $count += parent::executeStatement($shardConnection); + + $this->lastInsertConnection = $shardConnection; + } + return $count; + } else { + return $runner->executeStatement($this->builder, $this->allShards, $this->getShardKeys(), $this->getPrimaryKeys()); + } + } + return parent::executeStatement($connection); + } + + public function getLastInsertId(): int { + if ($this->lastInsertId) { + return $this->lastInsertId; + } + if ($this->lastInsertConnection) { + $table = $this->builder->prefixTableName($this->insertTable); + return $this->lastInsertConnection->lastInsertId($table); + } else { + return parent::getLastInsertId(); + } + } + + +} diff --git a/lib/private/DB/SchemaWrapper.php b/lib/private/DB/SchemaWrapper.php index 5720e10fbdb..473c0009237 100644 --- a/lib/private/DB/SchemaWrapper.php +++ b/lib/private/DB/SchemaWrapper.php @@ -36,6 +36,9 @@ class SchemaWrapper implements ISchemaWrapper { public function performDropTableCalls() { foreach ($this->tablesToDelete as $tableName => $true) { $this->connection->dropTable($tableName); + foreach ($this->connection->getShardConnections() as $shardConnection) { + $shardConnection->dropTable($tableName); + } unset($this->tablesToDelete[$tableName]); } } diff --git a/lib/private/DateTimeFormatter.php b/lib/private/DateTimeFormatter.php index cd765a2a14b..2882a7d8cd7 100644 --- a/lib/private/DateTimeFormatter.php +++ b/lib/private/DateTimeFormatter.php @@ -28,8 +28,8 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Get TimeZone to use * - * @param \DateTimeZone $timeZone The timezone to use - * @return \DateTimeZone The timezone to use, falling back to the current user's timezone + * @param \DateTimeZone $timeZone The timezone to use + * @return \DateTimeZone The timezone to use, falling back to the current user's timezone */ protected function getTimeZone($timeZone = null) { if ($timeZone === null) { @@ -42,8 +42,8 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Get \OCP\IL10N to use * - * @param \OCP\IL10N $l The locale to use - * @return \OCP\IL10N The locale to use, falling back to the current user's locale + * @param \OCP\IL10N $l The locale to use + * @return \OCP\IL10N The locale to use, falling back to the current user's locale */ protected function getLocale($l = null) { if ($l === null) { @@ -57,7 +57,7 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { * Generates a DateTime object with the given timestamp and TimeZone * * @param mixed $timestamp - * @param \DateTimeZone $timeZone The timezone to use + * @param \DateTimeZone $timeZone The timezone to use * @return \DateTime */ protected function getDateTime($timestamp, ?\DateTimeZone $timeZone = null) { @@ -77,15 +77,15 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the date of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $format Either 'full', 'long', 'medium' or 'short' - * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014' - * long: e.g. 'MMMM d, y' => 'August 20, 2014' - * medium: e.g. 'MMM d, y' => 'Aug 20, 2014' - * short: e.g. 'M/d/yy' => '8/20/14' - * The exact format is dependent on the language - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $format Either 'full', 'long', 'medium' or 'short' + * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014' + * long: e.g. 'MMMM d, y' => 'August 20, 2014' + * medium: e.g. 'MMM d, y' => 'Aug 20, 2014' + * short: e.g. 'M/d/yy' => '8/20/14' + * The exact format is dependent on the language + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted date string */ public function formatDate($timestamp, $format = 'long', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { @@ -95,16 +95,16 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the date of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $format Either 'full', 'long', 'medium' or 'short' - * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014' - * long: e.g. 'MMMM d, y' => 'August 20, 2014' - * medium: e.g. 'MMM d, y' => 'Aug 20, 2014' - * short: e.g. 'M/d/yy' => '8/20/14' - * The exact format is dependent on the language - * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $format Either 'full', 'long', 'medium' or 'short' + * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014' + * long: e.g. 'MMMM d, y' => 'August 20, 2014' + * medium: e.g. 'MMM d, y' => 'Aug 20, 2014' + * short: e.g. 'M/d/yy' => '8/20/14' + * The exact format is dependent on the language + * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted relative date string */ public function formatDateRelativeDay($timestamp, $format = 'long', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { @@ -119,13 +119,13 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { * Gives the relative date of the timestamp * Only works for past dates * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time + * @param \OCP\IL10N $l The locale to use * @return string Formatted date span. Dates returned are: - * < 1 month => Today, Yesterday, n days ago - * < 13 month => last month, n months ago - * >= 13 month => last year, n years ago + * < 1 month => Today, Yesterday, n days ago + * < 13 month => last month, n months ago + * >= 13 month => last year, n years ago */ public function formatDateSpan($timestamp, $baseTimestamp = null, ?\OCP\IL10N $l = null) { $l = $this->getLocale($l); @@ -182,15 +182,15 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the time of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $format Either 'full', 'long', 'medium' or 'short' - * full: e.g. 'h:mm:ss a zzzz' => '11:42:13 AM GMT+0:00' - * long: e.g. 'h:mm:ss a z' => '11:42:13 AM GMT' - * medium: e.g. 'h:mm:ss a' => '11:42:13 AM' - * short: e.g. 'h:mm a' => '11:42 AM' - * The exact format is dependent on the language - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $format Either 'full', 'long', 'medium' or 'short' + * full: e.g. 'h:mm:ss a zzzz' => '11:42:13 AM GMT+0:00' + * long: e.g. 'h:mm:ss a z' => '11:42:13 AM GMT' + * medium: e.g. 'h:mm:ss a' => '11:42:13 AM' + * short: e.g. 'h:mm a' => '11:42 AM' + * The exact format is dependent on the language + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted time string */ public function formatTime($timestamp, $format = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { @@ -200,16 +200,16 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Gives the relative past time of the timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time + * @param \OCP\IL10N $l The locale to use * @return string Formatted time span. Dates returned are: - * < 60 sec => seconds ago - * < 1 hour => n minutes ago - * < 1 day => n hours ago - * < 1 month => Yesterday, n days ago - * < 13 month => last month, n months ago - * >= 13 month => last year, n years ago + * < 60 sec => seconds ago + * < 1 hour => n minutes ago + * < 1 day => n hours ago + * < 1 month => Yesterday, n days ago + * < 13 month => last month, n months ago + * >= 13 month => last year, n years ago */ public function formatTimeSpan($timestamp, $baseTimestamp = null, ?\OCP\IL10N $l = null) { $l = $this->getLocale($l); @@ -247,11 +247,11 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the date and time of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $formatDate See formatDate() for description - * @param string $formatTime See formatTime() for description - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $formatDate See formatDate() for description + * @param string $formatTime See formatTime() for description + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted date and time string */ public function formatDateTime($timestamp, $formatDate = 'long', $formatTime = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { @@ -261,12 +261,12 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the date and time of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $formatDate See formatDate() for description - * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable - * @param string $formatTime See formatTime() for description - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $formatDate See formatDate() for description + * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable + * @param string $formatTime See formatTime() for description + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted relative date and time string */ public function formatDateTimeRelativeDay($timestamp, $formatDate = 'long', $formatTime = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { @@ -280,11 +280,11 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter { /** * Formats the date and time of the given timestamp * - * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object - * @param string $type One of 'date', 'datetime' or 'time' - * @param string $format Format string - * @param \DateTimeZone $timeZone The timezone to use - * @param \OCP\IL10N $l The locale to use + * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object + * @param string $type One of 'date', 'datetime' or 'time' + * @param string $format Format string + * @param \DateTimeZone $timeZone The timezone to use + * @param \OCP\IL10N $l The locale to use * @return string Formatted date and time string */ protected function format($timestamp, $type, $format, ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) { diff --git a/lib/private/DirectEditing/Token.php b/lib/private/DirectEditing/Token.php index 594cef98086..12ad9411216 100644 --- a/lib/private/DirectEditing/Token.php +++ b/lib/private/DirectEditing/Token.php @@ -42,7 +42,7 @@ class Token implements IToken { } public function hasBeenAccessed(): bool { - return (bool) $this->data['accessed']; + return (bool)$this->data['accessed']; } public function getEditor(): string { diff --git a/lib/private/Encryption/DecryptAll.php b/lib/private/Encryption/DecryptAll.php index f9a92d07d20..0007467298c 100644 --- a/lib/private/Encryption/DecryptAll.php +++ b/lib/private/Encryption/DecryptAll.php @@ -17,13 +17,13 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class DecryptAll { - /** @var OutputInterface */ + /** @var OutputInterface */ protected $output; - /** @var InputInterface */ + /** @var InputInterface */ protected $input; - /** @var array files which couldn't be decrypted */ + /** @var array files which couldn't be decrypted */ protected $failed; public function __construct( @@ -114,7 +114,7 @@ class DecryptAll { $fetchUsersProgress = new ProgressBar($this->output); $fetchUsersProgress->setFormat(" %message% \n [%bar%]"); $fetchUsersProgress->start(); - $fetchUsersProgress->setMessage("Fetch list of users..."); + $fetchUsersProgress->setMessage('Fetch list of users...'); $fetchUsersProgress->advance(); foreach ($this->userManager->getBackends() as $backend) { @@ -128,7 +128,7 @@ class DecryptAll { $offset += $limit; $fetchUsersProgress->advance(); } while (count($users) >= $limit); - $fetchUsersProgress->setMessage("Fetch list of users... finished"); + $fetchUsersProgress->setMessage('Fetch list of users... finished'); $fetchUsersProgress->finish(); } } else { @@ -140,7 +140,7 @@ class DecryptAll { $progress = new ProgressBar($this->output); $progress->setFormat(" %message% \n [%bar%]"); $progress->start(); - $progress->setMessage("starting to decrypt files..."); + $progress->setMessage('starting to decrypt files...'); $progress->advance(); $numberOfUsers = count($userList); @@ -151,7 +151,7 @@ class DecryptAll { $userNo++; } - $progress->setMessage("starting to decrypt files... finished"); + $progress->setMessage('starting to decrypt files... finished'); $progress->finish(); $this->output->writeln("\n\n"); diff --git a/lib/private/Encryption/EncryptionWrapper.php b/lib/private/Encryption/EncryptionWrapper.php index d3bf0aeb4d8..aec93a3ce4d 100644 --- a/lib/private/Encryption/EncryptionWrapper.php +++ b/lib/private/Encryption/EncryptionWrapper.php @@ -26,10 +26,10 @@ use Psr\Log\LoggerInterface; * @package OC\Encryption */ class EncryptionWrapper { - /** @var ArrayCache */ + /** @var ArrayCache */ private $arrayCache; - /** @var Manager */ + /** @var Manager */ private $manager; private LoggerInterface $logger; diff --git a/lib/private/Encryption/File.php b/lib/private/Encryption/File.php index a29d62946c4..26e643d1006 100644 --- a/lib/private/Encryption/File.php +++ b/lib/private/Encryption/File.php @@ -92,7 +92,7 @@ class File implements \OCP\Encryption\IFile { } // check if it is a group mount - if ($this->getAppManager()->isEnabledForUser("files_external")) { + if ($this->getAppManager()->isEnabledForUser('files_external')) { /** @var GlobalStoragesService $storageService */ $storageService = \OC::$server->get(GlobalStoragesService::class); $storages = $storageService->getAllStorages(); diff --git a/lib/private/Encryption/HookManager.php b/lib/private/Encryption/HookManager.php index 5ce51229e4e..39e7edabb95 100644 --- a/lib/private/Encryption/HookManager.php +++ b/lib/private/Encryption/HookManager.php @@ -42,7 +42,7 @@ class HookManager { $user = \OC::$server->getUserManager()->get($owner); } if (!$user) { - throw new \Exception("Inconsistent data, File unshared, but owner not found. Should not happen"); + throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen'); } $uid = ''; diff --git a/lib/private/EventSource.php b/lib/private/EventSource.php index dbeda25049e..18af6e35832 100644 --- a/lib/private/EventSource.php +++ b/lib/private/EventSource.php @@ -45,10 +45,10 @@ class EventSource implements IEventSource { * @link https://github.com/owncloud/core/issues/14286 */ header("Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'"); - header("Content-Type: text/html"); + header('Content-Type: text/html'); echo str_repeat('<span></span>' . PHP_EOL, 10); //dummy data to keep IE happy } else { - header("Content-Type: text/event-stream"); + header('Content-Type: text/event-stream'); } if (!$this->request->passesStrictCookieCheck()) { header('Location: '.\OC::$WEBROOT); @@ -69,7 +69,7 @@ class EventSource implements IEventSource { * @param mixed $data * * @throws \BadMethodCallException - * if only one parameter is given, a typeless message will be send with that parameter as data + * if only one parameter is given, a typeless message will be send with that parameter as data * @suppress PhanDeprecatedFunction */ public function send($type, $data = null) { diff --git a/lib/private/Federation/CloudIdManager.php b/lib/private/Federation/CloudIdManager.php index 3528d06a167..69d48a148b3 100644 --- a/lib/private/Federation/CloudIdManager.php +++ b/lib/private/Federation/CloudIdManager.php @@ -59,7 +59,7 @@ class CloudIdManager implements ICloudIdManager { if ($event instanceof CardUpdatedEvent) { $data = $event->getCardData()['carddata']; foreach (explode("\r\n", $data) as $line) { - if (str_starts_with($line, "CLOUD;")) { + if (str_starts_with($line, 'CLOUD;')) { $parts = explode(':', $line, 2); if (isset($parts[1])) { $key = $parts[1]; diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index a4290549dd9..a8d9067050d 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -9,6 +9,7 @@ namespace OC\Files\Cache; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OC\DB\Exceptions\DbalException; +use OC\DB\QueryBuilder\Sharded\ShardDefinition; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; use OC\Files\Storage\Wrapper\Encryption; @@ -284,6 +285,7 @@ class Cache implements ICache { if (count($extensionValues)) { $query = $this->getQueryBuilder(); $query->insert('filecache_extended'); + $query->hintShardKey('storage', $storageId); $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); foreach ($extensionValues as $column => $value) { @@ -357,6 +359,7 @@ class Cache implements ICache { try { $query = $this->getQueryBuilder(); $query->insert('filecache_extended'); + $query->hintShardKey('storage', $this->getNumericStorageId()); $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)); foreach ($extensionValues as $column => $value) { @@ -368,6 +371,7 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->update('filecache_extended') ->whereFileId($id) + ->hintShardKey('storage', $this->getNumericStorageId()) ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) { return $query->expr()->orX( $query->expr()->neq($key, $query->createNamedParameter($value)), @@ -520,7 +524,8 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->delete('filecache_extended') - ->whereFileId($entry->getId()); + ->whereFileId($entry->getId()) + ->hintShardKey('storage', $this->getNumericStorageId()); $query->execute(); if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) { @@ -564,7 +569,8 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->delete('filecache_extended') - ->where($query->expr()->in('fileid', $query->createParameter('childIds'))); + ->where($query->expr()->in('fileid', $query->createParameter('childIds'))) + ->hintShardKey('storage', $this->getNumericStorageId()); foreach (array_chunk($childIds, 1000) as $childIdChunk) { $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY); @@ -652,6 +658,15 @@ class Cache implements ICache { throw new \Exception('Invalid source storage path: ' . $sourcePath); } + $shardDefinition = $this->connection->getShardDefinition('filecache'); + if ( + $shardDefinition && + $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId()) + ) { + $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath); + return; + } + $sourceId = $sourceData['fileid']; $newParentId = $this->getParentId($targetPath); @@ -673,7 +688,7 @@ class Cache implements ICache { $childChunks = array_chunk($childIds, 1000); - $query = $this->connection->getQueryBuilder(); + $query = $this->getQueryBuilder(); $fun = $query->func(); $newPathFunction = $fun->concat( @@ -681,12 +696,15 @@ class Cache implements ICache { $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash ); $query->update('filecache') - ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT)) ->set('path_hash', $fun->md5($newPathFunction)) ->set('path', $newPathFunction) - ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT))) + ->whereStorageId($sourceStorageId) ->andWhere($query->expr()->in('fileid', $query->createParameter('files'))); + if ($sourceStorageId !== $targetStorageId) { + $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT); + } + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) { $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)); @@ -728,13 +746,17 @@ class Cache implements ICache { $query = $this->getQueryBuilder(); $query->update('filecache') - ->set('storage', $query->createNamedParameter($targetStorageId)) ->set('path', $query->createNamedParameter($targetPath)) ->set('path_hash', $query->createNamedParameter(md5($targetPath))) ->set('name', $query->createNamedParameter(basename($targetPath))) ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT)) + ->whereStorageId($sourceStorageId) ->whereFileId($sourceId); + if ($sourceStorageId !== $targetStorageId) { + $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT); + } + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) { $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)); @@ -839,7 +861,7 @@ class Cache implements ICache { * search for files by mimetype * * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image') - * where it will search for all mimetypes in the group ('image/*') + * where it will search for all mimetypes in the group ('image/*') * @return ICacheEntry[] an array of cache entries where the mimetype matches the search */ public function searchByMime($mimetype) { @@ -891,6 +913,7 @@ class Cache implements ICache { $query->select($query->func()->count()) ->from('filecache') ->whereParent($fileId) + ->whereStorageId($this->getNumericStorageId()) ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); $result = $query->execute(); @@ -1133,7 +1156,7 @@ class Cache implements ICache { */ public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { if ($sourceEntry->getId() < 0) { - throw new \RuntimeException("Invalid source cache entry on copyFromCache"); + throw new \RuntimeException('Invalid source cache entry on copyFromCache'); } $data = $this->cacheEntryToArray($sourceEntry); @@ -1144,7 +1167,7 @@ class Cache implements ICache { $fileId = $this->put($targetPath, $data); if ($fileId <= 0) { - throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " "); + throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' '); } if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId()); @@ -1183,4 +1206,72 @@ class Cache implements ICache { return null; } } + + private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath) { + if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { + $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourceEntry->getPath()); + } else { + $fileIds = []; + } + $fileIds[] = $sourceEntry->getId(); + + $helper = $this->connection->getCrossShardMoveHelper(); + + $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId()); + $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId()); + + $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds); + $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds); + $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds); + + // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark + $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper(); + + $sourcePathLength = strlen($sourceEntry->getPath()); + foreach ($cacheItems as &$cacheItem) { + if ($cacheItem['path'] === $sourceEntry->getPath()) { + $cacheItem['path'] = $targetPath; + $cacheItem['parent'] = $this->getParentId($targetPath); + $cacheItem['name'] = basename($cacheItem['path']); + } else { + $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash + } + $cacheItem['path_hash'] = md5($cacheItem['path']); + $cacheItem['storage'] = $this->getNumericStorageId(); + if ($removeEncryptedFlag) { + $cacheItem['encrypted'] = 0; + } + } + + $targetConnection->beginTransaction(); + + try { + $helper->saveItems($targetConnection, 'filecache', $cacheItems); + $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems); + $helper->saveItems($targetConnection, 'files_metadata', $metadataItems); + } catch (\Exception $e) { + $targetConnection->rollback(); + throw $e; + } + + $sourceConnection->beginTransaction(); + + try { + $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds); + $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds); + $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds); + } catch (\Exception $e) { + $targetConnection->rollback(); + $sourceConnection->rollBack(); + throw $e; + } + + try { + $sourceConnection->commit(); + } catch (\Exception $e) { + $targetConnection->rollback(); + throw $e; + } + $targetConnection->commit(); + } } diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php index 8ba2ac491bf..44c1016ca8e 100644 --- a/lib/private/Files/Cache/FailedCache.php +++ b/lib/private/Files/Cache/FailedCache.php @@ -125,7 +125,7 @@ class FailedCache implements ICache { } public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { - throw new \Exception("Invalid cache"); + throw new \Exception('Invalid cache'); } public function getQueryFilterForStorage(): ISearchOperator { diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 0b164912301..82f9ec3be66 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -194,7 +194,7 @@ class QuerySearchHelper { protected function requireUser(ISearchQuery $searchQuery): IUser { $user = $searchQuery->getUser(); if ($user === null) { - throw new \InvalidArgumentException("This search operation requires the user to be set in the query"); + throw new \InvalidArgumentException('This search operation requires the user to be set in the query'); } return $user; } diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index 0929907fcff..8d99a268dc0 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -80,7 +80,7 @@ class Storage { * Adjusts the storage id to use md5 if too long * @param string $storageId storage id * @return string unchanged $storageId if its length is less than 64 characters, - * else returns the md5 of $storageId + * else returns the md5 of $storageId */ public static function adjustStorageId($storageId) { if (strlen($storageId) > 64) { diff --git a/lib/private/Files/Cache/Updater.php b/lib/private/Files/Cache/Updater.php index e8c6d32599e..eab68b4f545 100644 --- a/lib/private/Files/Cache/Updater.php +++ b/lib/private/Files/Cache/Updater.php @@ -114,7 +114,7 @@ class Updater implements IUpdater { } // encryption is a pita and touches the cache itself - if (isset($data['encrypted']) && !!$data['encrypted']) { + if (isset($data['encrypted']) && (bool)$data['encrypted']) { $sizeDifference = null; } @@ -246,7 +246,7 @@ class Updater implements IUpdater { // ignore the failure. // with failures concurrent updates, someone else would have already done it. // in the worst case the `storage_mtime` isn't updated, which should at most only trigger an extra rescan - $this->logger->warning("Error while updating parent storage_mtime, should be safe to ignore", ['exception' => $e]); + $this->logger->warning('Error while updating parent storage_mtime, should be safe to ignore', ['exception' => $e]); } } } diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index 0e103690b6b..1dbc469c8c3 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -131,9 +131,9 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { } $lateMounts = $this->filterMounts($user, $lateMounts); - $this->eventLogger->start("fs:setup:add-mounts", "Add mounts to the filesystem"); + $this->eventLogger->start('fs:setup:add-mounts', 'Add mounts to the filesystem'); array_walk($lateMounts, [$mountManager, 'addMount']); - $this->eventLogger->end("fs:setup:add-mounts"); + $this->eventLogger->end('fs:setup:add-mounts'); return array_merge($lateMounts, $firstMounts); } @@ -223,7 +223,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { }, []); if (count($mounts) === 0) { - throw new \Exception("No root mounts provided by any provider"); + 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 67b2cad7ea2..94da770b63f 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -477,7 +477,7 @@ class UserMountCache implements IUserMountCache { } } - throw new NotFoundException("No cached mount for path " . $path); + throw new NotFoundException('No cached mount for path ' . $path); } public function getMountsInPath(IUser $user, string $path): array { diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index c3dcb531342..c47b8b1d1a7 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -134,7 +134,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @return int|null */ public function getId() { - return isset($this->data['fileid']) ? (int) $this->data['fileid'] : null; + return isset($this->data['fileid']) ? (int)$this->data['fileid'] : null; } /** @@ -196,7 +196,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { */ public function getMTime() { $this->updateEntryfromSubMounts(); - return (int) $this->data['mtime']; + return (int)$this->data['mtime']; } /** @@ -210,14 +210,14 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * 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; + return isset($this->data['encryptedVersion']) ? (int)$this->data['encryptedVersion'] : 1; } /** * @return int */ public function getPermissions() { - return (int) $this->data['permissions']; + return (int)$this->data['permissions']; } /** @@ -379,11 +379,11 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { } public function getCreationTime(): int { - return (int) $this->data['creation_time']; + return (int)$this->data['creation_time']; } public function getUploadTime(): int { - return (int) $this->data['upload_time']; + return (int)$this->data['upload_time']; } public function getParentId(): int { diff --git a/lib/private/Files/FilenameValidator.php b/lib/private/Files/FilenameValidator.php index b1ce8e02b13..fde45068df7 100644 --- a/lib/private/Files/FilenameValidator.php +++ b/lib/private/Files/FilenameValidator.php @@ -25,6 +25,8 @@ use Psr\Log\LoggerInterface; */ class FilenameValidator implements IFilenameValidator { + public const INVALID_FILE_TYPE = 100; + private IL10N $l10n; /** @@ -198,9 +200,7 @@ class FilenameValidator implements IFilenameValidator { } } - if ($this->isForbidden($filename)) { - throw new ReservedWordException(); - } + $this->checkForbiddenName($filename); $this->checkForbiddenExtension($filename); @@ -227,18 +227,25 @@ class FilenameValidator implements IFilenameValidator { return true; } + // Filename is not forbidden + return false; + } + + protected function checkForbiddenName($filename): void { + if ($this->isForbidden($filename)) { + throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden file or folder name.', [$filename])); + } + // Check for forbidden basenames - basenames are the part of the file until the first dot // (except if the dot is the first character as this is then part of the basename "hidden files") $basename = substr($filename, 0, strpos($filename, '.', 1) ?: null); $forbiddenNames = $this->getForbiddenBasenames(); if (in_array($basename, $forbiddenNames)) { - return true; + throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden prefix for file or folder names.', [$filename])); } - - // Filename is not forbidden - return false; } + /** * Check if a filename contains any of the forbidden characters * @param string $filename @@ -252,7 +259,7 @@ class FilenameValidator implements IFilenameValidator { foreach ($this->getForbiddenCharacters() as $char) { if (str_contains($filename, $char)) { - throw new InvalidCharacterInPathException($this->l10n->t('Invalid character "%1$s" in filename', [$char])); + throw new InvalidCharacterInPathException($this->l10n->t('"%1$s" is not allowed inside a file or folder name.', [$char])); } } } @@ -264,11 +271,15 @@ class FilenameValidator implements IFilenameValidator { */ protected function checkForbiddenExtension(string $filename): void { $filename = mb_strtolower($filename); - // Check for forbidden filename exten<sions + // Check for forbidden filename extensions $forbiddenExtensions = $this->getForbiddenExtensions(); foreach ($forbiddenExtensions as $extension) { if (str_ends_with($filename, $extension)) { - throw new InvalidPathException($this->l10n->t('Invalid filename extension "%1$s"', [$extension])); + if (str_starts_with($extension, '.')) { + throw new InvalidPathException($this->l10n->t('"%1$s" is a forbidden file type.', [$extension]), self::INVALID_FILE_TYPE); + } else { + throw new InvalidPathException($this->l10n->t('Filenames must not end with "%1$s".', [$extension])); + } } } } diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index db7420c3c4c..48c069de0b9 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -674,7 +674,7 @@ class Filesystem { * * @param string $path * @param bool|string $includeMountPoints whether to add mountpoint sizes, - * defaults to true + * defaults to true * @return \OC\Files\FileInfo|false False if file does not exist */ public static function getFileInfo($path, $includeMountPoints = true) { diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php index c2267af3c96..d118021afa2 100644 --- a/lib/private/Files/Mount/Manager.php +++ b/lib/private/Files/Mount/Manager.php @@ -84,7 +84,7 @@ class Manager implements IMountManager { 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"); + throw new \Exception('No mounts even after explicitly setting up the root mounts'); } } @@ -104,7 +104,7 @@ class Manager implements IMountManager { } } - throw new NotFoundException("No mount for path " . $path . " existing mounts (" . count($this->mounts) ."): " . implode(",", array_keys($this->mounts))); + throw new NotFoundException('No mount for path ' . $path . ' existing mounts (' . count($this->mounts) .'): ' . implode(',', array_keys($this->mounts))); } /** diff --git a/lib/private/Files/Mount/RootMountProvider.php b/lib/private/Files/Mount/RootMountProvider.php index 7ad7a7f059c..86f8188978f 100644 --- a/lib/private/Files/Mount/RootMountProvider.php +++ b/lib/private/Files/Mount/RootMountProvider.php @@ -56,7 +56,7 @@ class RootMountProvider implements IRootMountProvider { } private function getLocalRootMount(IStorageFactory $loader): MountPoint { - $configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data"); + $configDataDirectory = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data'); return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class); } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 9936487abb3..ca256b09d33 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -422,7 +422,7 @@ class Folder extends Node implements \OCP\Files\Folder { $filterNonRecentFiles = new SearchComparison( ISearchComparison::COMPARE_GREATER_THAN, 'mtime', - strtotime("-2 week") + strtotime('-2 week') ); if ($offset === 0 && $limit <= 100) { $query = new SearchQuery( diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php index f9908b9b7b6..77479c2fa5e 100644 --- a/lib/private/Files/Node/LazyUserFolder.php +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -59,7 +59,7 @@ class LazyUserFolder extends LazyFolder { } $mountPoint = $this->mountManager->find('/' . $this->user->getUID()); if (is_null($mountPoint)) { - throw new \Exception("No mountpoint for user folder"); + throw new \Exception('No mountpoint for user folder'); } return $mountPoint; } diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php index e7f533e73a9..5dbdc4054bf 100644 --- a/lib/private/Files/Node/Node.php +++ b/lib/private/Files/Node/Node.php @@ -158,7 +158,7 @@ class Node implements INode { public function getStorage() { $storage = $this->getMountPoint()->getStorage(); if (!$storage) { - throw new \Exception("No storage for node"); + throw new \Exception('No storage for node'); } return $storage; } @@ -432,11 +432,14 @@ class Node implements INode { $targetPath = $this->normalizePath($targetPath); $parent = $this->root->get(dirname($targetPath)); if ( - $parent instanceof Folder and - $this->isValidPath($targetPath) and - ( - $parent->isCreatable() || - ($parent->getInternalPath() === '' && $parent->getMountPoint() instanceof MoveableMount) + ($parent instanceof Folder) + && $this->isValidPath($targetPath) + && ( + $parent->isCreatable() + || ( + $parent->getInternalPath() === '' + && ($parent->getMountPoint() instanceof MoveableMount) + ) ) ) { $nonExisting = $this->createNonExistingNode($targetPath); diff --git a/lib/private/Files/Node/NonExistingFile.php b/lib/private/Files/Node/NonExistingFile.php index d154876432e..66ec2e6c040 100644 --- a/lib/private/Files/Node/NonExistingFile.php +++ b/lib/private/Files/Node/NonExistingFile.php @@ -38,6 +38,14 @@ class NonExistingFile extends File { } } + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + public function stat() { throw new NotFoundException(); } diff --git a/lib/private/Files/Node/NonExistingFolder.php b/lib/private/Files/Node/NonExistingFolder.php index 5650c99fe73..4489fdaf010 100644 --- a/lib/private/Files/Node/NonExistingFolder.php +++ b/lib/private/Files/Node/NonExistingFolder.php @@ -38,6 +38,14 @@ class NonExistingFolder extends Folder { } } + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + public function stat() { throw new NotFoundException(); } diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 5fb4013cfc9..416adc7f374 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -459,7 +459,7 @@ class Root extends Folder implements IRootFolder { if ($folder instanceof Folder) { return $folder->getByIdInRootMount($id); } else { - throw new \Exception("getByIdInPath with non folder"); + throw new \Exception('getByIdInPath with non folder'); } } return []; diff --git a/lib/private/Files/ObjectStore/Azure.php b/lib/private/Files/ObjectStore/Azure.php index 55400d4131c..2dacdac1f8d 100644 --- a/lib/private/Files/ObjectStore/Azure.php +++ b/lib/private/Files/ObjectStore/Azure.php @@ -21,7 +21,7 @@ class Azure implements IObjectStore { private $blobClient = null; /** @var string|null */ private $endpoint = null; - /** @var bool */ + /** @var bool */ private $autoCreate = false; /** @@ -45,7 +45,7 @@ class Azure implements IObjectStore { private function getBlobClient() { if (!$this->blobClient) { $protocol = $this->endpoint ? substr($this->endpoint, 0, strpos($this->endpoint, ':')) : 'https'; - $connectionString = "DefaultEndpointsProtocol=" . $protocol . ";AccountName=" . $this->accountName . ";AccountKey=" . $this->accountKey; + $connectionString = 'DefaultEndpointsProtocol=' . $protocol . ';AccountName=' . $this->accountName . ';AccountKey=' . $this->accountKey; if ($this->endpoint) { $connectionString .= ';BlobEndpoint=' . $this->endpoint; } diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 389f744eab4..228fc516677 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -594,6 +594,31 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } + public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool { + $sourceCache = $sourceStorage->getCache(); + if (!$sourceCacheEntry) { + $sourceCacheEntry = $sourceCache->get($sourceInternalPath); + } + if ($sourceCacheEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) { + foreach ($sourceCache->getFolderContents($sourceInternalPath) as $child) { + $this->moveFromStorage($sourceStorage, $child->getPath(), $targetInternalPath . '/' . $child->getName()); + } + $sourceStorage->rmdir($sourceInternalPath); + } else { + // move the cache entry before the contents so that we have the correct fileid/urn for the target + $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath); + try { + $this->writeStream($targetInternalPath, $sourceStorage->fopen($sourceInternalPath, 'r'), $sourceCacheEntry->getSize()); + } catch (\Exception $e) { + // restore the cache entry + $sourceCache->moveFromCache($this->getCache(), $targetInternalPath, $sourceInternalPath); + throw $e; + } + $sourceStorage->unlink($sourceInternalPath); + } + return true; + } + public function copy($source, $target) { $source = $this->normalizePath($source); $target = $this->normalizePath($target); @@ -632,7 +657,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil $sourceUrn = $this->getURN($sourceEntry->getId()); if (!$cache instanceof Cache) { - throw new \Exception("Invalid source cache for object store copy"); + throw new \Exception('Invalid source cache for object store copy'); } $targetId = $cache->copyFromCache($cache, $sourceEntry, $to); diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index 0506eb35353..9de85f00620 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -28,7 +28,7 @@ trait S3ConnectionTrait { protected function parseParams($params) { if (empty($params['bucket'])) { - throw new \Exception("Bucket has to be configured."); + throw new \Exception('Bucket has to be configured.'); } $this->id = 'amazon::' . $params['bucket']; @@ -132,7 +132,7 @@ trait S3ConnectionTrait { try { $logger->info('Bucket "' . $this->bucket . '" does not exist - creating it.', ['app' => 'objectstore']); if (!$this->connection::isBucketDnsCompatible($this->bucket)) { - throw new \Exception("The bucket will not be created because the name is not dns compatible, please correct it: " . $this->bucket); + throw new \Exception('The bucket will not be created because the name is not dns compatible, please correct it: ' . $this->bucket); } $this->connection->createBucket(['Bucket' => $this->bucket]); $this->testTimeout(); @@ -197,7 +197,7 @@ trait S3ConnectionTrait { } protected function getCertificateBundlePath(): ?string { - if ((int)($this->params['use_nextcloud_bundle'] ?? "0")) { + if ((int)($this->params['use_nextcloud_bundle'] ?? '0')) { // since we store the certificate bundles on the primary storage, we can't get the bundle while setting up the primary storage if (!isset($this->params['primary_storage'])) { /** @var ICertificateManager $certManager */ diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 5d00c184ca7..2e625033751 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -127,7 +127,7 @@ trait S3ObjectTrait { if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) { $this->getConnection()->abortMultipartUpload($uploadInfo); } - throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway("Error while uploading to S3 bucket", 0, $e); + throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway('Error while uploading to S3 bucket', 0, $e); } } @@ -144,7 +144,7 @@ trait S3ObjectTrait { // ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream // so the optimisation does not apply - $buffer = new Psr7\Stream(fopen("php://memory", 'rwb+')); + $buffer = new Psr7\Stream(fopen('php://memory', 'rwb+')); Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit); $buffer->seek(0); if ($buffer->getSize() < $this->putSizeLimit) { @@ -183,14 +183,14 @@ trait S3ObjectTrait { if ($this->useMultipartCopy && $size > $this->copySizeLimit) { $copy = new MultipartCopy($this->getConnection(), [ - "source_bucket" => $this->getBucket(), - "source_key" => $from + '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 + 'bucket' => $this->getBucket(), + 'key' => $to, + 'acl' => 'private', + 'params' => $this->getSSECParameters() + $this->getSSECParameters(true), + 'source_metadata' => $sourceMetadata ], $options)); $copy->copy(); } else { diff --git a/lib/private/Files/ObjectStore/S3Signature.php b/lib/private/Files/ObjectStore/S3Signature.php index e3a522b6581..4e9784ee81a 100644 --- a/lib/private/Files/ObjectStore/S3Signature.php +++ b/lib/private/Files/ObjectStore/S3Signature.php @@ -99,7 +99,7 @@ class S3Signature implements SignatureInterface { } /** - * @param RequestInterface $request + * @param RequestInterface $request * @param CredentialsInterface $creds * * @return RequestInterface diff --git a/lib/private/Files/ObjectStore/SwiftFactory.php b/lib/private/Files/ObjectStore/SwiftFactory.php index 0db5c9762d2..a2e22e45de2 100644 --- a/lib/private/Files/ObjectStore/SwiftFactory.php +++ b/lib/private/Files/ObjectStore/SwiftFactory.php @@ -170,7 +170,7 @@ class SwiftFactory { try { /** @var \OpenStack\Identity\v2\Models\Token $token */ $token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']); - $now = new \DateTimeImmutable("now"); + $now = new \DateTimeImmutable('now'); if ($token->expires > $now) { $hasValidCachedToken = true; $this->params['v2cachedToken'] = $token; diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php index e7cb031da3e..3c8711facd8 100644 --- a/lib/private/Files/Search/SearchQuery.php +++ b/lib/private/Files/Search/SearchQuery.php @@ -11,13 +11,13 @@ use OCP\Files\Search\ISearchQuery; use OCP\IUser; class SearchQuery implements ISearchQuery { - /** @var ISearchOperator */ + /** @var ISearchOperator */ private $searchOperation; - /** @var integer */ + /** @var integer */ private $limit; - /** @var integer */ + /** @var integer */ private $offset; - /** @var ISearchOrder[] */ + /** @var ISearchOrder[] */ private $order; /** @var ?IUser */ private $user; diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 53befe57e36..e74dd1042d6 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -107,8 +107,9 @@ class SetupManager { $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - if ($mount->getOptions() && $storage->instanceOfStorage(Common::class)) { - $storage->setMountOptions($mount->getOptions()); + if ($storage->instanceOfStorage(Common::class)) { + $options = array_merge($mount->getOptions(), ['mount_point' => $mountPoint]); + $storage->setMountOptions($options); } return $storage; }); @@ -381,7 +382,7 @@ class SetupManager { } // for the user's home folder, and includes children we need everything always - if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) { + if (rtrim($path) === '/' . $user->getUID() . '/files' && $includeChildren) { $this->setupForUser($user); return; } @@ -411,7 +412,7 @@ class SetupManager { $setupProviders[] = $cachedMount->getMountProvider(); $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]); } else { - $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); + $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup'); $this->eventLogger->end('fs:setup:user:path:find'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); @@ -428,7 +429,7 @@ class SetupManager { }, false); if ($needsFullSetup) { - $this->logger->debug("mount has no provider set, performing full setup"); + $this->logger->debug('mount has no provider set, performing full setup'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); return; @@ -490,7 +491,7 @@ class SetupManager { return; } - $this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers)); + $this->eventLogger->start('fs:setup:user:providers', 'Setup filesystem for ' . implode(', ', $providers)); $this->oneTimeUserSetup($user); diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index a8f8c05ab54..9541ba7c746 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -13,6 +13,7 @@ use OC\Files\Cache\Propagator; use OC\Files\Cache\Scanner; use OC\Files\Cache\Updater; use OC\Files\Cache\Watcher; +use OC\Files\FilenameValidator; use OC\Files\Filesystem; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\Wrapper; @@ -159,7 +160,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } public function file_get_contents($path) { - $handle = $this->fopen($path, "r"); + $handle = $this->fopen($path, 'r'); if (!$handle) { return false; } @@ -169,7 +170,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { } public function file_put_contents($path, $data) { - $handle = $this->fopen($path, "w"); + $handle = $this->fopen($path, 'w'); if (!$handle) { return false; } @@ -191,7 +192,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { $this->remove($target); $dir = $this->opendir($source); $this->mkdir($target); - while ($file = readdir($dir)) { + while (($file = readdir($dir)) !== false) { if (!Filesystem::isIgnoredDir($file)) { if (!$this->copy($source . '/' . $file, $target . '/' . $file)) { closedir($dir); @@ -430,11 +431,11 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { if ($this->stat('')) { return true; } - \OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed"); + \OC::$server->get(LoggerInterface::class)->info('External storage not available: stat() failed'); return false; } catch (\Exception $e) { \OC::$server->get(LoggerInterface::class)->warning( - "External storage not available: " . $e->getMessage(), + 'External storage not available: ' . $e->getMessage(), ['exception' => $e] ); return false; @@ -494,7 +495,18 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { $this->getFilenameValidator() ->validateFilename($fileName); - // NOTE: $path will remain unverified for now + // verify also the path is valid + if ($path && $path !== '/' && $path !== '.') { + try { + $this->verifyPath(dirname($path), basename($path)); + } catch (InvalidPathException $e) { + // Ignore invalid file type exceptions on directories + if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) { + $l = \OCP\Util::getL10N('lib'); + throw new InvalidPathException($l->t('Invalid parent path'), previous: $e); + } + } + } } /** @@ -816,7 +828,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { try { [$count, $result] = \OC_Helper::streamCopy($stream, $target); if (!$result) { - throw new GenericFileException("Failed to copy stream"); + throw new GenericFileException('Failed to copy stream'); } } finally { fclose($target); diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 63ef1399a69..7abc0ccc182 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -90,9 +90,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 (str_starts_with($host, "https://")) { + if (str_starts_with($host, 'https://')) { $host = substr($host, 8); - } elseif (str_starts_with($host, "http://")) { + } elseif (str_starts_with($host, 'http://')) { $host = substr($host, 7); } $this->host = $host; @@ -162,13 +162,13 @@ class DAV extends Common { $lastRequestStart = 0; $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) { - $this->logger->debug("sending dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl(), ['app' => 'dav']); + $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']); $lastRequestStart = microtime(true); - $this->eventLogger->start('fs:storage:dav:request', "Sending dav request to external storage"); + $this->eventLogger->start('fs:storage:dav:request', 'Sending dav request to external storage'); }); $this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) { $elapsed = microtime(true) - $lastRequestStart; - $this->logger->debug("dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl() . " took " . round($elapsed * 1000, 1) . "ms", ['app' => 'dav']); + $this->logger->debug('dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl() . ' took ' . round($elapsed * 1000, 1) . 'ms', ['app' => 'dav']); $this->eventLogger->end('fs:storage:dav:request'); }); } @@ -283,11 +283,11 @@ class DAV extends Common { return false; } $responseType = []; - if (isset($response["{DAV:}resourcetype"])) { + if (isset($response['{DAV:}resourcetype'])) { /** @var ResourceType[] $response */ - $responseType = $response["{DAV:}resourcetype"]->getValue(); + $responseType = $response['{DAV:}resourcetype']->getValue(); } - return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; + return (count($responseType) > 0 and $responseType[0] == '{DAV:}collection') ? 'dir' : 'file'; } catch (\Exception $e) { $this->convertException($e, $path); } @@ -582,11 +582,11 @@ class DAV extends Common { } $responseType = []; - if (isset($response["{DAV:}resourcetype"])) { + if (isset($response['{DAV:}resourcetype'])) { /** @var ResourceType[] $response */ - $responseType = $response["{DAV:}resourcetype"]->getValue(); + $responseType = $response['{DAV:}resourcetype']->getValue(); } - $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; + $type = (count($responseType) > 0 and $responseType[0] == '{DAV:}collection') ? 'dir' : 'file'; if ($type === 'dir') { $mimeType = 'httpd/unix-directory'; } elseif (isset($response['{DAV:}getcontenttype'])) { @@ -832,9 +832,9 @@ class DAV extends Common { * @param string $path optional path from the operation * * @throws StorageInvalidException if the storage is invalid, for example - * when the authentication expired or is invalid + * when the authentication expired or is invalid * @throws StorageNotAvailableException if the storage is not available, - * which might be temporary + * which might be temporary * @throws ForbiddenException if the action is not allowed */ protected function convertException(Exception $e, $path = '') { diff --git a/lib/private/Files/Storage/Home.php b/lib/private/Files/Storage/Home.php index 9a336d2efcc..a8d1f82b987 100644 --- a/lib/private/Files/Storage/Home.php +++ b/lib/private/Files/Storage/Home.php @@ -28,7 +28,7 @@ class Home extends Local implements \OCP\Files\IHomeStorage { * Construct a Home storage instance * * @param array $arguments array with "user" containing the - * storage owner + * storage owner */ public function __construct($arguments) { $this->user = $arguments['user']; diff --git a/lib/private/Files/Storage/PolyFill/CopyDirectory.php b/lib/private/Files/Storage/PolyFill/CopyDirectory.php index 4b3e367da78..5fe396d97e1 100644 --- a/lib/private/Files/Storage/PolyFill/CopyDirectory.php +++ b/lib/private/Files/Storage/PolyFill/CopyDirectory.php @@ -70,7 +70,7 @@ trait CopyDirectory { protected function copyRecursive($source, $target) { $dh = $this->opendir($source); $result = true; - while ($file = readdir($dh)) { + while (($file = readdir($dh)) !== false) { if (!\OC\Files\Filesystem::isIgnoredDir($file)) { if ($this->is_dir($source . '/' . $file)) { $this->mkdir($target . '/' . $file); diff --git a/lib/private/Files/Storage/StorageFactory.php b/lib/private/Files/Storage/StorageFactory.php index 18fca1d28e3..612592e2d3a 100644 --- a/lib/private/Files/Storage/StorageFactory.php +++ b/lib/private/Files/Storage/StorageFactory.php @@ -26,7 +26,7 @@ class StorageFactory implements IStorageFactory { * @param int $priority wrappers with the lower priority are applied last (meaning they get called first) * @param \OCP\Files\Mount\IMountPoint[] $existingMounts existing mount points to apply the wrapper to * @return bool true if the wrapper was added, false if there was already a wrapper with this - * name registered + * name registered */ public function addStorageWrapper($wrapperName, $callback, $priority = 50, $existingMounts = []) { if (isset($this->storageWrappers[$wrapperName])) { diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 7d45c356bfe..1ead1c342b0 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -14,6 +14,7 @@ use OC\Files\Cache\CacheEntry; use OC\Files\Filesystem; use OC\Files\Mount\Manager; use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\Storage\Common; use OC\Files\Storage\LocalTempFileTrait; use OC\Memcache\ArrayCache; use OCP\Cache\CappedMemoryCache; @@ -64,7 +65,7 @@ class Encryption extends Wrapper { /** @var array remember for which path we execute the repair step to avoid recursions */ private $fixUnencryptedSizeOf = []; - /** @var ArrayCache */ + /** @var ArrayCache */ private $arrayCache; /** @var CappedMemoryCache<bool> */ @@ -203,7 +204,7 @@ class Encryption extends Wrapper { $encryptionModule = $this->getEncryptionModule($path); if ($encryptionModule) { - $handle = $this->fopen($path, "r"); + $handle = $this->fopen($path, 'r'); if (!$handle) { return false; } @@ -776,9 +777,8 @@ class Encryption extends Wrapper { // first copy the keys that we reuse the existing file key on the target location // and don't create a new one which would break versions for example. - $mount = $this->mountManager->findByStorageId($sourceStorage->getId()); - if (count($mount) >= 1) { - $mountPoint = $mount[0]->getMountPoint(); + if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) { + $mountPoint = $sourceStorage->getMountOption('mount_point'); $source = $mountPoint . '/' . $sourceInternalPath; $target = $this->getFullPath($targetInternalPath); $this->copyKeys($source, $target); diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 8c6799fdd2d..b642c438266 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -40,7 +40,7 @@ class Quota extends Wrapper { if ($this->quota === null) { $quotaCallback = $this->quotaCallback; if ($quotaCallback === null) { - throw new \Exception("No quota or quota callback provider"); + throw new \Exception('No quota or quota callback provider'); } $this->quota = $quotaCallback(); } diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index 29acc9ad1c2..f8aa9d963dc 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -40,7 +40,7 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea */ public function getWrapperStorage() { if (!$this->storage) { - $message = "storage wrapper " . get_class($this) . " doesn't have a wrapped storage set"; + $message = 'storage wrapper ' . get_class($this) . " doesn't have a wrapped storage set"; $logger = Server::get(LoggerInterface::class); $logger->error($message); $this->storage = new FailedStorage(['exception' => new \Exception($message)]); diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 32c0021cd23..8f08f925da0 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -55,7 +55,7 @@ class Encryption extends Wrapper { /** @var string */ protected $fullPath; - /** @var bool */ + /** @var bool */ protected $signed; /** diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index 02ed1470fbd..5ed04ed9066 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -90,7 +90,7 @@ class SeekableHttpStream implements File { continue 2; } } - throw new \Exception("Failed to get source stream from stream wrapper of " . get_class($responseHead)); + throw new \Exception('Failed to get source stream from stream wrapper of ' . get_class($responseHead)); } $rangeHeaders = array_values(array_filter($responseHead, function ($v) { diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php index 247acf0141a..407df59b2e2 100644 --- a/lib/private/Files/Type/Loader.php +++ b/lib/private/Files/Type/Loader.php @@ -115,7 +115,7 @@ class Loader implements IMimeTypeLoader { throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype"); } - $mimetypeId = (int) $id; + $mimetypeId = (int)$id; } $this->mimetypes[$mimetypeId] = $mimetype; @@ -136,8 +136,8 @@ class Loader implements IMimeTypeLoader { $result->closeCursor(); foreach ($results as $row) { - $this->mimetypes[(int) $row['id']] = $row['mimetype']; - $this->mimetypeIds[$row['mimetype']] = (int) $row['id']; + $this->mimetypes[(int)$row['id']] = $row['mimetype']; + $this->mimetypeIds[$row['mimetype']] = (int)$row['id']; } } diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 0e5e433ccb6..64c7f744dd9 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -221,7 +221,7 @@ class View { $relPath = '/' . $pathParts[3]; $this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true); \OC_Hook::emit( - Filesystem::CLASSNAME, "umount", + Filesystem::CLASSNAME, 'umount', [Filesystem::signal_param_path => $relPath] ); $this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true); @@ -229,7 +229,7 @@ class View { $this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true); if ($result) { \OC_Hook::emit( - Filesystem::CLASSNAME, "post_umount", + Filesystem::CLASSNAME, 'post_umount', [Filesystem::signal_param_path => $relPath] ); } @@ -697,7 +697,7 @@ class View { $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target)); if (str_starts_with($absolutePath2, $absolutePath1 . '/')) { - throw new ForbiddenException("Moving a folder into a child folder is forbidden", false); + throw new ForbiddenException('Moving a folder into a child folder is forbidden', false); } $targetParts = explode('/', $absolutePath2); @@ -1330,7 +1330,7 @@ class View { * * @param string $path * @param bool|string $includeMountPoints true to add mountpoint sizes, - * 'ext' to add only ext storage mount point sizes. Defaults to true. + * 'ext' to add only ext storage mount point sizes. Defaults to true. * @return \OC\Files\FileInfo|false False if file does not exist */ public function getFileInfo($path, $includeMountPoints = true) { @@ -1826,28 +1826,39 @@ class View { /** * @param string $path * @param string $fileName + * @param bool $readonly Check only if the path is allowed for read-only access * @throws InvalidPathException */ - public function verifyPath($path, $fileName): void { + public function verifyPath($path, $fileName, $readonly = false): void { // All of the view's functions disallow '..' in the path so we can short cut if the path is invalid if (!Filesystem::isValidPath($path ?: '/')) { $l = \OCP\Util::getL10N('lib'); throw new InvalidPathException($l->t('Path contains invalid segments')); } + // Short cut for read-only validation + if ($readonly) { + $validator = \OCP\Server::get(FilenameValidator::class); + if ($validator->isForbidden($fileName)) { + $l = \OCP\Util::getL10N('lib'); + throw new InvalidPathException($l->t('Filename is a reserved word')); + } + return; + } + try { /** @type \OCP\Files\Storage $storage */ [$storage, $internalPath] = $this->resolvePath($path); $storage->verifyPath($internalPath, $fileName); } catch (ReservedWordException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name is a reserved word')); + throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename is a reserved word')); } catch (InvalidCharacterInPathException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name contains at least one invalid character')); + throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename contains at least one invalid character')); } catch (FileNameTooLongException $ex) { $l = \OCP\Util::getL10N('lib'); - throw new InvalidPathException($l->t('File name is too long')); + throw new InvalidPathException($l->t('Filename is too long')); } catch (InvalidDirectoryException $ex) { $l = \OCP\Util::getL10N('lib'); throw new InvalidPathException($l->t('Dot files are not allowed')); @@ -1889,7 +1900,7 @@ class View { * * @param string $absolutePath absolute path * @param bool $useParentMount true to return parent mount instead of whatever - * is mounted directly on the given path, false otherwise + * is mounted directly on the given path, false otherwise * @return IMountPoint mount point for which to apply locks */ private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint { @@ -2103,7 +2114,7 @@ class View { * @param string $absolutePath absolute path which is under "files" * * @return string path relative to "files" with trimmed slashes or null - * if the path was NOT relative to files + * if the path was NOT relative to files * * @throws \InvalidArgumentException if the given path was not under "files" * @since 8.1.0 diff --git a/lib/private/FilesMetadata/Service/IndexRequestService.php b/lib/private/FilesMetadata/Service/IndexRequestService.php index 32248ff5c24..b50fb378325 100644 --- a/lib/private/FilesMetadata/Service/IndexRequestService.php +++ b/lib/private/FilesMetadata/Service/IndexRequestService.php @@ -82,9 +82,9 @@ class IndexRequestService { 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)); + ->setValue('meta_key', $qb->createNamedParameter($key)) + ->setValue('meta_value_string', $qb->createNamedParameter($value)) + ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); $qb->executeStatement(); } @@ -100,9 +100,9 @@ class IndexRequestService { 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)); + ->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(); } @@ -118,9 +118,9 @@ class IndexRequestService { 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)); + ->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(); } @@ -167,7 +167,7 @@ class IndexRequestService { $qb = $this->dbConnection->getQueryBuilder(); $expr = $qb->expr(); $qb->delete(self::TABLE_METADATA_INDEX) - ->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + ->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); if ($key !== '') { $qb->andWhere($expr->eq('meta_key', $qb->createNamedParameter($key))); diff --git a/lib/private/FilesMetadata/Service/MetadataRequestService.php b/lib/private/FilesMetadata/Service/MetadataRequestService.php index 08982a2a659..b58912b0216 100644 --- a/lib/private/FilesMetadata/Service/MetadataRequestService.php +++ b/lib/private/FilesMetadata/Service/MetadataRequestService.php @@ -38,10 +38,10 @@ class MetadataRequestService { 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()')); + ->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(); } @@ -92,7 +92,7 @@ class MetadataRequestService { $list = []; $result = $qb->executeQuery(); while ($data = $result->fetch()) { - $fileId = (int) $data['file_id']; + $fileId = (int)$data['file_id']; $metadata = new FilesMetadata($fileId); try { $metadata->importFromDatabase($data); @@ -117,7 +117,7 @@ class MetadataRequestService { 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))); + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); $qb->executeStatement(); } @@ -134,15 +134,15 @@ class MetadataRequestService { $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())) - ) - ); + ->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(); } diff --git a/lib/private/FullTextSearch/Model/IndexDocument.php b/lib/private/FullTextSearch/Model/IndexDocument.php index 6f9da9416fa..8bd20bad1e0 100644 --- a/lib/private/FullTextSearch/Model/IndexDocument.php +++ b/lib/private/FullTextSearch/Model/IndexDocument.php @@ -402,7 +402,7 @@ class IndexDocument implements IIndexDocument, JsonSerializable { return $this; } - $this->hash = hash("md5", $this->getContent()); + $this->hash = hash('md5', $this->getContent()); return $this; } @@ -556,9 +556,9 @@ class IndexDocument implements IIndexDocument, JsonSerializable { * @since 16.0.0 */ private function cleanExcerpt(string $excerpt): string { - $excerpt = str_replace("\\n", ' ', $excerpt); - $excerpt = str_replace("\\r", ' ', $excerpt); - $excerpt = str_replace("\\t", ' ', $excerpt); + $excerpt = str_replace('\\n', ' ', $excerpt); + $excerpt = str_replace('\\r', ' ', $excerpt); + $excerpt = str_replace('\\t', ' ', $excerpt); $excerpt = str_replace("\n", ' ', $excerpt); $excerpt = str_replace("\r", ' ', $excerpt); $excerpt = str_replace("\t", ' ', $excerpt); diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php index 5827a31172d..72cd0ea4a91 100644 --- a/lib/private/Group/Database.php +++ b/lib/private/Group/Database.php @@ -325,8 +325,8 @@ class Database extends ABackend implements $qb = $this->dbConn->getQueryBuilder(); $qb->select('gid', 'displayname') - ->from('groups') - ->where($qb->expr()->in('gid', $qb->createParameter('ids'))); + ->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(); @@ -488,7 +488,7 @@ class Database extends ABackend implements $displayName = $result->fetchOne(); $result->closeCursor(); - return (string) $displayName; + return (string)$displayName; } public function getGroupDetails(string $gid): array { diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index dcda7c29bb5..147c5baf543 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -30,7 +30,7 @@ use OCP\IUser; use OCP\IUserManager; class Group implements IGroup { - /** @var null|string */ + /** @var null|string */ protected $displayName; /** @var string */ @@ -46,7 +46,7 @@ class Group implements IGroup { private $backends; /** @var IEventDispatcher */ private $dispatcher; - /** @var \OC\User\Manager|IUserManager */ + /** @var \OC\User\Manager|IUserManager */ private $userManager; /** @var PublicEmitter */ private $emitter; diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php index cad8e76fbbd..d18c7796805 100644 --- a/lib/private/Group/Manager.php +++ b/lib/private/Group/Manager.php @@ -452,7 +452,7 @@ class Manager extends PublicEmitter implements IGroupManager { $matchingUsers = []; foreach ($groupUsers as $groupUser) { - $matchingUsers[(string) $groupUser->getUID()] = $groupUser->getDisplayName(); + $matchingUsers[(string)$groupUser->getUID()] = $groupUser->getDisplayName(); } return $matchingUsers; } diff --git a/lib/private/Group/MetaData.php b/lib/private/Group/MetaData.php index da553c89a7b..fe0d931cb09 100644 --- a/lib/private/Group/MetaData.php +++ b/lib/private/Group/MetaData.php @@ -41,7 +41,7 @@ class MetaData { * [0] array containing meta data about admin groups * [1] array containing meta data about unprivileged groups * @param string $groupSearch only effective when instance was created with - * isAdmin being true + * isAdmin being true * @param string $userSearch the pattern users are search for */ public function get(string $groupSearch = '', string $userSearch = ''): array { diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php index 7cadf3fdf6e..0b72522c218 100644 --- a/lib/private/Http/Client/Client.php +++ b/lib/private/Http/Client/Client.php @@ -176,27 +176,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'query' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'query' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -212,22 +212,22 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -242,27 +242,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -283,27 +283,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -318,27 +318,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -353,27 +353,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -388,27 +388,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -440,27 +440,27 @@ class Client implements IClient { * @param string $method The HTTP method to use * @param string $uri * @param array $options Array such as - * 'query' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'query' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IResponse * @throws \Exception If the request could not get completed */ @@ -483,27 +483,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'query' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'query' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function getAsync(string $uri, array $options = []): IPromise { @@ -517,22 +517,22 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function headAsync(string $uri, array $options = []): IPromise { @@ -546,27 +546,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function postAsync(string $uri, array $options = []): IPromise { @@ -585,27 +585,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function putAsync(string $uri, array $options = []): IPromise { @@ -619,27 +619,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function deleteAsync(string $uri, array $options = []): IPromise { @@ -653,27 +653,27 @@ class Client implements IClient { * * @param string $uri * @param array $options Array such as - * 'body' => [ - * 'field' => 'abc', - * 'other_field' => '123', - * 'file_name' => fopen('/path/to/file', 'r'), - * ], - * 'headers' => [ - * 'foo' => 'bar', - * ], - * 'cookies' => [ - * 'foo' => 'bar', - * ], - * 'allow_redirects' => [ - * 'max' => 10, // allow at most 10 redirects. - * 'strict' => true, // use "strict" RFC compliant redirects. - * 'referer' => true, // add a Referer header - * 'protocols' => ['https'] // only allow https URLs - * ], - * 'sink' => '/path/to/file', // save to a file or a stream - * 'verify' => true, // bool or string to CA file - * 'debug' => true, - * 'timeout' => 5, + * 'body' => [ + * 'field' => 'abc', + * 'other_field' => '123', + * 'file_name' => fopen('/path/to/file', 'r'), + * ], + * 'headers' => [ + * 'foo' => 'bar', + * ], + * 'cookies' => [ + * 'foo' => 'bar', + * ], + * 'allow_redirects' => [ + * 'max' => 10, // allow at most 10 redirects. + * 'strict' => true, // use "strict" RFC compliant redirects. + * 'referer' => true, // add a Referer header + * 'protocols' => ['https'] // only allow https URLs + * ], + * 'sink' => '/path/to/file', // save to a file or a stream + * 'verify' => true, // bool or string to CA file + * 'debug' => true, + * 'timeout' => 5, * @return IPromise */ public function optionsAsync(string $uri, array $options = []): IPromise { diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php index 9a170be8752..b719f3d369d 100644 --- a/lib/private/Http/Client/ClientService.php +++ b/lib/private/Http/Client/ClientService.php @@ -61,7 +61,7 @@ class ClientService implements IClientService { $stack->push($this->dnsPinMiddleware->addDnsPinning()); } $stack->push(Middleware::tap(function (RequestInterface $request) { - $this->eventLogger->start('http:request', $request->getMethod() . " request to " . $request->getRequestTarget()); + $this->eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget()); }, function () { $this->eventLogger->end('http:request'); }), 'event logger'); diff --git a/lib/private/Http/Client/GuzzlePromiseAdapter.php b/lib/private/Http/Client/GuzzlePromiseAdapter.php index dc8be9bd2e0..03a9ed9a599 100644 --- a/lib/private/Http/Client/GuzzlePromiseAdapter.php +++ b/lib/private/Http/Client/GuzzlePromiseAdapter.php @@ -36,7 +36,7 @@ class GuzzlePromiseAdapter implements IPromise { * a new promise resolving to the return value of the called handler. * * @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument - * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument + * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument * * @return IPromise * @since 28.0.0 @@ -115,7 +115,7 @@ class GuzzlePromiseAdapter implements IPromise { * @return mixed * * @throws LogicException if the promise has no wait function or if the - * promise does not settle after waiting. + * promise does not settle after waiting. * @since 28.0.0 */ public function wait(bool $unwrap = true): mixed { diff --git a/lib/private/Http/Client/NegativeDnsCache.php b/lib/private/Http/Client/NegativeDnsCache.php index d5e32fa7c2d..ca8a477d6be 100644 --- a/lib/private/Http/Client/NegativeDnsCache.php +++ b/lib/private/Http/Client/NegativeDnsCache.php @@ -20,11 +20,11 @@ class NegativeDnsCache { } private function createCacheKey(string $domain, int $type) : string { - return $domain . "-" . (string)$type; + return $domain . '-' . (string)$type; } public function setNegativeCacheForDnsType(string $domain, int $type, int $ttl) : void { - $this->cache->set($this->createCacheKey($domain, $type), "true", $ttl); + $this->cache->set($this->createCacheKey($domain, $type), 'true', $ttl); } public function isNegativeCached(string $domain, int $type) : bool { diff --git a/lib/private/Http/WellKnown/RequestManager.php b/lib/private/Http/WellKnown/RequestManager.php index 38dde0eade2..3624bf73962 100644 --- a/lib/private/Http/WellKnown/RequestManager.php +++ b/lib/private/Http/WellKnown/RequestManager.php @@ -74,11 +74,11 @@ class RequestManager { $context = $this->coordinator->getRegistrationContext(); if ($context === null) { - throw new RuntimeException("Well known handlers requested before the apps had been fully registered"); + throw new RuntimeException('Well known handlers requested before the apps had been fully registered'); } $registrations = $context->getWellKnownHandlers(); - $this->logger->debug(count($registrations) . " well known handlers registered"); + $this->logger->debug(count($registrations) . ' well known handlers registered'); return array_filter( array_map(function (ServiceRegistration $registration) { diff --git a/lib/private/legacy/OC_Image.php b/lib/private/Image.php index 1d2326dcce9..3dd0bc49662 100644 --- a/lib/private/legacy/OC_Image.php +++ b/lib/private/Image.php @@ -1,17 +1,27 @@ <?php declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + +namespace OC; + +use finfo; +use GdImage; +use OCP\IAppConfig; +use OCP\IConfig; use OCP\IImage; +use OCP\Server; +use Psr\Log\LoggerInterface; /** * Class for basic image manipulation */ -class OC_Image implements \OCP\IImage { +class Image implements IImage { // Default memory limit for images to load (256 MBytes). protected const DEFAULT_MEMORY_LIMIT = 256; @@ -21,49 +31,34 @@ class OC_Image implements \OCP\IImage { // Default quality for webp images protected const DEFAULT_WEBP_QUALITY = 80; - /** @var false|\GdImage */ - protected $resource = false; // tmp resource. - /** @var int */ - protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident. - /** @var null|string */ - protected $mimeType = 'image/png'; // Default to png - /** @var null|string */ - protected $filePath = null; - /** @var ?finfo */ - private $fileInfo = null; - /** @var \OCP\ILogger */ - private $logger; - /** @var \OCP\IConfig */ - private $config; - /** @var ?array */ - private $exif = null; + // tmp resource. + protected GdImage|false $resource = false; + // Default to png if file type isn't evident. + protected int $imageType = IMAGETYPE_PNG; + // Default to png + protected ?string $mimeType = 'image/png'; + protected ?string $filePath = null; + private ?finfo $fileInfo = null; + private LoggerInterface $logger; + private IAppConfig $appConfig; + private IConfig $config; + private ?array $exif = null; /** - * Constructor. - * - * @param mixed $imageRef Deprecated, should be null - * @psalm-assert null $imageRef - * @param \OCP\ILogger $logger - * @param \OCP\IConfig $config * @throws \InvalidArgumentException in case the $imageRef parameter is not null */ - public function __construct($imageRef = null, ?\OCP\ILogger $logger = null, ?\OCP\IConfig $config = null) { - $this->logger = $logger; - if ($logger === null) { - $this->logger = \OC::$server->getLogger(); - } - $this->config = $config; - if ($config === null) { - $this->config = \OC::$server->getConfig(); - } + public function __construct( + ?LoggerInterface $logger = null, + ?IAppConfig $appConfig = null, + ?IConfig $config = null, + ) { + $this->logger = $logger ?? Server::get(LoggerInterface::class); + $this->appConfig = $appConfig ?? Server::get(IAppConfig::class); + $this->config = $config ?? Server::get(IConfig::class); if (\OC_Util::fileInfoLoaded()) { $this->fileInfo = new finfo(FILEINFO_MIME_TYPE); } - - if ($imageRef !== null) { - throw new \InvalidArgumentException('The first parameter in the constructor is not supported anymore. Please use any of the load* methods of the image object to load an image.'); - } } /** @@ -120,7 +115,7 @@ class OC_Image implements \OCP\IImage { */ public function widthTopLeft(): int { $o = $this->getOrientation(); - $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']); + $this->logger->debug('Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']); switch ($o) { case -1: case 1: @@ -144,7 +139,7 @@ class OC_Image implements \OCP\IImage { */ public function heightTopLeft(): int { $o = $this->getOrientation(); - $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']); + $this->logger->debug('Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']); switch ($o) { case -1: case 1: @@ -171,7 +166,9 @@ class OC_Image implements \OCP\IImage { if ($mimeType === null) { $mimeType = $this->mimeType(); } - header('Content-Type: ' . $mimeType); + if ($mimeType !== null) { + header('Content-Type: ' . $mimeType); + } return $this->_output(null, $mimeType); } @@ -201,13 +198,10 @@ class OC_Image implements \OCP\IImage { /** * Outputs/saves the image. * - * @param string $filePath - * @param string $mimeType - * @return bool - * @throws Exception + * @throws \Exception */ private function _output(?string $filePath = null, ?string $mimeType = null): bool { - if ($filePath) { + if ($filePath !== null && $filePath !== '') { if (!file_exists(dirname($filePath))) { mkdir(dirname($filePath), 0777, true); } @@ -247,7 +241,7 @@ class OC_Image implements \OCP\IImage { $imageType = IMAGETYPE_WEBP; break; default: - throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format'); + throw new \Exception('Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format'); } } @@ -266,7 +260,7 @@ class OC_Image implements \OCP\IImage { if (function_exists('imagexbm')) { $retVal = imagexbm($this->resource, $filePath); } else { - throw new Exception('\OC_Image::_output(): imagexbm() is not supported.'); + throw new \Exception('Image::_output(): imagexbm() is not supported.'); } break; @@ -334,27 +328,27 @@ class OC_Image implements \OCP\IImage { } ob_start(); switch ($this->mimeType) { - case "image/png": + case 'image/png': $res = imagepng($this->resource); break; - case "image/jpeg": + case 'image/jpeg': imageinterlace($this->resource, true); $quality = $this->getJpegQuality(); $res = imagejpeg($this->resource, null, $quality); break; - case "image/gif": + case 'image/gif': $res = imagegif($this->resource); break; - case "image/webp": + case 'image/webp': $res = imagewebp($this->resource, null, $this->getWebpQuality()); break; default: $res = imagepng($this->resource); - $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']); + $this->logger->info('Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']); break; } if (!$res) { - $this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']); + $this->logger->error('Image->data. Error getting image data.', ['app' => 'core']); } return ob_get_clean(); } @@ -363,31 +357,34 @@ class OC_Image implements \OCP\IImage { * @return string - base64 encoded, which is suitable for embedding in a VCard. */ public function __toString(): string { - return base64_encode($this->data()); + $data = $this->data(); + if ($data === null) { + return ''; + } else { + return base64_encode($data); + } } - /** - * @return int - */ protected function getJpegQuality(): int { - $quality = $this->config->getAppValue('preview', 'jpeg_quality', (string) self::DEFAULT_JPEG_QUALITY); - // TODO: remove when getAppValue is type safe - if ($quality === null) { - $quality = self::DEFAULT_JPEG_QUALITY; - } - return min(100, max(10, (int) $quality)); + $quality = $this->appConfig->getValueInt('preview', 'jpeg_quality', self::DEFAULT_JPEG_QUALITY); + return min(100, max(10, $quality)); } - /** - * @return int - */ protected function getWebpQuality(): int { - $quality = $this->config->getAppValue('preview', 'webp_quality', (string) self::DEFAULT_WEBP_QUALITY); - // TODO: remove when getAppValue is type safe - if ($quality === null) { - $quality = self::DEFAULT_WEBP_QUALITY; + $quality = $this->appConfig->getValueInt('preview', 'webp_quality', self::DEFAULT_WEBP_QUALITY); + return min(100, max(10, $quality)); + } + + private function isValidExifData(array $exif): bool { + if (!isset($exif['Orientation'])) { + return false; } - return min(100, max(10, (int) $quality)); + + if (!is_numeric($exif['Orientation'])) { + return false; + } + + return true; } /** @@ -402,47 +399,41 @@ class OC_Image implements \OCP\IImage { } if ($this->imageType !== IMAGETYPE_JPEG) { - $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']); return -1; } if (!is_callable('exif_read_data')) { - $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); return -1; } if (!$this->valid()) { - $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']); return -1; } if (is_null($this->filePath) || !is_readable($this->filePath)) { - $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() No readable file path set.', ['app' => 'core']); return -1; } $exif = @exif_read_data($this->filePath, 'IFD0'); - if (!$exif) { - return -1; - } - if (!isset($exif['Orientation'])) { + if ($exif === false || !$this->isValidExifData($exif)) { return -1; } $this->exif = $exif; - return $exif['Orientation']; + return (int)$exif['Orientation']; } - public function readExif($data): void { + public function readExif(string $data): void { if (!is_callable('exif_read_data')) { - $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); return; } if (!$this->valid()) { - $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']); return; } $exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data)); - if (!$exif) { - return; - } - if (!isset($exif['Orientation'])) { + if ($exif === false || !$this->isValidExifData($exif)) { return; } $this->exif = $exif; @@ -460,7 +451,7 @@ class OC_Image implements \OCP\IImage { return false; } $o = $this->getOrientation(); - $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']); $rotate = 0; $flip = false; switch ($o) { @@ -507,15 +498,15 @@ class OC_Image implements \OCP\IImage { $this->resource = $res; return true; } else { - $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Error during alpha-saving', ['app' => 'core']); return false; } } else { - $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Error during alpha-blending', ['app' => 'core']); return false; } } else { - $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']); + $this->logger->debug('Image->fixOrientation() Error during orientation fixing', ['app' => 'core']); return false; } } @@ -626,10 +617,10 @@ class OC_Image implements \OCP\IImage { imagealphablending($this->resource, true); imagesavealpha($this->resource, true); } else { - $this->logger->debug('OC_Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']); } } else { - $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']); } break; case IMAGETYPE_JPEG: @@ -640,10 +631,10 @@ class OC_Image implements \OCP\IImage { if (@getimagesize($imagePath) !== false) { $this->resource = @imagecreatefromjpeg($imagePath); } else { - $this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']); } } else { - $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']); } break; case IMAGETYPE_PNG: @@ -657,10 +648,10 @@ class OC_Image implements \OCP\IImage { imagealphablending($this->resource, true); imagesavealpha($this->resource, true); } else { - $this->logger->debug('OC_Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']); } } else { - $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']); } break; case IMAGETYPE_XBM: @@ -670,7 +661,7 @@ class OC_Image implements \OCP\IImage { } $this->resource = @imagecreatefromxbm($imagePath); } else { - $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']); } break; case IMAGETYPE_WBMP: @@ -680,7 +671,7 @@ class OC_Image implements \OCP\IImage { } $this->resource = @imagecreatefromwbmp($imagePath); } else { - $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']); } break; case IMAGETYPE_BMP: @@ -702,7 +693,7 @@ class OC_Image implements \OCP\IImage { return false; } $data = fread($fp, 90); - if (!$data) { + if ($data === false) { return false; } fclose($fp); @@ -716,7 +707,7 @@ class OC_Image implements \OCP\IImage { $header = unpack($headerFormat, $data); unset($data, $headerFormat); - if (!$header) { + if ($header === false) { return false; } @@ -734,13 +725,13 @@ class OC_Image implements \OCP\IImage { // Check for animation indicators if (strpos(strtoupper($header['Chunk']), 'ANIM') !== false || strpos(strtoupper($header['Chunk']), 'ANMF') !== false) { // Animated so don't let it reach libgd - $this->logger->debug('OC_Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']); } else { // We're safe so give it to libgd $this->resource = @imagecreatefromwebp($imagePath); } } else { - $this->logger->debug('OC_Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']); } break; /* @@ -776,7 +767,7 @@ class OC_Image implements \OCP\IImage { } $this->resource = @imagecreatefromstring($data); $iType = IMAGETYPE_PNG; - $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, Default', ['app' => 'core']); break; } if ($this->valid()) { @@ -788,12 +779,9 @@ class OC_Image implements \OCP\IImage { } /** - * Loads an image from a string of data. - * - * @param string $str A string of image data as read from a file. - * @return bool|\GdImage An image resource or false on error + * @inheritDoc */ - public function loadFromData(string $str) { + public function loadFromData(string $str): GdImage|false { if (!$this->checkImageDataSize($str)) { return false; } @@ -807,7 +795,7 @@ class OC_Image implements \OCP\IImage { } if (!$this->resource) { - $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']); + $this->logger->debug('Image->loadFromFile, could not load', ['app' => 'core']); return false; } return $this->resource; @@ -830,7 +818,7 @@ class OC_Image implements \OCP\IImage { $this->mimeType = $this->fileInfo->buffer($data); } if (!$this->resource) { - $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']); + $this->logger->debug('Image->loadFromBase64, could not load', ['app' => 'core']); return false; } return $this->resource; @@ -856,11 +844,7 @@ class OC_Image implements \OCP\IImage { return $this->valid(); } - /** - * @param $maxSize - * @return bool|\GdImage - */ - private function resizeNew(int $maxSize) { + private function resizeNew(int $maxSize): \GdImage|false { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -896,12 +880,7 @@ class OC_Image implements \OCP\IImage { return $this->valid(); } - /** - * @param int $width - * @param int $height - * @return bool|\GdImage - */ - public function preciseResizeNew(int $width, int $height) { + public function preciseResizeNew(int $width, int $height): \GdImage|false { if (!($width > 0) || !($height > 0)) { $this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']); return false; @@ -920,7 +899,11 @@ class OC_Image implements \OCP\IImage { // preserve transparency if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { - imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); + $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127); + if ($alpha === false) { + $alpha = null; + } + imagecolortransparent($process, $alpha); imagealphablending($process, false); imagesavealpha($process, true); } @@ -942,7 +925,7 @@ class OC_Image implements \OCP\IImage { */ public function centerCrop(int $size = 0): bool { if (!$this->valid()) { - $this->logger->debug('OC_Image->centerCrop, No image loaded', ['app' => 'core']); + $this->logger->debug('Image->centerCrop, No image loaded', ['app' => 'core']); return false; } $widthOrig = imagesx($this->resource); @@ -954,10 +937,10 @@ class OC_Image implements \OCP\IImage { $width = $height = min($widthOrig, $heightOrig); if ($ratioOrig > 1) { - $x = (int) (($widthOrig / 2) - ($width / 2)); + $x = (int)(($widthOrig / 2) - ($width / 2)); $y = 0; } else { - $y = (int) (($heightOrig / 2) - ($height / 2)); + $y = (int)(($heightOrig / 2) - ($height / 2)); $x = 0; } if ($size > 0) { @@ -969,20 +952,24 @@ class OC_Image implements \OCP\IImage { } $process = imagecreatetruecolor($targetWidth, $targetHeight); if ($process === false) { - $this->logger->debug('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']); + $this->logger->debug('Image->centerCrop, Error creating true color image', ['app' => 'core']); return false; } // preserve transparency if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { - imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127) ?: null); + $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127); + if ($alpha === false) { + $alpha = null; + } + imagecolortransparent($process, $alpha); imagealphablending($process, false); imagesavealpha($process, true); } $result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height); if ($result === false) { - $this->logger->debug('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']); + $this->logger->debug('Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']); return false; } imagedestroy($this->resource); @@ -1032,7 +1019,11 @@ class OC_Image implements \OCP\IImage { // preserve transparency if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { - imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127) ?: null); + $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127); + if ($alpha === false) { + $alpha = null; + } + imagecolortransparent($process, $alpha); imagealphablending($process, false); imagesavealpha($process, true); } @@ -1093,11 +1084,19 @@ class OC_Image implements \OCP\IImage { } public function copy(): IImage { - $image = new OC_Image(null, $this->logger, $this->config); + $image = new self($this->logger, $this->appConfig, $this->config); + if (!$this->valid()) { + /* image is invalid, return an empty one */ + return $image; + } $image->resource = imagecreatetruecolor($this->width(), $this->height()); + if (!$image->valid()) { + /* image creation failed, cannot copy in it */ + return $image; + } imagecopy( - $image->resource(), - $this->resource(), + $image->resource, + $this->resource, 0, 0, 0, @@ -1110,7 +1109,7 @@ class OC_Image implements \OCP\IImage { } public function cropCopy(int $x, int $y, int $w, int $h): IImage { - $image = new OC_Image(null, $this->logger, $this->config); + $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; $image->resource = $this->cropNew($x, $y, $w, $h); @@ -1119,7 +1118,7 @@ class OC_Image implements \OCP\IImage { } public function preciseResizeCopy(int $width, int $height): IImage { - $image = new OC_Image(null, $this->logger, $this->config); + $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; $image->resource = $this->preciseResizeNew($width, $height); @@ -1128,7 +1127,7 @@ class OC_Image implements \OCP\IImage { } public function resizeCopy(int $maxSize): IImage { - $image = new OC_Image(null, $this->logger, $this->config); + $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; $image->resource = $this->resizeNew($maxSize); diff --git a/lib/private/Installer.php b/lib/private/Installer.php index ad80b26d8bc..e0f7644304e 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -499,7 +499,7 @@ class Installer { while (false !== ($filename = readdir($dir))) { if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) { if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) { - if ($config->getAppValue($filename, "installed_version", null) === null) { + if ($config->getAppValue($filename, 'installed_version', null) === null) { $enabled = $appManager->isDefaultEnabled($filename); if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps())) && $config->getAppValue($filename, 'enabled') !== 'no') { diff --git a/lib/private/L10N/L10N.php b/lib/private/L10N/L10N.php index 39d778f80d6..50db373a65d 100644 --- a/lib/private/L10N/L10N.php +++ b/lib/private/L10N/L10N.php @@ -83,7 +83,7 @@ class L10N implements IL10N { $parameters = [$parameters]; } - return (string) new L10NString($this, $text, $parameters); + return (string)new L10NString($this, $text, $parameters); } /** @@ -104,14 +104,14 @@ class L10N implements IL10N { public function n(string $text_singular, string $text_plural, int $count, array $parameters = []): string { $identifier = "_{$text_singular}_::_{$text_plural}_"; if (isset($this->translations[$identifier])) { - return (string) new L10NString($this, $identifier, $parameters, $count); + return (string)new L10NString($this, $identifier, $parameters, $count); } if ($count === 1) { - return (string) new L10NString($this, $text_singular, $parameters, $count); + return (string)new L10NString($this, $text_singular, $parameters, $count); } - return (string) new L10NString($this, $text_plural, $parameters, $count); + return (string)new L10NString($this, $text_plural, $parameters, $count); } /** @@ -146,10 +146,10 @@ class L10N implements IL10N { } if ($type === 'firstday') { - return (int) Calendar::getFirstWeekday($this->locale); + return (int)Calendar::getFirstWeekday($this->locale); } if ($type === 'jsdate') { - return (string) Calendar::getDateFormat('short', $this->locale); + return (string)Calendar::getDateFormat('short', $this->locale); } $value = new \DateTime(); @@ -167,13 +167,13 @@ class L10N implements IL10N { $width = $options['width']; switch ($type) { case 'date': - return (string) Calendar::formatDate($value, $width, $this->locale); + return (string)Calendar::formatDate($value, $width, $this->locale); case 'datetime': - return (string) Calendar::formatDatetime($value, $width, $this->locale); + return (string)Calendar::formatDatetime($value, $width, $this->locale); case 'time': - return (string) Calendar::formatTime($value, $width, $this->locale); + return (string)Calendar::formatTime($value, $width, $this->locale); case 'weekdayName': - return (string) Calendar::getWeekdayName($value, $width, $this->locale); + return (string)Calendar::getWeekdayName($value, $width, $this->locale); default: return false; } diff --git a/lib/private/LDAP/NullLDAPProviderFactory.php b/lib/private/LDAP/NullLDAPProviderFactory.php index 55561b5692e..60588e4d15b 100644 --- a/lib/private/LDAP/NullLDAPProviderFactory.php +++ b/lib/private/LDAP/NullLDAPProviderFactory.php @@ -16,7 +16,7 @@ class NullLDAPProviderFactory implements ILDAPProviderFactory { } public function getLDAPProvider() { - throw new \Exception("No LDAP provider is available"); + throw new \Exception('No LDAP provider is available'); } public function isAvailable(): bool { diff --git a/lib/private/LargeFileHelper.php b/lib/private/LargeFileHelper.php index 4d96e79ead4..238fb0790b8 100755 --- a/lib/private/LargeFileHelper.php +++ b/lib/private/LargeFileHelper.php @@ -153,7 +153,7 @@ class LargeFileHelper { // For file sizes between 2 GiB and 4 GiB, filesize() will return a // negative int, as the PHP data type int is signed. Interpret the // returned int as an unsigned integer and put it into a float. - return (float) sprintf('%u', $result); + return (float)sprintf('%u', $result); } return $result; } diff --git a/lib/private/Lock/MemcacheLockingProvider.php b/lib/private/Lock/MemcacheLockingProvider.php index 883abb5da98..b249e08d717 100644 --- a/lib/private/Lock/MemcacheLockingProvider.php +++ b/lib/private/Lock/MemcacheLockingProvider.php @@ -62,8 +62,8 @@ class MemcacheLockingProvider extends AbstractLockingProvider { if ($type === self::LOCK_SHARED) { // save the old TTL to for `restoreTTL` $this->oldTTLs[$path] = [ - "ttl" => $this->getTTL($path), - "time" => $this->timeFactory->getTime() + 'ttl' => $this->getTTL($path), + 'time' => $this->timeFactory->getTime() ]; if (!$this->memcache->inc($path)) { throw new LockedException($path, null, $this->getExistingLockForException($path), $readablePath); diff --git a/lib/private/Log.php b/lib/private/Log.php index 4fce0436707..4f6003cf53d 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -41,13 +41,9 @@ class Log implements ILogger, IDataLogger { public function __construct( private IWriter $logger, private SystemConfig $config, - private ?Normalizer $normalizer = null, + private Normalizer $normalizer = new Normalizer(), private ?IRegistry $crashReporters = null ) { - // FIXME: php8.1 allows "private Normalizer $normalizer = new Normalizer()," in initializer - if ($normalizer === null) { - $this->normalizer = new Normalizer(); - } } public function setEventDispatcher(IEventDispatcher $eventDispatcher): void { @@ -330,7 +326,7 @@ class Log implements ILogger, IDataLogger { try { $serializer = $this->getSerializer(); } catch (Throwable $e) { - $this->error("Failed to load ExceptionSerializer serializer while trying to log " . $exception->getMessage()); + $this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage()); return; } $data = $context; diff --git a/lib/private/Log/File.php b/lib/private/Log/File.php index 28cc856b980..bc14de4ffdf 100644 --- a/lib/private/Log/File.php +++ b/lib/private/Log/File.php @@ -73,7 +73,7 @@ class File extends LogDetails implements IWriter, IFileBased { * get entries from the log in reverse chronological order */ public function getEntries(int $limit = 50, int $offset = 0): array { - $minLevel = $this->config->getValue("loglevel", ILogger::WARN); + $minLevel = $this->config->getValue('loglevel', ILogger::WARN); $entries = []; $handle = @fopen($this->logFile, 'rb'); if ($handle) { diff --git a/lib/private/Log/LogDetails.php b/lib/private/Log/LogDetails.php index bf2c1a22c49..95b09c0a181 100644 --- a/lib/private/Log/LogDetails.php +++ b/lib/private/Log/LogDetails.php @@ -22,7 +22,7 @@ abstract class LogDetails { } catch (\Exception $e) { $timezone = new \DateTimeZone('UTC'); } - $time = \DateTime::createFromFormat("U.u", number_format(microtime(true), 4, ".", "")); + $time = \DateTime::createFromFormat('U.u', number_format(microtime(true), 4, '.', '')); if ($time === false) { $time = new \DateTime('now', $timezone); } else { diff --git a/lib/private/Log/PsrLoggerAdapter.php b/lib/private/Log/PsrLoggerAdapter.php index 16e609eefdb..57a67bd67a7 100644 --- a/lib/private/Log/PsrLoggerAdapter.php +++ b/lib/private/Log/PsrLoggerAdapter.php @@ -35,7 +35,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { /** * System is unusable. * - * @param $message + * @param $message * @param mixed[] $context */ public function emergency($message, array $context = []): void { @@ -58,7 +58,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param $message + * @param $message * @param mixed[] $context */ public function alert($message, array $context = []): void { @@ -80,7 +80,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * * Example: Application component unavailable, unexpected exception. * - * @param $message + * @param $message * @param mixed[] $context */ public function critical($message, array $context = []): void { @@ -101,7 +101,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param $message + * @param $message * @param mixed[] $context */ public function error($message, array $context = []): void { @@ -124,7 +124,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param $message + * @param $message * @param mixed[] $context */ public function warning($message, array $context = []): void { @@ -144,7 +144,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { /** * Normal but significant events. * - * @param $message + * @param $message * @param mixed[] $context */ public function notice($message, array $context = []): void { @@ -166,7 +166,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * * Example: User logs in, SQL logs. * - * @param $message + * @param $message * @param mixed[] $context */ public function info($message, array $context = []): void { @@ -186,7 +186,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { /** * Detailed debug information. * - * @param $message + * @param $message * @param mixed[] $context */ public function debug($message, array $context = []): void { @@ -207,7 +207,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { * Logs with an arbitrary level. * * @param mixed $level - * @param $message + * @param $message * @param mixed[] $context * * @throws InvalidArgumentException diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 8047cb80bad..2cb222fd137 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -346,7 +346,7 @@ EOF; * Adds a heading to the email * * @param string|bool $plainTitle Title that is used in the plain text email - * if empty the $title is used, if false none will be used + * if empty the $title is used, if false none will be used */ public function addHeading(string $title, $plainTitle = ''): void { if ($this->footerAdded) { @@ -379,7 +379,7 @@ EOF; * * @param string $text Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email * @param string|bool $plainText Text that is used in the plain text email - * if empty the $text is used, if false none will be used + * if empty the $text is used, if false none will be used */ public function addBodyText(string $text, $plainText = ''): void { if ($this->footerAdded) { @@ -406,9 +406,9 @@ EOF; * @param string $metaInfo Note: When $plainMetaInfo falls back to this, HTML is automatically escaped in the HTML email * @param string $icon Absolute path, must be 16*16 pixels * @param string|bool $plainText Text that is used in the plain text email - * if empty or true the $text is used, if false none will be used + * if empty or true the $text is used, if false none will be used * @param string|bool $plainMetaInfo Meta info that is used in the plain text email - * if empty or true the $metaInfo is used, if false none will be used + * if empty or true the $metaInfo is used, if false none will be used * @param integer $plainIndent plainIndent If > 0, Indent plainText by this amount. * @since 12.0.0 */ @@ -425,7 +425,7 @@ EOF; if ($plainText === '' || $plainText === true) { $plainText = $text; $text = htmlspecialchars($text); - $text = str_replace("\n", "<br/>", $text); // convert newlines to HTML breaks + $text = str_replace("\n", '<br/>', $text); // convert newlines to HTML breaks } if ($plainMetaInfo === '' || $plainMetaInfo === true) { $plainMetaInfo = $metaInfo; @@ -536,7 +536,7 @@ EOF; * @param string $text Text of button; Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email * @param string $url URL of button * @param string|false $plainText Text of button in plain text version - * if empty the $text is used, if false none will be used + * if empty the $text is used, if false none will be used * * @since 12.0.0 */ diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php index 4ddb748fc26..0a818b847aa 100644 --- a/lib/private/Mail/Mailer.php +++ b/lib/private/Mail/Mailer.php @@ -103,23 +103,11 @@ class Mailer implements IMailer { * @since 12.0.0 */ public function createEMailTemplate(string $emailId, array $data = []): IEMailTemplate { - $class = $this->config->getSystemValueString('mail_template_class', ''); - - if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) { - return new $class( - $this->defaults, - $this->urlGenerator, - $this->l10nFactory, - $emailId, - $data - ); - } - $logoDimensions = $this->config->getAppValue('theming', 'logoDimensions', self::DEFAULT_DIMENSIONS); if (str_contains($logoDimensions, 'x')) { [$width, $height] = explode('x', $logoDimensions); - $width = (int) $width; - $height = (int) $height; + $width = (int)$width; + $height = (int)$height; if ($width > self::MAX_LOGO_SIZE || $height > self::MAX_LOGO_SIZE) { if ($width === $height) { @@ -127,9 +115,9 @@ class Mailer implements IMailer { $logoHeight = self::MAX_LOGO_SIZE; } elseif ($width > $height) { $logoWidth = self::MAX_LOGO_SIZE; - $logoHeight = (int) (($height / $width) * self::MAX_LOGO_SIZE); + $logoHeight = (int)(($height / $width) * self::MAX_LOGO_SIZE); } else { - $logoWidth = (int) (($width / $height) * self::MAX_LOGO_SIZE); + $logoWidth = (int)(($width / $height) * self::MAX_LOGO_SIZE); $logoHeight = self::MAX_LOGO_SIZE; } } else { @@ -140,6 +128,20 @@ class Mailer implements IMailer { $logoWidth = $logoHeight = null; } + $class = $this->config->getSystemValueString('mail_template_class', ''); + + if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) { + return new $class( + $this->defaults, + $this->urlGenerator, + $this->l10nFactory, + $logoWidth, + $logoHeight, + $emailId, + $data + ); + } + return new EMailTemplate( $this->defaults, $this->urlGenerator, diff --git a/lib/private/Mail/Provider/Manager.php b/lib/private/Mail/Provider/Manager.php index 244aa86d68d..14ffeac287b 100644 --- a/lib/private/Mail/Provider/Manager.php +++ b/lib/private/Mail/Provider/Manager.php @@ -60,7 +60,7 @@ class Manager implements IManager { * * @since 30.0.0 * - * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector'] + * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector'] */ public function types(): array { @@ -80,7 +80,7 @@ class Manager implements IManager { * * @since 30.0.0 * - * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject] + * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject] */ public function providers(): array { @@ -121,11 +121,11 @@ class Manager implements IManager { * * @since 30.0.0 * - * @param string $providerId provider id + * @param string $providerId provider id * * @return IProvider|null */ - public function findProviderById(string $providerId): IProvider | null { + public function findProviderById(string $providerId): IProvider|null { // evaluate if we already have a cached collection of providers if (!is_array($this->providersCollection)) { @@ -145,9 +145,9 @@ class Manager implements IManager { * * @since 30.0.0 * - * @param string $userId user id + * @param string $userId user id * - * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]] + * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]] */ public function services(string $userId): array { @@ -172,13 +172,13 @@ class Manager implements IManager { * * @since 30.0.0 * - * @param string $userId user id - * @param string $serviceId service id - * @param string $providerId provider id + * @param string $userId user id + * @param string $serviceId service id + * @param string $providerId provider id * - * @return IService|null returns service object or null if none found + * @return IService|null returns service object or null if none found */ - public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService | null { + public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService|null { // evaluate if provider id was specified if ($providerId !== null) { @@ -216,13 +216,13 @@ class Manager implements IManager { * * @since 30.0.0 * - * @param string $userId user id - * @param string $address mail address (e.g. test@example.com) - * @param string $providerId provider id + * @param string $userId user id + * @param string $address mail address (e.g. test@example.com) + * @param string $providerId provider id * - * @return IService|null returns service object or null if none found + * @return IService|null returns service object or null if none found */ - public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService | null { + public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService|null { // evaluate if provider id was specified if ($providerId !== null) { diff --git a/lib/private/Memcache/CADTrait.php b/lib/private/Memcache/CADTrait.php index bb010e238dc..3bf94246338 100644 --- a/lib/private/Memcache/CADTrait.php +++ b/lib/private/Memcache/CADTrait.php @@ -35,4 +35,21 @@ trait CADTrait { return false; } } + + public function ncad(string $key, mixed $old): bool { + //no native cad, emulate with locking + if ($this->add($key . '_lock', true)) { + $value = $this->get($key); + if ($value !== null && $value !== $old) { + $this->remove($key); + $this->remove($key . '_lock'); + return true; + } else { + $this->remove($key . '_lock'); + return false; + } + } else { + return false; + } + } } diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index c0f4f787200..931c871d0f1 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -16,7 +16,7 @@ use Psr\Log\LoggerInterface; class Factory implements ICacheFactory { public const NULL_CACHE = NullCache::class; - private string $globalPrefix; + private ?string $globalPrefix = null; private LoggerInterface $logger; @@ -40,17 +40,23 @@ class Factory implements ICacheFactory { private IProfiler $profiler; /** - * @param string $globalPrefix + * @param callable $globalPrefixClosure * @param LoggerInterface $logger * @param ?class-string<ICache> $localCacheClass * @param ?class-string<ICache> $distributedCacheClass * @param ?class-string<IMemcache> $lockingCacheClass * @param string $logFile */ - public function __construct(string $globalPrefix, LoggerInterface $logger, IProfiler $profiler, - ?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, string $logFile = '') { + public function __construct( + private $globalPrefixClosure, + LoggerInterface $logger, + IProfiler $profiler, + ?string $localCacheClass = null, + ?string $distributedCacheClass = null, + ?string $lockingCacheClass = null, + string $logFile = '' + ) { $this->logFile = $logFile; - $this->globalPrefix = $globalPrefix; if (!$localCacheClass) { $localCacheClass = self::NULL_CACHE; @@ -59,6 +65,7 @@ class Factory implements ICacheFactory { if (!$distributedCacheClass) { $distributedCacheClass = $localCacheClass; } + $distributedCacheClass = ltrim($distributedCacheClass, '\\'); $missingCacheMessage = 'Memcache {class} not available for {use} cache'; @@ -85,6 +92,13 @@ class Factory implements ICacheFactory { $this->profiler = $profiler; } + private function getGlobalPrefix(): ?string { + if (is_null($this->globalPrefix)) { + $this->globalPrefix = ($this->globalPrefixClosure)(); + } + return $this->globalPrefix; + } + /** * create a cache instance for storing locks * @@ -92,8 +106,13 @@ class Factory implements ICacheFactory { * @return IMemcache */ public function createLocking(string $prefix = ''): IMemcache { + $globalPrefix = $this->getGlobalPrefix(); + if (is_null($globalPrefix)) { + return new ArrayCache($prefix); + } + assert($this->lockingCacheClass !== null); - $cache = new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix); + $cache = new $this->lockingCacheClass($globalPrefix . '/' . $prefix); if ($this->lockingCacheClass === Redis::class && $this->profiler->isEnabled()) { // We only support the profiler with Redis $cache = new ProfilerWrapperCache($cache, 'Locking'); @@ -114,8 +133,13 @@ class Factory implements ICacheFactory { * @return ICache */ public function createDistributed(string $prefix = ''): ICache { + $globalPrefix = $this->getGlobalPrefix(); + if (is_null($globalPrefix)) { + return new ArrayCache($prefix); + } + assert($this->distributedCacheClass !== null); - $cache = new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix); + $cache = new $this->distributedCacheClass($globalPrefix . '/' . $prefix); if ($this->distributedCacheClass === Redis::class && $this->profiler->isEnabled()) { // We only support the profiler with Redis $cache = new ProfilerWrapperCache($cache, 'Distributed'); @@ -136,8 +160,13 @@ class Factory implements ICacheFactory { * @return ICache */ public function createLocal(string $prefix = ''): ICache { + $globalPrefix = $this->getGlobalPrefix(); + if (is_null($globalPrefix)) { + return new ArrayCache($prefix); + } + assert($this->localCacheClass !== null); - $cache = new $this->localCacheClass($this->globalPrefix . '/' . $prefix); + $cache = new $this->localCacheClass($globalPrefix . '/' . $prefix); if ($this->localCacheClass === Redis::class && $this->profiler->isEnabled()) { // We only support the profiler with Redis $cache = new ProfilerWrapperCache($cache, 'Local'); diff --git a/lib/private/Memcache/LoggerWrapperCache.php b/lib/private/Memcache/LoggerWrapperCache.php index 11497e2a5d8..c2a06731910 100644 --- a/lib/private/Memcache/LoggerWrapperCache.php +++ b/lib/private/Memcache/LoggerWrapperCache.php @@ -150,6 +150,17 @@ class LoggerWrapperCache extends Cache implements IMemcacheTTL { } /** @inheritDoc */ + public function ncad(string $key, mixed $old): bool { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::ncad::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->cad($key, $old); + } + + /** @inheritDoc */ public function setTTL(string $key, int $ttl) { $this->wrappedCache->setTTL($key, $ttl); } diff --git a/lib/private/Memcache/NullCache.php b/lib/private/Memcache/NullCache.php index ab5c491913a..b667869bf0d 100644 --- a/lib/private/Memcache/NullCache.php +++ b/lib/private/Memcache/NullCache.php @@ -43,6 +43,11 @@ class NullCache extends Cache implements \OCP\IMemcache { return true; } + public function ncad(string $key, mixed $old): bool { + return true; + } + + public function clear($prefix = '') { return true; } diff --git a/lib/private/Memcache/ProfilerWrapperCache.php b/lib/private/Memcache/ProfilerWrapperCache.php index 84e3d880a0c..97d9d828a32 100644 --- a/lib/private/Memcache/ProfilerWrapperCache.php +++ b/lib/private/Memcache/ProfilerWrapperCache.php @@ -18,7 +18,7 @@ use OCP\IMemcacheTTL; * @template-implements \ArrayAccess<string,mixed> */ class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess { - /** @var Redis $wrappedCache*/ + /** @var Redis $wrappedCache */ protected $wrappedCache; /** @var string $prefix */ @@ -167,6 +167,18 @@ class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL } /** @inheritDoc */ + public function ncad(string $key, mixed $old): bool { + $start = microtime(true); + $ret = $this->wrappedCache->ncad($key, $old); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::ncad::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ public function setTTL(string $key, int $ttl) { $this->wrappedCache->setTTL($key, $ttl); } diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index 87dc86ab10d..711531e0ac2 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -23,6 +23,10 @@ class Redis extends Cache implements IMemcacheTTL { 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end', 'cf0e94b2e9ffc7e04395cf88f7583fc309985910', ], + 'ncad' => [ + 'if redis.call("get", KEYS[1]) ~= ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end', + '75526f8048b13ce94a41b58eee59c664b4990ab2', + ], 'caSetTtl' => [ 'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("expire", KEYS[1], ARGV[2]) return 1 else return 0 end', 'fa4acbc946d23ef41d7d3910880b60e6e4972d72', @@ -164,6 +168,12 @@ class Redis extends Cache implements IMemcacheTTL { return $this->evalLua('cad', [$key], [$old]) > 0; } + public function ncad(string $key, mixed $old): bool { + $old = self::encodeValue($old); + + return $this->evalLua('ncad', [$key], [$old]) > 0; + } + public function setTTL($key, $ttl) { if ($ttl === 0) { // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades @@ -202,10 +212,10 @@ class Redis extends Cache implements IMemcacheTTL { } protected static function encodeValue(mixed $value): string { - return is_int($value) ? (string) $value : json_encode($value); + return is_int($value) ? (string)$value : json_encode($value); } protected static function decodeValue(string $value): mixed { - return is_numeric($value) ? (int) $value : json_decode($value, true); + return is_numeric($value) ? (int)$value : json_decode($value, true); } } diff --git a/lib/private/NaturalSort.php b/lib/private/NaturalSort.php index 120e05a8eb2..9b097340b63 100644 --- a/lib/private/NaturalSort.php +++ b/lib/private/NaturalSort.php @@ -82,7 +82,7 @@ class NaturalSort { * @param string $a first string to compare * @param string $b second string to compare * @return int -1 if $b comes before $a, 1 if $a comes before $b - * or 0 if the strings are identical + * or 0 if the strings are identical */ public function compare($a, $b) { // Needed because PHP doesn't sort correctly when numbers are enclosed in diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php index 7d042e6f8d8..8edbca0380d 100644 --- a/lib/private/Notification/Manager.php +++ b/lib/private/Notification/Manager.php @@ -68,7 +68,7 @@ class Manager implements IManager { } /** * @param string $appClass The service must implement IApp, otherwise a - * \InvalidArgumentException is thrown later + * \InvalidArgumentException is thrown later * @since 17.0.0 */ public function registerApp(string $appClass): void { @@ -78,8 +78,8 @@ class Manager implements IManager { /** * @param \Closure $service The service must implement INotifier, otherwise a * \InvalidArgumentException is thrown later - * @param \Closure $info An array with the keys 'id' and 'name' containing - * the app id and the app name + * @param \Closure $info An array with the keys 'id' and 'name' containing + * the app id and the app name * @deprecated 17.0.0 use registerNotifierService instead. * @since 8.2.0 - Parameter $info was added in 9.0.0 */ @@ -93,7 +93,7 @@ class Manager implements IManager { /** * @param string $notifierService The service must implement INotifier, otherwise a - * \InvalidArgumentException is thrown later + * \InvalidArgumentException is thrown later * @since 17.0.0 */ public function registerNotifierService(string $notifierService): void { diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 17a356428f7..9bda95ebc17 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -163,8 +163,8 @@ class OCMProvider implements IOCMProvider { */ public function import(array $data): static { $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) - ->setApiVersion((string)($data['apiVersion'] ?? '')) - ->setEndPoint($data['endPoint'] ?? ''); + ->setApiVersion((string)($data['apiVersion'] ?? '')) + ->setEndPoint($data['endPoint'] ?? ''); $resources = []; foreach (($data['resourceTypes'] ?? []) as $resourceData) { diff --git a/lib/private/OCM/Model/OCMResource.php b/lib/private/OCM/Model/OCMResource.php index c69763ca4ba..68f9ee18f79 100644 --- a/lib/private/OCM/Model/OCMResource.php +++ b/lib/private/OCM/Model/OCMResource.php @@ -85,8 +85,8 @@ class OCMResource implements IOCMResource { */ public function import(array $data): static { return $this->setName((string)($data['name'] ?? '')) - ->setShareTypes($data['shareTypes'] ?? []) - ->setProtocols($data['protocols'] ?? []); + ->setShareTypes($data['shareTypes'] ?? []) + ->setProtocols($data['protocols'] ?? []); } /** diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php index deadcd007b1..acf7bf22f52 100644 --- a/lib/private/Preview/BackgroundCleanupJob.php +++ b/lib/private/Preview/BackgroundCleanupJob.php @@ -16,6 +16,7 @@ use OCP\Files\IMimeTypeLoader; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IDBConnection; +use function Symfony\Component\Translation\t; class BackgroundCleanupJob extends TimedJob { /** @var IDBConnection */ @@ -64,6 +65,11 @@ class BackgroundCleanupJob extends TimedJob { } private function getOldPreviewLocations(): \Iterator { + if ($this->connection->getShardDefinition('filecache')) { + // sharding is new enough that we don't need to support this + return; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('a.name') ->from('filecache', 'a') @@ -106,6 +112,15 @@ class BackgroundCleanupJob extends TimedJob { return []; } + if ($this->connection->getShardDefinition('filecache')) { + $chunks = $this->getAllPreviewIds($data['path'], 1000); + foreach ($chunks as $chunk) { + yield from $this->findMissingSources($chunk); + } + + return; + } + /* * This lovely like is the result of the way the new previews are stored * We take the md5 of the name (fileid) and split the first 7 chars. That way @@ -155,4 +170,46 @@ class BackgroundCleanupJob extends TimedJob { $cursor->closeCursor(); } + + private function getAllPreviewIds(string $previewRoot, int $chunkSize): \Iterator { + // See `getNewPreviewLocations` for some more info about the logic here + $like = $this->connection->escapeLikeParameter($previewRoot). '/_/_/_/_/_/_/_/%'; + + $qb = $this->connection->getQueryBuilder(); + $qb->select('name', 'fileid') + ->from('filecache') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('storage', $qb->createNamedParameter($this->previewFolder->getStorageId())), + $qb->expr()->like('path', $qb->createNamedParameter($like)), + $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))), + $qb->expr()->gt('fileid', $qb->createParameter('min_id')), + ) + ) + ->orderBy('fileid', 'ASC') + ->setMaxResults($chunkSize); + + $minId = 0; + while (true) { + $qb->setParameter('min_id', $minId); + $rows = $qb->executeQuery()->fetchAll(); + if (count($rows) > 0) { + $minId = $rows[count($rows) - 1]['fileid']; + yield array_map(function ($row) { + return (int)$row['name']; + }, $rows); + } else { + break; + } + } + } + + private function findMissingSources(array $ids): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('fileid') + ->from('filecache') + ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + return array_diff($ids, $found); + } } diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php index c9fad50841f..a3d5fbfd4ec 100644 --- a/lib/private/Preview/Bitmap.php +++ b/lib/private/Preview/Bitmap.php @@ -61,7 +61,7 @@ abstract class Bitmap extends ProviderV2 { //new bitmap image object $image = new \OCP\Image(); - $image->loadFromData((string) $bp); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 4083b9f4f61..460637c9a99 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -171,7 +171,7 @@ class Generator { $previewFiles[] = $preview; } } catch (\InvalidArgumentException $e) { - throw new NotFoundException("", 0, $e); + throw new NotFoundException('', 0, $e); } if ($preview->getSize() === 0) { @@ -272,13 +272,13 @@ class Generator { $hardwareConcurrency = self::getHardwareConcurrency(); switch ($type) { - case "preview_concurrency_all": + case 'preview_concurrency_all': $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; $concurrency_all = $this->config->getSystemValueInt($type, $fallback); - $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new"); + $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new'); $cached[$type] = max($concurrency_all, $concurrency_new); break; - case "preview_concurrency_new": + case 'preview_concurrency_new': $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; $cached[$type] = $this->config->getSystemValueInt($type, $fallback); break; diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index d198f11fdef..e5d73c943a4 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -31,7 +31,7 @@ class HEIC extends ProviderV2 { * {@inheritDoc} */ public function isAvailable(FileInfo $file): bool { - return in_array('HEIC', \Imagick::queryFormats("HEI*")); + return in_array('HEIC', \Imagick::queryFormats('HEI*')); } /** @@ -70,7 +70,7 @@ class HEIC extends ProviderV2 { //new bitmap image object $image = new \OCP\Image(); - $image->loadFromData((string) $bp); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } diff --git a/lib/private/Preview/MarkDown.php b/lib/private/Preview/MarkDown.php index 41a79455042..c20433a1ac0 100644 --- a/lib/private/Preview/MarkDown.php +++ b/lib/private/Preview/MarkDown.php @@ -52,7 +52,7 @@ class MarkDown extends TXT { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; + $fontSize = $maxX ? (int)((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; $image = imagecreate($maxX, $maxY); imagecolorallocate($image, 255, 255, 255); diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php index cfc05b8cce9..4a6104930d6 100644 --- a/lib/private/Preview/Movie.php +++ b/lib/private/Preview/Movie.php @@ -120,7 +120,7 @@ class Movie extends ProviderV2 { $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); $returnCode = -1; - $output = ""; + $output = ''; if (is_resource($proc)) { $stdout = trim(stream_get_contents($pipes[1])); $stderr = trim(stream_get_contents($pipes[2])); diff --git a/lib/private/Preview/SVG.php b/lib/private/Preview/SVG.php index 73dc0488bf1..d9f7701f411 100644 --- a/lib/private/Preview/SVG.php +++ b/lib/private/Preview/SVG.php @@ -58,7 +58,7 @@ class SVG extends ProviderV2 { //new image object $image = new \OCP\Image(); - $image->loadFromData((string) $svg); + $image->loadFromData((string)$svg); //check if image object is valid if ($image->valid()) { $image->scaleDownToFit($maxX, $maxY); diff --git a/lib/private/Preview/TXT.php b/lib/private/Preview/TXT.php index 68597f8dbd0..1a1d64f3e08 100644 --- a/lib/private/Preview/TXT.php +++ b/lib/private/Preview/TXT.php @@ -50,7 +50,7 @@ class TXT extends ProviderV2 { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / 32) * $maxX) : 5; //5px + $fontSize = $maxX ? (int)((1 / 32) * $maxX) : 5; //5px $lineSize = ceil($fontSize * 1.5); $image = imagecreate($maxX, $maxY); @@ -67,7 +67,7 @@ class TXT extends ProviderV2 { $index = $index + 1; $x = 1; - $y = (int) ($index * $lineSize); + $y = (int)($index * $lineSize); if ($canUseTTF === true) { imagettftext($image, $fontSize, 0, $x, $y, $textColor, $fontFile, $line); diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index f19ff25abb7..6f43687ceea 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -389,7 +389,7 @@ class PreviewManager implements IPreview { if (is_string($movieBinary)) { - $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ["movieBinary" => $movieBinary]); + $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]); } } } diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php index e575740f970..5e36a9c2f56 100644 --- a/lib/private/Profile/ProfileManager.php +++ b/lib/private/Profile/ProfileManager.php @@ -93,7 +93,7 @@ class ProfileManager implements IProfileManager { } $account = $this->accountManager->getAccount($user); - return (bool) filter_var( + return (bool)filter_var( $account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE, diff --git a/lib/private/Profiler/FileProfilerStorage.php b/lib/private/Profiler/FileProfilerStorage.php index 8cb6f2e6f79..b4494ef7a37 100644 --- a/lib/private/Profiler/FileProfilerStorage.php +++ b/lib/private/Profiler/FileProfilerStorage.php @@ -46,7 +46,7 @@ class FileProfilerStorage { while (\count($result) < $limit && $line = $this->readLineFromFile($file)) { $values = str_getcsv($line); [$csvToken, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values; - $csvTime = (int) $csvTime; + $csvTime = (int)$csvTime; if ($url && !str_contains($csvUrl, $url) || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) { continue; diff --git a/lib/private/RedisFactory.php b/lib/private/RedisFactory.php index b5779cbfbaa..dcb56cee9ef 100644 --- a/lib/private/RedisFactory.php +++ b/lib/private/RedisFactory.php @@ -13,7 +13,7 @@ class RedisFactory { public const REDIS_MINIMAL_VERSION = '4.0.0'; public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0'; - /** @var \Redis|\RedisCluster */ + /** @var \Redis|\RedisCluster */ private $instance; private SystemConfig $config; diff --git a/lib/private/Repair.php b/lib/private/Repair.php index d1904e08431..630ee249209 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -99,7 +99,7 @@ class Repair implements IOutput { try { $step->run($this); } catch (\Exception $e) { - $this->logger->error("Exception while executing repair step " . $step->getName(), ['exception' => $e]); + $this->logger->error('Exception while executing repair step ' . $step->getName(), ['exception' => $e]); $this->dispatcher->dispatchTyped(new RepairErrorEvent($e->getMessage())); } } diff --git a/lib/private/Repair/CleanTags.php b/lib/private/Repair/CleanTags.php index f2fc8156f29..258808cb28a 100644 --- a/lib/private/Repair/CleanTags.php +++ b/lib/private/Repair/CleanTags.php @@ -107,7 +107,7 @@ class CleanTags implements IRepairStep { $output, '%d tags for delete files have been removed.', 'vcategory_to_object', 'objid', - 'filecache', 'fileid', 'path_hash' + 'filecache', 'fileid', 'fileid' ); } @@ -147,8 +147,8 @@ class CleanTags implements IRepairStep { * @param string $deleteId * @param string $sourceTable * @param string $sourceId - * @param string $sourceNullColumn If this column is null in the source table, - * the entry is deleted in the $deleteTable + * @param string $sourceNullColumn If this column is null in the source table, + * the entry is deleted in the $deleteTable */ protected function deleteOrphanEntries(IOutput $output, $repairInfo, $deleteTable, $deleteId, $sourceTable, $sourceId, $sourceNullColumn) { $qb = $this->connection->getQueryBuilder(); @@ -166,19 +166,20 @@ class CleanTags implements IRepairStep { $orphanItems = []; while ($row = $result->fetch()) { - $orphanItems[] = (int) $row[$deleteId]; + $orphanItems[] = (int)$row[$deleteId]; } + $deleteQuery = $this->connection->getQueryBuilder(); + $deleteQuery->delete($deleteTable) + ->where( + $deleteQuery->expr()->eq('type', $deleteQuery->expr()->literal('files')) + ) + ->andWhere($deleteQuery->expr()->in($deleteId, $deleteQuery->createParameter('ids'))); if (!empty($orphanItems)) { $orphanItemsBatch = array_chunk($orphanItems, 200); foreach ($orphanItemsBatch as $items) { - $qb->delete($deleteTable) - ->where( - $qb->expr()->eq('type', $qb->expr()->literal('files')) - ) - ->andWhere($qb->expr()->in($deleteId, $qb->createParameter('ids'))); - $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY); - $qb->execute(); + $deleteQuery->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY); + $deleteQuery->executeStatement(); } } diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php index a01a684151b..9557aabd718 100644 --- a/lib/private/Repair/Collation.php +++ b/lib/private/Repair/Collation.php @@ -15,7 +15,7 @@ use OCP\Migration\IRepairStep; use Psr\Log\LoggerInterface; class Collation implements IRepairStep { - /** @var IConfig */ + /** @var IConfig */ protected $config; protected LoggerInterface $logger; @@ -92,14 +92,14 @@ class Collation implements IRepairStep { * @return string[] */ protected function getAllNonUTF8BinTables(IDBConnection $connection) { - $dbName = $this->config->getSystemValueString("dbname"); + $dbName = $this->config->getSystemValueString('dbname'); $characterSet = $this->config->getSystemValueBool('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8'; // fetch tables by columns $statement = $connection->executeQuery( - "SELECT DISTINCT(TABLE_NAME) AS `table`" . - " FROM INFORMATION_SCHEMA . COLUMNS" . - " WHERE TABLE_SCHEMA = ?" . + 'SELECT DISTINCT(TABLE_NAME) AS `table`' . + ' FROM INFORMATION_SCHEMA . COLUMNS' . + ' WHERE TABLE_SCHEMA = ?' . " AND (COLLATION_NAME <> '" . $characterSet . "_bin' OR CHARACTER_SET_NAME <> '" . $characterSet . "')" . " AND TABLE_NAME LIKE '*PREFIX*%'", [$dbName] @@ -112,9 +112,9 @@ class Collation implements IRepairStep { // fetch tables by collation $statement = $connection->executeQuery( - "SELECT DISTINCT(TABLE_NAME) AS `table`" . - " FROM INFORMATION_SCHEMA . TABLES" . - " WHERE TABLE_SCHEMA = ?" . + 'SELECT DISTINCT(TABLE_NAME) AS `table`' . + ' FROM INFORMATION_SCHEMA . TABLES' . + ' WHERE TABLE_SCHEMA = ?' . " AND TABLE_COLLATION <> '" . $characterSet . "_bin'" . " AND TABLE_NAME LIKE '*PREFIX*%'", [$dbName] diff --git a/lib/private/Repair/NC25/AddMissingSecretJob.php b/lib/private/Repair/NC25/AddMissingSecretJob.php index b407ef2a2a9..46b89d5f6f7 100644 --- a/lib/private/Repair/NC25/AddMissingSecretJob.php +++ b/lib/private/Repair/NC25/AddMissingSecretJob.php @@ -33,7 +33,7 @@ class AddMissingSecretJob implements IRepairStep { try { $this->config->setSystemValue('passwordsalt', $this->random->generate(30)); } catch (HintException $e) { - $output->warning("passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually."); + $output->warning('passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually.'); } } @@ -42,7 +42,7 @@ class AddMissingSecretJob implements IRepairStep { try { $this->config->setSystemValue('secret', $this->random->generate(48)); } catch (HintException $e) { - $output->warning("secret is missing from your config.php and your config.php is read only. Please fix it manually."); + $output->warning('secret is missing from your config.php and your config.php is read only. Please fix it manually.'); } } } diff --git a/lib/private/Repair/OldGroupMembershipShares.php b/lib/private/Repair/OldGroupMembershipShares.php index 54f2078395e..027f179596c 100644 --- a/lib/private/Repair/OldGroupMembershipShares.php +++ b/lib/private/Repair/OldGroupMembershipShares.php @@ -57,9 +57,9 @@ class OldGroupMembershipShares implements IRepairStep { ->from('share', 's1') ->where($query->expr()->isNotNull('s1.parent')) // \OC\Share\Constant::$shareTypeGroupUserUnique === 2 - ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2))) - ->andWhere($query->expr()->isNotNull('s2.id')) - ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(IShare::TYPE_GROUP))) + ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2))) + ->andWhere($query->expr()->isNotNull('s2.id')) + ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(IShare::TYPE_GROUP))) ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id')); $deleteQuery = $this->connection->getQueryBuilder(); @@ -69,7 +69,7 @@ class OldGroupMembershipShares implements IRepairStep { $result = $query->execute(); while ($row = $result->fetch()) { if (!$this->isMember($row['group'], $row['user'])) { - $deletedEntries += $deleteQuery->setParameter('share', (int) $row['id']) + $deletedEntries += $deleteQuery->setParameter('share', (int)$row['id']) ->execute(); } } diff --git a/lib/private/Repair/Owncloud/MigrateOauthTables.php b/lib/private/Repair/Owncloud/MigrateOauthTables.php index e8728cd2f66..94ec0eba3e6 100644 --- a/lib/private/Repair/Owncloud/MigrateOauthTables.php +++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php @@ -32,11 +32,11 @@ class MigrateOauthTables implements IRepairStep { public function run(IOutput $output) { $schema = new SchemaWrapper($this->db); if (!$schema->hasTable('oauth2_clients')) { - $output->info("oauth2_clients table does not exist."); + $output->info('oauth2_clients table does not exist.'); return; } - $output->info("Update the oauth2_access_tokens table schema."); + $output->info('Update the oauth2_access_tokens table schema.'); $schema = new SchemaWrapper($this->db); $table = $schema->getTable('oauth2_access_tokens'); if (!$table->hasColumn('hashed_code')) { @@ -58,7 +58,7 @@ class MigrateOauthTables implements IRepairStep { $table->addIndex(['client_id'], 'oauth2_access_client_id_idx'); } - $output->info("Update the oauth2_clients table schema."); + $output->info('Update the oauth2_clients table schema.'); $schema = new SchemaWrapper($this->db); $table = $schema->getTable('oauth2_clients'); if ($table->getColumn('name')->getLength() !== 64) { @@ -114,7 +114,7 @@ class MigrateOauthTables implements IRepairStep { $result->closeCursor(); // 2. Insert them into the client_identifier column. - foreach ($identifiers as ["id" => $id, "identifier" => $clientIdentifier]) { + foreach ($identifiers as ['id' => $id, 'identifier' => $clientIdentifier]) { $insertQuery = $this->db->getQueryBuilder(); $insertQuery->update('oauth2_clients') ->set('client_identifier', $insertQuery->createNamedParameter($clientIdentifier, IQueryBuilder::PARAM_STR)) @@ -122,7 +122,7 @@ class MigrateOauthTables implements IRepairStep { ->executeStatement(); } - $output->info("Drop the identifier column."); + $output->info('Drop the identifier column.'); $schema = new SchemaWrapper($this->db); $table = $schema->getTable('oauth2_clients'); $table->dropColumn('identifier'); diff --git a/lib/private/Repair/Owncloud/SaveAccountsTableData.php b/lib/private/Repair/Owncloud/SaveAccountsTableData.php index 1b6da7c858f..08665687b29 100644 --- a/lib/private/Repair/Owncloud/SaveAccountsTableData.php +++ b/lib/private/Repair/Owncloud/SaveAccountsTableData.php @@ -150,7 +150,7 @@ class SaveAccountsTableData implements IRepairStep { * @throws \UnexpectedValueException */ protected function migrateUserInfo(IQueryBuilder $update, $userdata) { - $state = (int) $userdata['state']; + $state = (int)$userdata['state']; if ($state === 3) { // Deleted user, ignore return; diff --git a/lib/private/Repair/RemoveLinkShares.php b/lib/private/Repair/RemoveLinkShares.php index f128b6f731b..634494acb2f 100644 --- a/lib/private/Repair/RemoveLinkShares.php +++ b/lib/private/Repair/RemoveLinkShares.php @@ -111,7 +111,7 @@ class RemoveLinkShares implements IRepairStep { $data = $result->fetch(); $result->closeCursor(); - return (int) $data['total']; + return (int)$data['total']; } /** @@ -180,7 +180,7 @@ class RemoveLinkShares implements IRepairStep { $users = array_keys($this->userToNotify); foreach ($users as $user) { - $notification->setUser((string) $user); + $notification->setUser((string)$user); $this->notificationManager->notify($notification); } } diff --git a/lib/private/Repair/RepairInvalidShares.php b/lib/private/Repair/RepairInvalidShares.php index f28ae1c45fb..71e6359da5b 100644 --- a/lib/private/Repair/RepairInvalidShares.php +++ b/lib/private/Repair/RepairInvalidShares.php @@ -65,7 +65,7 @@ class RepairInvalidShares implements IRepairStep { $query->select('s1.parent') ->from('share', 's1') ->where($query->expr()->isNotNull('s1.parent')) - ->andWhere($query->expr()->isNull('s2.id')) + ->andWhere($query->expr()->isNull('s2.id')) ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id')) ->groupBy('s1.parent') ->setMaxResults(self::CHUNK_SIZE); @@ -80,7 +80,7 @@ class RepairInvalidShares implements IRepairStep { $result = $query->execute(); while ($row = $result->fetch()) { $deletedInLastChunk++; - $deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent']) + $deletedEntries += $deleteQuery->setParameter('parent', (int)$row['parent']) ->execute(); } $result->closeCursor(); diff --git a/lib/private/Repair/RepairLogoDimension.php b/lib/private/Repair/RepairLogoDimension.php index 122da205986..854aeb3ab07 100644 --- a/lib/private/Repair/RepairLogoDimension.php +++ b/lib/private/Repair/RepairLogoDimension.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace OC\Repair; use OCA\Theming\ImageManager; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -44,9 +46,18 @@ class RepairLogoDimension implements IRepairStep { return; } - $simpleFile = $imageManager->getImage('logo', false); - - $image = @imagecreatefromstring($simpleFile->getContent()); + try { + try { + $simpleFile = $imageManager->getImage('logo', false); + $image = @imagecreatefromstring($simpleFile->getContent()); + } catch (NotFoundException|NotPermittedException) { + $simpleFile = $imageManager->getImage('logo'); + $image = false; + } + } catch (NotFoundException|NotPermittedException) { + $output->info('Theming is not used to provide a logo'); + return; + } $dimensions = ''; if ($image !== false) { diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php index 2eece761c8d..6932299dc4a 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -58,6 +58,7 @@ class RepairMimeTypes implements IRepairStep { $update = $this->connection->getQueryBuilder(); $update->update('filecache') + ->runAcrossAllShards() ->set('mimetype', $update->createParameter('mimetype')) ->where($update->expr()->neq('mimetype', $update->createParameter('mimetype'), IQueryBuilder::PARAM_INT)) ->andWhere($update->expr()->neq('mimetype', $update->createParameter('folder'), IQueryBuilder::PARAM_INT)) @@ -226,10 +227,10 @@ class RepairMimeTypes implements IRepairStep { */ private function introduceFlatOpenDocumentType(): IResult|int|null { $updatedMimetypes = [ - "fodt" => "application/vnd.oasis.opendocument.text-flat-xml", - "fods" => "application/vnd.oasis.opendocument.spreadsheet-flat-xml", - "fodg" => "application/vnd.oasis.opendocument.graphics-flat-xml", - "fodp" => "application/vnd.oasis.opendocument.presentation-flat-xml", + 'fodt' => 'application/vnd.oasis.opendocument.text-flat-xml', + 'fods' => 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', + 'fodg' => 'application/vnd.oasis.opendocument.graphics-flat-xml', + 'fodp' => 'application/vnd.oasis.opendocument.presentation-flat-xml', ]; return $this->updateMimetypes($updatedMimetypes); @@ -251,8 +252,8 @@ class RepairMimeTypes implements IRepairStep { */ private function introduceOnlyofficeFormType(): IResult|int|null { $updatedMimetypes = [ - "oform" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform", - "docxf" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf", + 'oform' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform', + 'docxf' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf', ]; return $this->updateMimetypes($updatedMimetypes); diff --git a/lib/private/Route/Route.php b/lib/private/Route/Route.php index 2fef3b10806..ab5a1f6b59a 100644 --- a/lib/private/Route/Route.php +++ b/lib/private/Route/Route.php @@ -128,7 +128,7 @@ class Route extends SymfonyRoute implements IRoute { */ public function actionInclude($file) { $function = function ($param) use ($file) { - unset($param["_route"]); + unset($param['_route']); $_GET = array_merge($_GET, $param); unset($param); require_once "$file"; diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index b04b6a4d21c..646d1d4e6ed 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -111,9 +111,14 @@ class Router implements IRouter { if ($this->loaded) { return; } + $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp); if (is_null($app)) { $this->loaded = true; $routingFiles = $this->getRoutingFiles(); + + foreach (\OC_App::getEnabledApps() as $enabledApp) { + $this->loadAttributeRoutes($enabledApp); + } } else { if (isset($this->loadedApps[$app])) { return; @@ -125,21 +130,9 @@ class Router implements IRouter { } else { $routingFiles = []; } - } - $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp); - - if ($requestedApp !== null && in_array($requestedApp, \OC_App::getEnabledApps())) { - $routes = $this->getAttributeRoutes($requestedApp); - if (count($routes) > 0) { - $this->useCollection($requestedApp); - $this->setupRoutes($routes, $requestedApp); - $collection = $this->getCollection($requestedApp); - $this->root->addCollection($collection); - // Also add the OCS collection - $collection = $this->getCollection($requestedApp . '.ocs'); - $collection->addPrefix('/ocsapp'); - $this->root->addCollection($collection); + if ($this->appManager->isEnabledForUser($app)) { + $this->loadAttributeRoutes($app); } } @@ -413,6 +406,23 @@ class Router implements IRouter { return $routeName; } + private function loadAttributeRoutes(string $app): void { + $routes = $this->getAttributeRoutes($app); + if (count($routes) === 0) { + return; + } + + $this->useCollection($app); + $this->setupRoutes($routes, $app); + $collection = $this->getCollection($app); + $this->root->addCollection($collection); + + // Also add the OCS collection + $collection = $this->getCollection($app . '.ocs'); + $collection->addPrefix('/ocsapp'); + $this->root->addCollection($collection); + } + /** * @throws ReflectionException */ diff --git a/lib/private/Search/FilterFactory.php b/lib/private/Search/FilterFactory.php index 1317dd759af..1466042291d 100644 --- a/lib/private/Search/FilterFactory.php +++ b/lib/private/Search/FilterFactory.php @@ -27,7 +27,7 @@ final class FilterFactory { 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), + FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array)$filter), default => throw new RuntimeException('Invalid filter type '. $type), }; } diff --git a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php index 0e272d94d0d..33c2a3aae62 100644 --- a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php +++ b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php @@ -45,7 +45,7 @@ class DatabaseBackend implements IBackend { $row = $result->fetch(); $result->closeCursor(); - return (int) $row['attempts']; + return (int)$row['attempts']; } /** diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 7e310035ce4..596fcf408fa 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -176,7 +176,7 @@ class Throttler implements IThrottler { return 0; } - $maxAgeTimestamp = (int) ($this->timeFactory->getTime() - 3600 * $maxAgeHours); + $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours); return $this->backend->getAttempts( $ipAddress->getSubnet(), @@ -204,7 +204,7 @@ class Throttler implements IThrottler { if ($delay > self::MAX_DELAY) { return self::MAX_DELAY_MS; } - return (int) \ceil($delay * 1000); + return (int)\ceil($delay * 1000); } /** diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php index 0f637e5afd6..993f74ae0e4 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php +++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php @@ -30,7 +30,11 @@ class ContentSecurityPolicyNonceManager { public function getNonce(): string { if ($this->nonce === '') { if (empty($this->request->server['CSP_NONCE'])) { - $this->nonce = base64_encode($this->csrfTokenManager->getToken()->getEncryptedValue()); + // Get the token from the CSRF token, we only use the "shared secret" part + // as the first part does not add any security / entropy to the token + // so it can be ignored to keep the nonce short while keeping the same randomness + $csrfSecret = explode(':', ($this->csrfTokenManager->getToken()->getEncryptedValue())); + $this->nonce = end($csrfSecret); } else { $this->nonce = $this->request->server['CSP_NONCE']; } diff --git a/lib/private/Security/Ip/Range.php b/lib/private/Security/Ip/Range.php index 4f960166d6a..39c03677f81 100644 --- a/lib/private/Security/Ip/Range.php +++ b/lib/private/Security/Ip/Range.php @@ -30,7 +30,7 @@ class Range implements IRange { } public function contains(IAddress $address): bool { - return $this->range->contains(Factory::parseAddressString((string) $address)); + return $this->range->contains(Factory::parseAddressString((string)$address)); } public function __toString(): string { diff --git a/lib/private/Security/Ip/RemoteAddress.php b/lib/private/Security/Ip/RemoteAddress.php index 54cdb96132a..cef511099b1 100644 --- a/lib/private/Security/Ip/RemoteAddress.php +++ b/lib/private/Security/Ip/RemoteAddress.php @@ -66,6 +66,6 @@ class RemoteAddress implements IRemoteAddress, IAddress { } public function __toString(): string { - return (string) $this->ip; + return (string)$this->ip; } } diff --git a/lib/private/Security/SecureRandom.php b/lib/private/Security/SecureRandom.php index 459d43475b7..b2a3d19ce74 100644 --- a/lib/private/Security/SecureRandom.php +++ b/lib/private/Security/SecureRandom.php @@ -24,7 +24,7 @@ class SecureRandom implements ISecureRandom { * Generate a secure random string of specified length. * @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. + * specified all valid base64 characters are used. * @throws \LengthException if an invalid length is requested */ public function generate( diff --git a/lib/private/Server.php b/lib/private/Server.php index a0072b43ee2..01d5bdac0b6 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -637,44 +637,51 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService(Factory::class, function (Server $c) { $profiler = $c->get(IProfiler::class); - $arrayCacheFactory = new \OC\Memcache\Factory('', $c->get(LoggerInterface::class), + $arrayCacheFactory = new \OC\Memcache\Factory(fn () => '', $c->get(LoggerInterface::class), $profiler, ArrayCache::class, ArrayCache::class, ArrayCache::class ); - /** @var \OCP\IConfig $config */ - $config = $c->get(\OCP\IConfig::class); - - if ($config->getSystemValueBool('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { - if (!$config->getSystemValueBool('log_query')) { - try { - $v = \OC_App::getAppVersions(); - } catch (\Doctrine\DBAL\Exception $e) { - // Database service probably unavailable - // Probably related to https://github.com/nextcloud/server/issues/37424 - return $arrayCacheFactory; + /** @var SystemConfig $config */ + $config = $c->get(SystemConfig::class); + + if ($config->getValue('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { + $logQuery = $config->getValue('log_query'); + $prefixClosure = function () use ($logQuery) { + if (!$logQuery) { + try { + $v = \OC_App::getAppVersions(); + } catch (\Doctrine\DBAL\Exception $e) { + // Database service probably unavailable + // Probably related to https://github.com/nextcloud/server/issues/37424 + return null; + } + } else { + // If the log_query is enabled, we can not get the app versions + // as that does a query, which will be logged and the logging + // depends on redis and here we are back again in the same function. + $v = [ + 'log_query' => 'enabled', + ]; } - } else { - // If the log_query is enabled, we can not get the app versions - // as that does a query, which will be logged and the logging - // depends on redis and here we are back again in the same function. - $v = [ - 'log_query' => 'enabled', - ]; - } - $v['core'] = implode(',', \OC_Util::getVersion()); - $version = implode(',', $v); - $instanceId = \OC_Util::getInstanceId(); - $path = \OC::$SERVERROOT; - $prefix = md5($instanceId . '-' . $version . '-' . $path); - return new \OC\Memcache\Factory($prefix, + $v['core'] = implode(',', \OC_Util::getVersion()); + $version = implode(',', $v); + $instanceId = \OC_Util::getInstanceId(); + $path = \OC::$SERVERROOT; + return md5($instanceId . '-' . $version . '-' . $path); + }; + return new \OC\Memcache\Factory($prefixClosure, $c->get(LoggerInterface::class), $profiler, - $config->getSystemValue('memcache.local', null), - $config->getSystemValue('memcache.distributed', null), - $config->getSystemValue('memcache.locking', null), - $config->getSystemValueString('redis_log_file') + /** @psalm-taint-escape callable */ + $config->getValue('memcache.local', null), + /** @psalm-taint-escape callable */ + $config->getValue('memcache.distributed', null), + /** @psalm-taint-escape callable */ + $config->getValue('memcache.locking', null), + /** @psalm-taint-escape callable */ + $config->getValue('redis_log_file') ); } return $arrayCacheFactory; @@ -735,7 +742,7 @@ class Server extends ServerContainer implements IServerContainer { $logger = $factory->get($logType); $registry = $c->get(\OCP\Support\CrashReport\IRegistry::class); - return new Log($logger, $this->get(SystemConfig::class), null, $registry); + return new Log($logger, $this->get(SystemConfig::class), crashReporters: $registry); }); $this->registerAlias(ILogger::class, \OC\Log::class); /** @deprecated 19.0.0 */ @@ -804,7 +811,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(IDBConnection::class, ConnectionAdapter::class); $this->registerService(Connection::class, function (Server $c) { $systemConfig = $c->get(SystemConfig::class); - $factory = new \OC\DB\ConnectionFactory($systemConfig); + $factory = new \OC\DB\ConnectionFactory($systemConfig, $c->get(ICacheFactory::class)); $type = $systemConfig->getValue('dbtype', 'sqlite'); if (!$factory->isValidType($type)) { throw new \OC\DatabaseException('Invalid database type'); diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php index 5398dc710af..b465bcd3eda 100644 --- a/lib/private/Session/Internal.php +++ b/lib/private/Session/Internal.php @@ -29,8 +29,10 @@ class Internal extends Session { * @param string $name * @throws \Exception */ - public function __construct(string $name, - private LoggerInterface $logger) { + public function __construct( + string $name, + private ?LoggerInterface $logger, + ) { set_error_handler([$this, 'trapError']); $this->invoke('session_name', [$name]); $this->invoke('session_cache_limiter', ['']); @@ -204,7 +206,7 @@ class Internal extends Session { $timeSpent > 0.5 => ILogger::INFO, default => ILogger::DEBUG, }; - $this->logger->log( + $this->logger?->log( $logLevel, "Slow session operation $functionName detected", [ diff --git a/lib/private/Settings/Section.php b/lib/private/Settings/Section.php index 4f8234254b1..9cc6523b9ae 100644 --- a/lib/private/Settings/Section.php +++ b/lib/private/Settings/Section.php @@ -32,7 +32,7 @@ class Section implements IIconSection { /** * @return string The ID of the section. It is supposed to be a lower case string, - * e.g. 'ldap' + * e.g. 'ldap' */ public function getID() { return $this->id; @@ -40,7 +40,7 @@ class Section implements IIconSection { /** * @return string The translated name as it should be displayed, e.g. 'LDAP / AD - * integration'. Use the L10N service to translate it. + * integration'. Use the L10N service to translate it. */ public function getName() { return $this->name; @@ -48,8 +48,8 @@ class Section implements IIconSection { /** * @return int whether the form should be rather on the top or bottom of - * the settings navigation. The sections are arranged in ascending order of - * the priority values. It is required to return a value between 0 and 99. + * the settings navigation. The sections are arranged in ascending order of + * the priority values. It is required to return a value between 0 and 99. * * E.g.: 70 */ @@ -59,7 +59,7 @@ class Section implements IIconSection { /** * @return string The relative path to an 16*16 icon describing the section. - * e.g. '/core/img/places/files.svg' + * e.g. '/core/img/places/files.svg' * * @since 12 */ diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 62db4879bbc..6212a561abb 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -147,7 +147,7 @@ class Setup { * a few system checks. * * @return array of system info, including an "errors" value - * in case of errors/warnings + * in case of errors/warnings */ public function getSystemInfo(bool $allowAllDatabases = false): array { $databases = $this->getSupportedDatabases($allowAllDatabases); @@ -230,7 +230,7 @@ class Setup { $error[] = $l->t('Set an admin password.'); } if (empty($options['directory'])) { - $options['directory'] = \OC::$SERVERROOT . "/data"; + $options['directory'] = \OC::$SERVERROOT . '/data'; } if (!isset(self::$dbSetupClasses[$dbType])) { @@ -248,7 +248,7 @@ class Setup { // validate the data directory if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) { - $error[] = $l->t("Cannot create or write into the data directory %s", [$dataDir]); + $error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]); } if (!empty($error)) { @@ -392,7 +392,7 @@ class Setup { $userSession->login($username, $password); $user = $userSession->getUser(); if (!$user) { - $error[] = "No account found in session."; + $error[] = 'No account found in session.'; return $error; } $userSession->createSessionToken($request, $user->getUID(), $username, $password); @@ -509,7 +509,7 @@ class Setup { $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!"); + 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 @@ -542,7 +542,7 @@ class Setup { $content .= "# Section for Apache 2.2 to 2.6\n"; $content .= "<IfModule mod_autoindex.c>\n"; $content .= " IndexIgnore *\n"; - $content .= "</IfModule>"; + $content .= '</IfModule>'; $baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data'); file_put_contents($baseDir . '/.htaccess', $content); diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index e96ca8a2f3e..b1d93f55cc0 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -50,14 +50,14 @@ abstract class AbstractDatabase { public function validate($config) { $errors = []; if (empty($config['dbuser']) && empty($config['dbname'])) { - $errors[] = $this->trans->t("Enter the database Login and name for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database Login and name for %s', [$this->dbprettyname]); } elseif (empty($config['dbuser'])) { - $errors[] = $this->trans->t("Enter the database Login for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database Login for %s', [$this->dbprettyname]); } elseif (empty($config['dbname'])) { - $errors[] = $this->trans->t("Enter the database name for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database name for %s', [$this->dbprettyname]); } if (substr_count($config['dbname'], '.') >= 1) { - $errors[] = $this->trans->t("You cannot use dots in the database name %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('You cannot use dots in the database name %s', [$this->dbprettyname]); } return $errors; } @@ -70,9 +70,9 @@ abstract class AbstractDatabase { $dbPort = !empty($config['dbport']) ? $config['dbport'] : ''; $dbTablePrefix = $config['dbtableprefix'] ?? 'oc_'; - $createUserConfig = $this->config->getValue("setup_create_db_user", true); + $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 - $this->tryCreateDbUser = $createUserConfig !== false && $createUserConfig !== "false"; + $this->tryCreateDbUser = $createUserConfig !== false && $createUserConfig !== 'false'; $this->config->setValues([ 'dbname' => $dbName, @@ -133,7 +133,7 @@ abstract class AbstractDatabase { abstract public function setupDatabase($username); public function runMigrations(?IOutput $output = null) { - if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) { + if (!is_dir(\OC::$SERVERROOT.'/core/Migrations')) { return; } $ms = new MigrationService('core', \OC::$server->get(Connection::class), $output); diff --git a/lib/private/Setup/OCI.php b/lib/private/Setup/OCI.php index 3a0fa34d8d1..47e5e5436a5 100644 --- a/lib/private/Setup/OCI.php +++ b/lib/private/Setup/OCI.php @@ -31,11 +31,11 @@ class OCI extends AbstractDatabase { public function validate($config) { $errors = []; if (empty($config['dbuser']) && empty($config['dbname'])) { - $errors[] = $this->trans->t("Enter the database Login and name for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database Login and name for %s', [$this->dbprettyname]); } elseif (empty($config['dbuser'])) { - $errors[] = $this->trans->t("Enter the database Login for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database Login for %s', [$this->dbprettyname]); } elseif (empty($config['dbname'])) { - $errors[] = $this->trans->t("Enter the database name for %s", [$this->dbprettyname]); + $errors[] = $this->trans->t('Enter the database name for %s', [$this->dbprettyname]); } return $errors; } diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php index 4ece8957ce6..73f2dfe0623 100644 --- a/lib/private/Setup/PostgreSQL.php +++ b/lib/private/Setup/PostgreSQL.php @@ -51,16 +51,6 @@ class PostgreSQL extends AbstractDatabase { $this->dbPassword = \OC::$server->get(ISecureRandom::class)->generate(30, ISecureRandom::CHAR_ALPHANUMERIC); $this->createDBUser($connection); - - // Go to the main database and grant create on the public schema - // The code below is implemented to make installing possible with PostgreSQL version 15: - // https://www.postgresql.org/docs/release/15.0/ - // From the release notes: For new databases having no need to defend against insider threats, granting CREATE permission will yield the behavior of prior releases - // Therefore we assume that the database is only used by one user/service which is Nextcloud - // Additional services should get installed in a separate database in order to stay secure - // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS - $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO "' . addslashes($this->dbUser) . '"'); - $connectionMainDatabase->close(); } } @@ -73,6 +63,20 @@ class PostgreSQL extends AbstractDatabase { $this->createDatabase($connection); // the connection to dbname=postgres is not needed anymore $connection->close(); + + if ($this->tryCreateDbUser) { + if ($canCreateRoles) { + // Go to the main database and grant create on the public schema + // The code below is implemented to make installing possible with PostgreSQL version 15: + // https://www.postgresql.org/docs/release/15.0/ + // From the release notes: For new databases having no need to defend against insider threats, granting CREATE permission will yield the behavior of prior releases + // Therefore we assume that the database is only used by one user/service which is Nextcloud + // Additional services should get installed in a separate database in order to stay secure + // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS + $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO "' . addslashes($this->dbUser) . '"'); + $connectionMainDatabase->close(); + } + } } catch (\Exception $e) { $this->logger->warning('Error trying to connect as "postgres", assuming database is setup and tables need to be created', [ 'exception' => $e, @@ -101,7 +105,7 @@ class PostgreSQL extends AbstractDatabase { private function createDatabase(Connection $connection) { if (!$this->databaseExists($connection)) { //The database does not exists... let's create it - $query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER \"" . addslashes($this->dbUser) . '"'); + $query = $connection->prepare('CREATE DATABASE ' . addslashes($this->dbName) . ' OWNER "' . addslashes($this->dbUser) . '"'); try { $query->execute(); } catch (DatabaseException $e) { @@ -110,7 +114,7 @@ class PostgreSQL extends AbstractDatabase { ]); } } else { - $query = $connection->prepare("REVOKE ALL PRIVILEGES ON DATABASE " . addslashes($this->dbName) . " FROM PUBLIC"); + $query = $connection->prepare('REVOKE ALL PRIVILEGES ON DATABASE ' . addslashes($this->dbName) . ' FROM PUBLIC'); try { $query->execute(); } catch (DatabaseException $e) { @@ -151,7 +155,7 @@ class PostgreSQL extends AbstractDatabase { } // create the user - $query = $connection->prepare("CREATE USER \"" . addslashes($this->dbUser) . "\" CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); + $query = $connection->prepare('CREATE USER "' . addslashes($this->dbUser) . "\" CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); $query->execute(); if ($this->databaseExists($connection)) { $query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO "' . addslashes($this->dbUser) . '"'); diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 366b9cad976..3ea429dfe3d 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -28,6 +28,7 @@ use OCP\L10N\IFactory; use OCP\Mail\IMailer; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IAttributes; +use OCP\Share\IManager; use OCP\Share\IShare; use OCP\Share\IShareProviderSupportsAccept; use OCP\Share\IShareProviderWithNotification; @@ -54,6 +55,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv private IURLGenerator $urlGenerator, private ITimeFactory $timeFactory, private LoggerInterface $logger, + private IManager $shareManager, ) { } @@ -95,6 +97,8 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv if ($expirationDate !== null) { $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); } + + $qb->setValue('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL)); } elseif ($share->getShareType() === IShare::TYPE_GROUP) { //Set the GID of the group we share with $qb->setValue('share_with', $qb->createNamedParameter($share->getSharedWith())); @@ -221,6 +225,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATE)) ->set('note', $qb->createNamedParameter($share->getNote())) ->set('accepted', $qb->createNamedParameter($share->getStatus())) + ->set('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL)) ->execute(); } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $qb = $this->dbConn->getQueryBuilder(); @@ -607,7 +612,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) { if (!$shallow) { - throw new \Exception("non-shallow getSharesInFolder is no longer supported"); + throw new \Exception('non-shallow getSharesInFolder is no longer supported'); } $qb = $this->dbConn->getQueryBuilder(); @@ -670,6 +675,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv foreach ($chunks as $chunk) { $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $a = $qb->getSQL(); $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { $shares[$data['fileid']][] = $this->createShare($data); @@ -771,7 +777,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv // If the recipient is set for a group share resolve to that user if ($recipientId !== null && $share->getShareType() === IShare::TYPE_GROUP) { - $share = $this->resolveGroupShares([(int) $share->getId() => $share], $recipientId)[0]; + $share = $this->resolveGroupShares([(int)$share->getId() => $share], $recipientId)[0]; } return $share; @@ -880,9 +886,9 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv while ($data = $cursor->fetch()) { if ($data['fileid'] && $data['path'] === null) { - $data['path'] = (string) $data['path']; - $data['name'] = (string) $data['name']; - $data['checksum'] = (string) $data['checksum']; + $data['path'] = (string)$data['path']; + $data['name'] = (string)$data['name']; + $data['checksum'] = (string)$data['checksum']; } if ($this->isAccessibleResult($data)) { $shares[] = $this->createShare($data); @@ -1003,7 +1009,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv } /** - * Create a share object from an database row + * Create a share object from a database row * * @param mixed[] $data * @return \OCP\Share\IShare @@ -1065,6 +1071,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv $share->setProviderId($this->identifier()); $share->setHideDownload((int)$data['hide_download'] === 1); + $share->setReminderSent((bool)$data['reminder_sent']); return $share; } @@ -1200,10 +1207,14 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv if (!empty($ids)) { $chunks = array_chunk($ids, 100); + + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents'))); + foreach ($chunks as $chunk) { - $qb->delete('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) - ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY); $qb->execute(); } } @@ -1223,6 +1234,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv * * @param string $uid * @param string $gid + * @return void */ public function userDeletedFromGroup($uid, $gid) { /* @@ -1234,7 +1246,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $ids = []; while ($row = $cursor->fetch()) { $ids[] = (int)$row['id']; @@ -1243,15 +1255,58 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv if (!empty($ids)) { $chunks = array_chunk($ids, 100); + + /* + * Delete all special shares with this user for the found group shares + */ + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents'))); + foreach ($chunks as $chunk) { - /* - * Delete all special shares with this users for the found group shares - */ - $qb->delete('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) - ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($uid))) - ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); - $qb->execute(); + $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); + } + } + + if ($this->shareManager->shareWithGroupMembersOnly()) { + $user = $this->userManager->get($uid); + if ($user === null) { + return; + } + $userGroups = $this->groupManager->getUserGroupIds($user); + $userGroups = array_diff($userGroups, $this->shareManager->shareWithGroupMembersOnlyExcludeGroupsList()); + + // Delete user shares received by the user from users in the group. + $userReceivedShares = $this->shareManager->getSharedWith($uid, IShare::TYPE_USER, null, -1); + foreach ($userReceivedShares as $share) { + $owner = $this->userManager->get($share->getSharedBy()); + if ($owner === null) { + continue; + } + $ownerGroups = $this->groupManager->getUserGroupIds($owner); + $mutualGroups = array_intersect($userGroups, $ownerGroups); + + if (count($mutualGroups) === 0) { + $this->shareManager->deleteShare($share); + } + } + + // Delete user shares from the user to users in the group. + $userEmittedShares = $this->shareManager->getSharesBy($uid, IShare::TYPE_USER, null, true, -1); + foreach ($userEmittedShares as $share) { + $recipient = $this->userManager->get($share->getSharedWith()); + if ($recipient === null) { + continue; + } + $recipientGroups = $this->groupManager->getUserGroupIds($recipient); + $mutualGroups = array_intersect($userGroups, $recipientGroups); + + if (count($mutualGroups) === 0) { + $this->shareManager->deleteShare($share); + } } } } @@ -1339,8 +1394,8 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv protected function filterSharesOfUser(array $shares) { // Group shares when the user has a share exception foreach ($shares as $id => $share) { - $type = (int) $share['share_type']; - $permissions = (int) $share['permissions']; + $type = (int)$share['share_type']; + $permissions = (int)$share['permissions']; if ($type === IShare::TYPE_USERGROUP) { unset($shares[$share['parent']]); diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index e76f4586dfd..3e085e08d7d 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -11,6 +11,7 @@ use OC\Files\Mount\MoveableMount; use OC\KnownUser\KnownUserService; use OC\Share20\Exception\ProviderException; use OCA\Files_Sharing\AppInfo\Application; +use OCA\Files_Sharing\SharedStorage; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\Folder; @@ -570,7 +571,7 @@ class Manager implements IManager { // No sense in checking if the method is not there. if (method_exists($share, 'setParent')) { $storage = $share->getNode()->getStorage(); - if ($storage->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) { + if ($storage->instanceOfStorage(SharedStorage::class)) { /** @var \OCA\Files_Sharing\SharedStorage $storage */ $share->setParent($storage->getShareId()); } @@ -931,7 +932,7 @@ class Manager implements IManager { * * @param IShare $share the share to update its password. * @param IShare $originalShare the original share to compare its - * password with. + * password with. * @return boolean whether the password was updated or not. */ private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) { @@ -1101,7 +1102,7 @@ class Manager implements IManager { public function getSharesInFolder($userId, Folder $node, $reshares = false, $shallow = true) { $providers = $this->factory->getAllProviders(); if (!$shallow) { - throw new \Exception("non-shallow getSharesInFolder is no longer supported"); + throw new \Exception('non-shallow getSharesInFolder is no longer supported'); } return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) { diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index dbe251a49df..e1a2c9a5375 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -44,9 +44,9 @@ class ProviderFactory implements IProviderFactory { private $defaultProvider = null; /** @var FederatedShareProvider */ private $federatedProvider = null; - /** @var ShareByMailProvider */ + /** @var ShareByMailProvider */ private $shareByMailProvider; - /** @var \OCA\Circles\ShareByCircleProvider */ + /** @var \OCA\Circles\ShareByCircleProvider */ private $shareByCircleProvider = null; /** @var bool */ private $circlesAreNotAvailable = false; @@ -88,6 +88,7 @@ class ProviderFactory implements IProviderFactory { $this->serverContainer->getURLGenerator(), $this->serverContainer->query(ITimeFactory::class), $this->serverContainer->get(LoggerInterface::class), + $this->serverContainer->get(IManager::class), ); } diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index ac95e3ac0d4..1c0b21b8038 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -72,6 +72,7 @@ class Share implements IShare { private $nodeCacheEntry; /** @var bool */ private $hideDownload = false; + private bool $reminderSent = false; private bool $noExpirationDate = false; @@ -191,7 +192,7 @@ class Share implements IShare { } if ($this->fileId === null) { - throw new NotFoundException("Share source not found"); + throw new NotFoundException('Share source not found'); } else { return $this->fileId; } @@ -613,4 +614,13 @@ class Share implements IShare { public function getHideDownload(): bool { return $this->hideDownload; } + + public function setReminderSent(bool $reminderSent): IShare { + $this->reminderSent = $reminderSent; + return $this; + } + + public function getReminderSent(): bool { + return $this->reminderSent; + } } diff --git a/lib/private/Share20/ShareAttributes.php b/lib/private/Share20/ShareAttributes.php index fbdcbf1ad26..96da1e336e3 100644 --- a/lib/private/Share20/ShareAttributes.php +++ b/lib/private/Share20/ShareAttributes.php @@ -47,9 +47,9 @@ class ShareAttributes implements IAttributes { foreach ($this->attributes as $scope => $keys) { foreach ($keys as $key => $value) { $result[] = [ - "scope" => $scope, - "key" => $key, - "value" => $value, + 'scope' => $scope, + 'key' => $key, + 'value' => $value, ]; } } diff --git a/lib/private/SpeechToText/SpeechToTextManager.php b/lib/private/SpeechToText/SpeechToTextManager.php index d6cda473875..d69add2d80b 100644 --- a/lib/private/SpeechToText/SpeechToTextManager.php +++ b/lib/private/SpeechToText/SpeechToTextManager.php @@ -24,6 +24,9 @@ use OCP\SpeechToText\ISpeechToTextManager; use OCP\SpeechToText\ISpeechToTextProvider; use OCP\SpeechToText\ISpeechToTextProviderWithId; use OCP\SpeechToText\ISpeechToTextProviderWithUserId; +use OCP\TaskProcessing\IManager as ITaskProcessingManager; +use OCP\TaskProcessing\Task; +use OCP\TaskProcessing\TaskTypes\AudioToText; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; @@ -41,6 +44,7 @@ class SpeechToTextManager implements ISpeechToTextManager { private IJobList $jobList, private IConfig $config, private IUserSession $userSession, + private ITaskProcessingManager $taskProcessingManager, ) { } @@ -112,7 +116,30 @@ class SpeechToTextManager implements ISpeechToTextManager { } } - public function transcribeFile(File $file): string { + public function transcribeFile(File $file, ?string $userId = null, string $appId = 'core'): string { + // try to run a TaskProcessing core:audio2text task + // this covers scheduling as well because OC\SpeechToText\TranscriptionJob calls this method + try { + if (isset($this->taskProcessingManager->getAvailableTaskTypes()['core:audio2text'])) { + $taskProcessingTask = new Task( + AudioToText::ID, + ['input' => $file->getId()], + $appId, + $userId, + 'from-SpeechToTextManager||' . $file->getId() . '||' . ($userId ?? '') . '||' . $appId, + ); + $resultTask = $this->taskProcessingManager->runTask($taskProcessingTask); + if ($resultTask->getStatus() === Task::STATUS_SUCCESSFUL) { + $output = $resultTask->getOutput(); + if (isset($output['output']) && is_string($output['output'])) { + return $output['output']; + } + } + } + } catch (Throwable $e) { + throw new RuntimeException('Failed to run a Speech-to-text job from STTManager with TaskProcessing for file ' . $file->getId(), 0, $e); + } + if (!$this->hasProviders()) { throw new PreConditionNotMetException('No SpeechToText providers have been registered'); } diff --git a/lib/private/SpeechToText/TranscriptionJob.php b/lib/private/SpeechToText/TranscriptionJob.php index a46fd737865..6e899ef6e96 100644 --- a/lib/private/SpeechToText/TranscriptionJob.php +++ b/lib/private/SpeechToText/TranscriptionJob.php @@ -63,7 +63,7 @@ class TranscriptionJob extends QueuedJob { ); return; } - $result = $this->speechToTextManager->transcribeFile($file); + $result = $this->speechToTextManager->transcribeFile($file, $userId, $appId); $this->eventDispatcher->dispatchTyped( new TranscriptionSuccessfulEvent( $fileId, diff --git a/lib/private/StreamImage.php b/lib/private/StreamImage.php index 9290bf38b0f..34fe590efbd 100644 --- a/lib/private/StreamImage.php +++ b/lib/private/StreamImage.php @@ -133,4 +133,12 @@ class StreamImage implements IStreamImage { public function resizeCopy(int $maxSize): IImage { throw new \BadMethodCallException('Not implemented'); } + + public function loadFromData(string $str): \GdImage|false { + throw new \BadMethodCallException('Not implemented'); + } + + public function readExif(string $data): void { + throw new \BadMethodCallException('Not implemented'); + } } diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php index 4eca1105002..1f510f730ee 100644 --- a/lib/private/Streamer.php +++ b/lib/private/Streamer.php @@ -32,7 +32,7 @@ class Streamer { * @param IRequest $request * @param int|float $size The size of the files in bytes * @param int $numberOfFiles The number of files (and directories) that will - * be included in the streamed file + * be included in the streamed file */ public function __construct(IRequest $request, int|float $size, int $numberOfFiles) { /** diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index f817e327b19..ed77526c29c 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -115,6 +115,24 @@ class SystemConfig { } /** + * Since system config is admin controlled, we can tell psalm to ignore any taint + * + * @psalm-taint-escape sql + * @psalm-taint-escape html + * @psalm-taint-escape ldap + * @psalm-taint-escape callable + * @psalm-taint-escape file + * @psalm-taint-escape ssrf + * @psalm-taint-escape cookie + * @psalm-taint-escape header + * @psalm-taint-escape has_quotes + * @psalm-pure + */ + public static function trustSystemConfig(mixed $value): mixed { + return $value; + } + + /** * Lists all available config keys * @return array an array of key names */ @@ -150,7 +168,7 @@ class SystemConfig { * @return mixed the value or $default */ public function getValue($key, $default = '') { - return $this->config->getValue($key, $default); + return $this->trustSystemConfig($this->config->getValue($key, $default)); } /** diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php index 10117eebeaa..157948e6e0c 100644 --- a/lib/private/SystemTag/SystemTagObjectMapper.php +++ b/lib/private/SystemTag/SystemTagObjectMapper.php @@ -234,7 +234,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { return ((int)$row[0] === \count($objIds)); } - return (bool) $row; + return (bool)$row; } /** diff --git a/lib/private/TagManager.php b/lib/private/TagManager.php index 3258ce50bc5..f99653f2c05 100644 --- a/lib/private/TagManager.php +++ b/lib/private/TagManager.php @@ -43,7 +43,7 @@ class TagManager implements ITagManager, IEventListener { * @param array $defaultTags An array of default tags to be used if none are stored. * @param boolean $includeShared Whether to include tags for items shared with this user by others. * @param string $userId user for which to retrieve the tags, defaults to the currently - * logged in user + * logged in user * @return \OCP\ITags * * since 20.0.0 $includeShared isn't used anymore diff --git a/lib/private/Tags.php b/lib/private/Tags.php index 420cabc36b9..20fd644132b 100644 --- a/lib/private/Tags.php +++ b/lib/private/Tags.php @@ -149,7 +149,7 @@ class Tags implements ITags { * * @param array $objIds array of object ids * @return array|false of tags id as key to array of tag names - * or false if an error occurred + * or false if an error occurred */ public function getTagsForObjects(array $objIds) { $entries = []; @@ -343,7 +343,7 @@ class Tags implements ITags { * Add a list of new tags. * * @param string|string[] $names A string with a name or an array of strings containing - * the name(s) of the tag(s) to add. + * the name(s) of the tag(s) to add. * @param bool $sync When true, save the tags * @param int|null $id int Optional object id to add to this|these tag(s) * @return bool Returns false on error. diff --git a/lib/private/Talk/Broker.php b/lib/private/Talk/Broker.php index fc8e0280043..86e7e7ff4c1 100644 --- a/lib/private/Talk/Broker.php +++ b/lib/private/Talk/Broker.php @@ -47,7 +47,7 @@ class Broker implements IBroker { $context = $this->coordinator->getRegistrationContext(); if ($context === null) { // Backend requested too soon, e.g. from the bootstrap `register` method of an app - throw new RuntimeException("Not all apps have been registered yet"); + throw new RuntimeException('Not all apps have been registered yet'); } $backendRegistration = $context->getTalkBackendRegistration(); if ($backendRegistration === null) { @@ -63,7 +63,7 @@ class Broker implements IBroker { // Remember and return return $this->hasBackend = true; } catch (Throwable $e) { - $this->logger->error("Talk backend {class} could not be loaded: " . $e->getMessage(), [ + $this->logger->error('Talk backend {class} could not be loaded: ' . $e->getMessage(), [ 'class' => $backendRegistration->getService(), 'exception' => $e, ]); @@ -81,7 +81,7 @@ class Broker implements IBroker { array $moderators, ?IConversationOptions $options = null): IConversation { if (!$this->hasBackend()) { - throw new NoBackendException("The Talk broker has no registered backend"); + throw new NoBackendException('The Talk broker has no registered backend'); } return $this->backend->createConversation( @@ -93,7 +93,7 @@ class Broker implements IBroker { public function deleteConversation(string $id): void { if (!$this->hasBackend()) { - throw new NoBackendException("The Talk broker has no registered backend"); + throw new NoBackendException('The Talk broker has no registered backend'); } $this->backend->deleteConversation($id); diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index a804115a631..fb0a4da4c4e 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -68,7 +68,7 @@ class Manager implements IManager { public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:'; public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:'; - /** @var list<IProvider>|null */ + /** @var list<IProvider>|null */ private ?array $providers = null; /** @@ -87,9 +87,7 @@ class Manager implements IManager { private IEventDispatcher $dispatcher, IAppDataFactory $appDataFactory, private IRootFolder $rootFolder, - private \OCP\TextProcessing\IManager $textProcessingManager, private \OCP\TextToImage\IManager $textToImageManager, - private \OCP\SpeechToText\ISpeechToTextManager $speechToTextManager, private IUserMountCache $userMountCache, private IClientService $clientService, private IAppManager $appManager, @@ -98,8 +96,34 @@ class Manager implements IManager { } + /** + * This is almost a copy of textProcessingManager->getProviders + * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager + */ + private function _getRawTextProcessingProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return []; + } + + $providers = []; + + foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) { + $class = $providerServiceRegistration->getService(); + try { + $providers[$class] = $this->serverContainer->get($class); + } catch (\Throwable $e) { + $this->logger->error('Failed to load Text processing provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return $providers; + } + private function _getTextProcessingProviders(): array { - $oldProviders = $this->textProcessingManager->getProviders(); + $oldProviders = $this->_getRawTextProcessingProviders(); $newProviders = []; foreach ($oldProviders as $oldProvider) { $provider = new class($oldProvider) implements IProvider, ISynchronousProvider { @@ -190,7 +214,7 @@ class Manager implements IManager { * @return ITaskType[] */ private function _getTextProcessingTaskTypes(): array { - $oldProviders = $this->textProcessingManager->getProviders(); + $oldProviders = $this->_getRawTextProcessingProviders(); $newTaskTypes = []; foreach ($oldProviders as $oldProvider) { // These are already implemented in the TaskProcessing realm @@ -344,12 +368,35 @@ class Manager implements IManager { return $newProviders; } + /** + * This is almost a copy of SpeechToTextManager->getProviders + * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager + */ + private function _getRawSpeechToTextProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return []; + } + $providers = []; + foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) { + $class = $providerServiceRegistration->getService(); + try { + $providers[$class] = $this->serverContainer->get($class); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) { + $this->logger->error('Failed to load SpeechToText provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return $providers; + } /** * @return IProvider[] */ private function _getSpeechToTextProviders(): array { - $oldProviders = $this->speechToTextManager->getProviders(); + $oldProviders = $this->_getRawSpeechToTextProviders(); $newProviders = []; foreach ($oldProviders as $oldProvider) { $newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider { @@ -649,24 +696,24 @@ class Manager implements IManager { return $this->providers; } - public function getPreferredProvider(string $taskType) { + public function getPreferredProvider(string $taskTypeId) { try { $preferences = json_decode($this->config->getAppValue('core', 'ai.taskprocessing_provider_preferences', 'null'), associative: true, flags: JSON_THROW_ON_ERROR); $providers = $this->getProviders(); - if (isset($preferences[$taskType])) { - $provider = current(array_values(array_filter($providers, fn ($provider) => $provider->getId() === $preferences[$taskType]))); + if (isset($preferences[$taskTypeId])) { + $provider = current(array_values(array_filter($providers, fn ($provider) => $provider->getId() === $preferences[$taskTypeId]))); if ($provider !== false) { return $provider; } } // By default, use the first available provider foreach ($providers as $provider) { - if ($provider->getTaskTypeId() === $taskType) { + if ($provider->getTaskTypeId() === $taskTypeId) { return $provider; } } } catch (\JsonException $e) { - $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskType, ['exception' => $e]); + $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]); } throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found'); } @@ -674,14 +721,14 @@ class Manager implements IManager { public function getAvailableTaskTypes(): array { if ($this->availableTaskTypes === null) { $taskTypes = $this->_getTaskTypes(); - $providers = $this->getProviders(); $availableTaskTypes = []; - foreach ($providers as $provider) { - if (!isset($taskTypes[$provider->getTaskTypeId()])) { + foreach ($taskTypes as $taskType) { + try { + $provider = $this->getPreferredProvider($taskType->getId()); + } catch (\OCP\TaskProcessing\Exception\Exception $e) { continue; } - $taskType = $taskTypes[$provider->getTaskTypeId()]; try { $availableTaskTypes[$provider->getTaskTypeId()] = [ 'name' => $taskType->getName(), @@ -716,55 +763,69 @@ class Manager implements IManager { if (!$this->canHandleTask($task)) { throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId()); } - $taskTypes = $this->getAvailableTaskTypes(); - $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape']; - $inputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['inputShapeDefaults']; - $inputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['inputShapeEnumValues']; - $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape']; - $optionalInputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeEnumValues']; - $optionalInputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeDefaults']; - // validate input - $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput()); - $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true); - // authenticate access to mentioned files - $ids = []; - foreach ($inputShape + $optionalInputShape as $key => $descriptor) { - if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { - /** @var list<int>|int $inputSlot */ - $inputSlot = $task->getInput()[$key]; - if (is_array($inputSlot)) { - $ids += $inputSlot; - } else { - $ids[] = $inputSlot; - } - } - } - foreach ($ids as $fileId) { - $this->validateFileId($fileId); - $this->validateUserAccessToFile($fileId, $task->getUserId()); - } - // remove superfluous keys and set input - $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape); - $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults); - $task->setInput($inputWithDefaults); + $this->prepareTask($task); $task->setStatus(Task::STATUS_SCHEDULED); - $task->setScheduledAt(time()); - $provider = $this->getPreferredProvider($task->getTaskTypeId()); - // calculate expected completion time - $completionExpectedAt = new \DateTime('now'); - $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S')); - $task->setCompletionExpectedAt($completionExpectedAt); - // create a db entity and insert into db table - $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task); - $this->taskMapper->insert($taskEntity); - // make sure the scheduler knows the id - $task->setId($taskEntity->getId()); + $this->storeTask($task); // schedule synchronous job if the provider is synchronous + $provider = $this->getPreferredProvider($task->getTaskTypeId()); if ($provider instanceof ISynchronousProvider) { $this->jobList->add(SynchronousBackgroundJob::class, null); } } + public function runTask(Task $task): Task { + if (!$this->canHandleTask($task)) { + throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId()); + } + + $provider = $this->getPreferredProvider($task->getTaskTypeId()); + if ($provider instanceof ISynchronousProvider) { + $this->prepareTask($task); + $task->setStatus(Task::STATUS_SCHEDULED); + $this->storeTask($task); + $this->processTask($task, $provider); + $task = $this->getTask($task->getId()); + } else { + $this->scheduleTask($task); + // poll task + while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) { + sleep(1); + $task = $this->getTask($task->getId()); + } + } + return $task; + } + + public function processTask(Task $task, ISynchronousProvider $provider): bool { + try { + try { + $input = $this->prepareInputData($task); + } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) { + $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]); + $this->setTaskResult($task->getId(), $e->getMessage(), null); + return false; + } + try { + $this->setTaskStatus($task, Task::STATUS_RUNNING); + $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress)); + } catch (ProcessingException $e) { + $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]); + $this->setTaskResult($task->getId(), $e->getMessage(), null); + return false; + } catch (\Throwable $e) { + $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]); + $this->setTaskResult($task->getId(), $e->getMessage(), null); + return false; + } + $this->setTaskResult($task->getId(), null, $output); + } catch (NotFoundException $e) { + $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]); + } catch (Exception $e) { + $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]); + } + return true; + } + public function deleteTask(Task $task): void { $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task); $this->taskMapper->delete($taskEntity); @@ -1096,6 +1157,72 @@ class Manager implements IManager { } /** + * Validate input, fill input default values, set completionExpectedAt, set scheduledAt + * + * @param Task $task + * @return void + * @throws UnauthorizedException + * @throws ValidationException + * @throws \OCP\TaskProcessing\Exception\Exception + */ + private function prepareTask(Task $task): void { + $taskTypes = $this->getAvailableTaskTypes(); + $taskType = $taskTypes[$task->getTaskTypeId()]; + $inputShape = $taskType['inputShape']; + $inputShapeDefaults = $taskType['inputShapeDefaults']; + $inputShapeEnumValues = $taskType['inputShapeEnumValues']; + $optionalInputShape = $taskType['optionalInputShape']; + $optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues']; + $optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults']; + // validate input + $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput()); + $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true); + // authenticate access to mentioned files + $ids = []; + foreach ($inputShape + $optionalInputShape as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var list<int>|int $inputSlot */ + $inputSlot = $task->getInput()[$key]; + if (is_array($inputSlot)) { + $ids += $inputSlot; + } else { + $ids[] = $inputSlot; + } + } + } + foreach ($ids as $fileId) { + $this->validateFileId($fileId); + $this->validateUserAccessToFile($fileId, $task->getUserId()); + } + // remove superfluous keys and set input + $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape); + $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults); + $task->setInput($inputWithDefaults); + $task->setScheduledAt(time()); + $provider = $this->getPreferredProvider($task->getTaskTypeId()); + // calculate expected completion time + $completionExpectedAt = new \DateTime('now'); + $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S')); + $task->setCompletionExpectedAt($completionExpectedAt); + } + + /** + * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param + * + * @param Task $task + * @return void + * @throws Exception + * @throws \JsonException + */ + private function storeTask(Task $task): void { + // create a db entity and insert into db table + $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task); + $this->taskMapper->insert($taskEntity); + // make sure the scheduler knows the id + $task->setId($taskEntity->getId()); + } + + /** * @param array $output * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep * @return array @@ -1189,9 +1316,9 @@ class Manager implements IManager { ]; try { $client->request($httpMethod, $uri, $options); - } catch (ClientException | ServerException $e) { + } catch (ClientException|ServerException $e) { $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]); - } catch (\Exception | \Throwable $e) { + } catch (\Exception|\Throwable $e) { $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]); } } elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) { diff --git a/lib/private/TaskProcessing/SynchronousBackgroundJob.php b/lib/private/TaskProcessing/SynchronousBackgroundJob.php index 093882d4c1e..de3b424176c 100644 --- a/lib/private/TaskProcessing/SynchronousBackgroundJob.php +++ b/lib/private/TaskProcessing/SynchronousBackgroundJob.php @@ -9,14 +9,8 @@ namespace OC\TaskProcessing; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\QueuedJob; -use OCP\Files\GenericFileException; -use OCP\Files\NotPermittedException; -use OCP\Lock\LockedException; use OCP\TaskProcessing\Exception\Exception; use OCP\TaskProcessing\Exception\NotFoundException; -use OCP\TaskProcessing\Exception\ProcessingException; -use OCP\TaskProcessing\Exception\UnauthorizedException; -use OCP\TaskProcessing\Exception\ValidationException; use OCP\TaskProcessing\IManager; use OCP\TaskProcessing\ISynchronousProvider; use OCP\TaskProcessing\Task; @@ -43,55 +37,41 @@ class SynchronousBackgroundJob extends QueuedJob { if (!$provider instanceof ISynchronousProvider) { continue; } - $taskType = $provider->getTaskTypeId(); + $taskTypeId = $provider->getTaskTypeId(); + // only use this provider if it is the preferred one + $preferredProvider = $this->taskProcessingManager->getPreferredProvider($taskTypeId); + if ($provider->getId() !== $preferredProvider->getId()) { + continue; + } try { - $task = $this->taskProcessingManager->getNextScheduledTask([$taskType]); + $task = $this->taskProcessingManager->getNextScheduledTask([$taskTypeId]); } catch (NotFoundException $e) { continue; } catch (Exception $e) { $this->logger->error('Unknown error while retrieving scheduled TaskProcessing tasks', ['exception' => $e]); continue; } - try { - try { - $input = $this->taskProcessingManager->prepareInputData($task); - } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) { - $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]); - $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null); - // Schedule again - $this->jobList->add(self::class, $argument); - return; - } - try { - $this->taskProcessingManager->setTaskStatus($task, Task::STATUS_RUNNING); - $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->taskProcessingManager->setTaskProgress($task->getId(), $progress)); - } catch (ProcessingException $e) { - $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]); - $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null); - // Schedule again - $this->jobList->add(self::class, $argument); - return; - } catch (\Throwable $e) { - $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]); - $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null); - // Schedule again - $this->jobList->add(self::class, $argument); - return; - } - $this->taskProcessingManager->setTaskResult($task->getId(), null, $output); - } catch (NotFoundException $e) { - $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]); - } catch (Exception $e) { - $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]); + if (!$this->taskProcessingManager->processTask($task, $provider)) { + // Schedule again + $this->jobList->add(self::class, $argument); } } + // check if this job needs to be scheduled again: + // if there is at least one preferred synchronous provider that has a scheduled task $synchronousProviders = array_filter($providers, fn ($provider) => $provider instanceof ISynchronousProvider); - $taskTypes = array_values(array_map(fn ($provider) => - $provider->getTaskTypeId(), - $synchronousProviders - )); + $synchronousPreferredProviders = array_filter($synchronousProviders, function ($provider) { + $taskTypeId = $provider->getTaskTypeId(); + $preferredProvider = $this->taskProcessingManager->getPreferredProvider($taskTypeId); + return $provider->getId() === $preferredProvider->getId(); + }); + $taskTypes = array_values( + array_map( + fn ($provider) => $provider->getTaskTypeId(), + $synchronousPreferredProviders + ) + ); $taskTypesWithTasks = array_filter($taskTypes, function ($taskType) { try { $this->taskProcessingManager->getNextScheduledTask([$taskType]); diff --git a/lib/private/TempManager.php b/lib/private/TempManager.php index 68a53f493db..74b4d6b1f24 100644 --- a/lib/private/TempManager.php +++ b/lib/private/TempManager.php @@ -138,7 +138,7 @@ class TempManager implements ITempManager { \OC_Helper::rmdirr($file); } catch (\UnexpectedValueException $ex) { $this->log->warning( - "Error deleting temporary file/folder: {file} - Reason: {error}", + 'Error deleting temporary file/folder: {file} - Reason: {error}', [ 'file' => $file, 'error' => $ex->getMessage(), diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php index 2adf9172f6b..b48c10143cf 100644 --- a/lib/private/Template/Base.php +++ b/lib/private/Template/Base.php @@ -24,11 +24,14 @@ class Base { * @param string $template * @param string $requestToken * @param \OCP\IL10N $l10n + * @param string $cspNonce * @param Defaults $theme */ - public function __construct($template, $requestToken, $l10n, $theme) { - $this->vars = []; - $this->vars['requesttoken'] = $requestToken; + public function __construct($template, $requestToken, $l10n, $theme, $cspNonce) { + $this->vars = [ + 'cspNonce' => $cspNonce, + 'requesttoken' => $requestToken, + ]; $this->l10n = $l10n; $this->template = $template; $this->theme = $theme; diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index eb253042f19..55fb348f01b 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -160,16 +160,16 @@ class JSConfigHelper { ]; $array = [ - "_oc_debug" => $this->config->getSystemValue('debug', false) ? 'true' : 'false', - "_oc_isadmin" => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false', - "backendAllowsPasswordConfirmation" => $userBackendAllowsPasswordConfirmation ? 'true' : 'false', - "oc_dataURL" => is_string($dataLocation) ? "\"" . $dataLocation . "\"" : 'false', - "_oc_webroot" => "\"" . \OC::$WEBROOT . "\"", - "_oc_appswebroots" => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution - "datepickerFormatDate" => json_encode($this->l->l('jsdate', null)), + '_oc_debug' => $this->config->getSystemValue('debug', false) ? 'true' : 'false', + '_oc_isadmin' => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false', + 'backendAllowsPasswordConfirmation' => $userBackendAllowsPasswordConfirmation ? 'true' : 'false', + 'oc_dataURL' => is_string($dataLocation) ? '"' . $dataLocation . '"' : 'false', + '_oc_webroot' => '"' . \OC::$WEBROOT . '"', + '_oc_appswebroots' => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution + 'datepickerFormatDate' => json_encode($this->l->l('jsdate', null)), 'nc_lastLogin' => $lastConfirmTimestamp, 'nc_pageLoad' => time(), - "dayNames" => json_encode([ + 'dayNames' => json_encode([ $this->l->t('Sunday'), $this->l->t('Monday'), $this->l->t('Tuesday'), @@ -178,7 +178,7 @@ class JSConfigHelper { $this->l->t('Friday'), $this->l->t('Saturday') ]), - "dayNamesShort" => json_encode([ + 'dayNamesShort' => json_encode([ $this->l->t('Sun.'), $this->l->t('Mon.'), $this->l->t('Tue.'), @@ -187,7 +187,7 @@ class JSConfigHelper { $this->l->t('Fri.'), $this->l->t('Sat.') ]), - "dayNamesMin" => json_encode([ + 'dayNamesMin' => json_encode([ $this->l->t('Su'), $this->l->t('Mo'), $this->l->t('Tu'), @@ -196,7 +196,7 @@ class JSConfigHelper { $this->l->t('Fr'), $this->l->t('Sa') ]), - "monthNames" => json_encode([ + 'monthNames' => json_encode([ $this->l->t('January'), $this->l->t('February'), $this->l->t('March'), @@ -210,7 +210,7 @@ class JSConfigHelper { $this->l->t('November'), $this->l->t('December') ]), - "monthNamesShort" => json_encode([ + 'monthNamesShort' => json_encode([ $this->l->t('Jan.'), $this->l->t('Feb.'), $this->l->t('Mar.'), @@ -224,9 +224,9 @@ class JSConfigHelper { $this->l->t('Nov.'), $this->l->t('Dec.') ]), - "firstDay" => json_encode($firstDay), - "_oc_config" => json_encode($config), - "oc_appconfig" => json_encode([ + 'firstDay' => json_encode($firstDay), + '_oc_config' => json_encode($config), + 'oc_appconfig' => json_encode([ 'core' => [ 'defaultExpireDateEnabled' => $defaultExpireDateEnabled, 'defaultExpireDate' => $defaultExpireDate, @@ -246,7 +246,7 @@ class JSConfigHelper { 'defaultRemoteExpireDateEnforced' => $defaultRemoteExpireDateEnforced, ] ]), - "_theme" => json_encode([ + '_theme' => json_encode([ 'entity' => $this->defaults->getEntity(), 'name' => $this->defaults->getName(), 'productName' => $this->defaults->getProductName(), diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 7b33f88d4db..2e397d2a3e2 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -8,6 +8,7 @@ namespace OC; use bantu\IniGetWrapper\IniGetWrapper; +use OC\AppFramework\Http\Request; use OC\Authentication\Token\IProvider; use OC\Files\FilenameValidator; use OC\Search\SearchQuery; @@ -20,6 +21,7 @@ use OCP\Defaults; use OCP\IConfig; use OCP\IInitialStateService; use OCP\INavigationManager; +use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; use OCP\L10N\IFactory; @@ -286,6 +288,13 @@ class TemplateLayout extends \OC_Template { } } + $request = \OCP\Server::get(IRequest::class); + if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) { + // Prevent auto zoom with iOS but still allow user zoom + // On chrome (and others) this does not work (will also disable user zoom) + $this->assign('viewport_maximum_scale', '1.0'); + } + $this->assign('initialStates', $this->initialState->getInitialStates()); $this->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content'); @@ -300,7 +309,7 @@ class TemplateLayout extends \OC_Template { protected function getVersionHashSuffix($path = false, $file = false) { if ($this->config->getSystemValueBool('debug', false)) { // allows chrome workspace mapping in debug mode - return ""; + return ''; } $themingSuffix = ''; $v = []; diff --git a/lib/private/TextProcessing/Manager.php b/lib/private/TextProcessing/Manager.php index a03c028a5c9..9801a99ddec 100644 --- a/lib/private/TextProcessing/Manager.php +++ b/lib/private/TextProcessing/Manager.php @@ -20,13 +20,22 @@ use OCP\DB\Exception; use OCP\IConfig; use OCP\IServerContainer; use OCP\PreConditionNotMetException; +use OCP\TaskProcessing\IManager as TaskProcessingIManager; +use OCP\TaskProcessing\TaskTypes\TextToText; +use OCP\TaskProcessing\TaskTypes\TextToTextHeadline; +use OCP\TaskProcessing\TaskTypes\TextToTextSummary; +use OCP\TaskProcessing\TaskTypes\TextToTextTopics; use OCP\TextProcessing\Exception\TaskFailureException; +use OCP\TextProcessing\FreePromptTaskType; +use OCP\TextProcessing\HeadlineTaskType; use OCP\TextProcessing\IManager; use OCP\TextProcessing\IProvider; use OCP\TextProcessing\IProviderWithExpectedRuntime; use OCP\TextProcessing\IProviderWithId; +use OCP\TextProcessing\SummaryTaskType; use OCP\TextProcessing\Task; use OCP\TextProcessing\Task as OCPTask; +use OCP\TextProcessing\TopicsTaskType; use Psr\Log\LoggerInterface; use RuntimeException; use Throwable; @@ -42,6 +51,7 @@ class Manager implements IManager { private IJobList $jobList, private TaskMapper $taskMapper, private IConfig $config, + private TaskProcessingIManager $taskProcessingManager, ) { } @@ -98,6 +108,55 @@ class Manager implements IManager { * @inheritDoc */ public function runTask(OCPTask $task): string { + // try to run a task processing task if possible + $taskTypeClass = $task->getType(); + $taskProcessingCompatibleTaskTypes = [ + FreePromptTaskType::class => TextToText::ID, + HeadlineTaskType::class => TextToTextHeadline::ID, + SummaryTaskType::class => TextToTextSummary::ID, + TopicsTaskType::class => TextToTextTopics::ID, + ]; + if (isset($taskProcessingCompatibleTaskTypes[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[$taskProcessingCompatibleTaskTypes[$taskTypeClass]])) { + try { + $taskProcessingTaskTypeId = $taskProcessingCompatibleTaskTypes[$taskTypeClass]; + $taskProcessingTask = new \OCP\TaskProcessing\Task( + $taskProcessingTaskTypeId, + ['input' => $task->getInput()], + $task->getAppId(), + $task->getUserId(), + $task->getIdentifier(), + ); + + $task->setStatus(OCPTask::STATUS_RUNNING); + if ($task->getId() === null) { + $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); + $task->setId($taskEntity->getId()); + } else { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } + $this->logger->debug('Running a TextProcessing (' . $taskTypeClass . ') task with TaskProcessing'); + $taskProcessingResultTask = $this->taskProcessingManager->runTask($taskProcessingTask); + if ($taskProcessingResultTask->getStatus() === \OCP\TaskProcessing\Task::STATUS_SUCCESSFUL) { + $output = $taskProcessingResultTask->getOutput(); + if (isset($output['output']) && is_string($output['output'])) { + $task->setOutput($output['output']); + $task->setStatus(OCPTask::STATUS_SUCCESSFUL); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + return $output['output']; + } + } + } catch (\Throwable $e) { + $this->logger->error('TextProcessing to TaskProcessing failed', ['exception' => $e]); + $task->setStatus(OCPTask::STATUS_FAILED); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + throw new TaskFailureException('TextProcessing to TaskProcessing failed: ' . $e->getMessage(), 0, $e); + } + $task->setStatus(OCPTask::STATUS_FAILED); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + throw new TaskFailureException('Could not run task'); + } + + // try to run the text processing task if (!$this->canHandleTask($task)) { throw new PreConditionNotMetException('No text processing provider is installed that can handle this task'); } @@ -169,7 +228,7 @@ class Manager implements IManager { throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); } [$provider,] = $this->getPreferredProviders($task); - $maxExecutionTime = (int) ini_get('max_execution_time'); + $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) { diff --git a/lib/private/TextToImage/Manager.php b/lib/private/TextToImage/Manager.php index 6ad3592a1b7..2d6cc6450a1 100644 --- a/lib/private/TextToImage/Manager.php +++ b/lib/private/TextToImage/Manager.php @@ -124,16 +124,16 @@ class Manager implements IManager { $folder = $this->appData->newFolder('text2image'); } try { - $folder = $folder->getFolder((string) $task->getId()); + $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()); + $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); + $file = $folder->newFile((string)$i); $files[] = $file; $resource = $file->write(); if ($resource !== false && $resource !== true && is_resource($resource)) { @@ -216,7 +216,7 @@ class Manager implements IManager { 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'); + $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); diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index ab568a3296d..1ddc9aaa0e1 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -112,7 +112,7 @@ class URLGenerator implements IURLGenerator { * @param string $appName app * @param string $file file * @param array $args array with param=>value, will be appended to the returned url - * The value of $args will be urlencoded + * The value of $args will be urlencoded * @return string the url * * Returns a url to the given app and file. @@ -303,7 +303,7 @@ class URLGenerator implements IURLGenerator { */ public function getBaseUrl(): string { // BaseUrl can be equal to 'http(s)://' during the first steps of the initial setup. - if ($this->baseUrl === null || $this->baseUrl === "http://" || $this->baseUrl === "https://") { + if ($this->baseUrl === null || $this->baseUrl === 'http://' || $this->baseUrl === 'https://') { $this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT; } return $this->baseUrl; diff --git a/lib/private/Updater.php b/lib/private/Updater.php index e26faf86f92..2722c172f1a 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -147,7 +147,7 @@ class Updater extends BasicEmitter { // this should really be a JSON file require \OC::$SERVERROOT . '/version.php'; /** @var string $vendor */ - return (string) $vendor; + return (string)$vendor; } /** diff --git a/lib/private/Updater/VersionCheck.php b/lib/private/Updater/VersionCheck.php index 9ad129db1a4..cc5ff63379c 100644 --- a/lib/private/Updater/VersionCheck.php +++ b/lib/private/Updater/VersionCheck.php @@ -61,7 +61,7 @@ class VersionCheck { $version['php_minor'] = PHP_MINOR_VERSION; $version['php_release'] = PHP_RELEASE_VERSION; $version['category'] = $this->computeCategory(); - $version['isSubscriber'] = (int) $this->registry->delegateHasValidSubscription(); + $version['isSubscriber'] = (int)$this->registry->delegateHasValidSubscription(); $versionString = implode('x', $version); //fetch xml data from updater diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php index bd6aa7ba2c2..a80cc39a732 100644 --- a/lib/private/User/Database.php +++ b/lib/private/User/Database.php @@ -108,7 +108,7 @@ class Database extends ABackend implements // Repopulate the cache $this->loadUser($uid); - return (bool) $result; + return (bool)$result; }, $this->dbConn); } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 2c8cc10dc15..96be58a84a2 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -455,7 +455,7 @@ class Manager extends PublicEmitter implements IUserManager { * returns how many users per backend exist (if supported by backend) * * @param boolean $hasLoggedIn when true only users that have a lastLogin - * entry in the preferences table will be affected + * entry in the preferences table will be affected * @return array<string, int> an array of backend class as key and count number as value */ public function countUsers() { @@ -486,7 +486,7 @@ class Manager extends PublicEmitter implements IUserManager { * * @param IGroup[] $groups an array of gid to search in * @return array|int an array of backend class as key and count number as value - * if $hasLoggedIn is true only an int is returned + * if $hasLoggedIn is true only an int is returned */ public function countUsersOfGroups(array $groups) { $users = []; @@ -506,7 +506,7 @@ class Manager extends PublicEmitter implements IUserManager { * @psalm-param \Closure(\OCP\IUser):?bool $callback * @param string $search * @param boolean $onlySeen when true only users that have a lastLogin entry - * in the preferences table will be affected + * in the preferences table will be affected * @since 9.0.0 */ public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) { diff --git a/lib/private/User/User.php b/lib/private/User/User.php index 6495b5cf276..6f7ceb08532 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -216,9 +216,9 @@ class User implements IUser { */ public function getLastLogin() { if ($this->lastLogin === null) { - $this->lastLogin = (int) $this->config->getUserValue($this->uid, 'login', 'lastLogin', 0); + $this->lastLogin = (int)$this->config->getUserValue($this->uid, 'login', 'lastLogin', 0); } - return (int) $this->lastLogin; + return (int)$this->lastLogin; } /** diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 3347e4a6ec2..f48f4fd0b98 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -171,7 +171,7 @@ class OC_App { * * @param bool $forceRefresh whether to refresh the cache * @param bool $all whether to return apps for all users, not only the - * currently logged in one + * currently logged in one * @return string[] */ public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array { @@ -886,7 +886,7 @@ class OC_App { } elseif ($englishFallback !== false) { return $englishFallback; } - return (string) $fallback; + return (string)$fallback; } /** diff --git a/lib/private/legacy/OC_Files.php b/lib/private/legacy/OC_Files.php index 76d61a98558..07caba42ff2 100644 --- a/lib/private/legacy/OC_Files.php +++ b/lib/private/legacy/OC_Files.php @@ -43,7 +43,7 @@ class OC_Files { OC_Response::setContentDispositionHeader($name, 'attachment'); header('Content-Transfer-Encoding: binary', true); header('Expires: 0'); - header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); $fileSize = \OC\Files\Filesystem::filesize($filename); $type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename)); if ($fileSize > -1) { @@ -334,8 +334,8 @@ class OC_Files { foreach ($rangeArray as $range) { echo "\r\n--".self::getBoundary()."\r\n". - "Content-type: ".$type."\r\n". - "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n"; + 'Content-type: '.$type."\r\n". + 'Content-range: bytes '.$range['from'].'-'.$range['to'].'/'.$range['size']."\r\n\r\n"; $view->readfilePart($filename, $range['from'], $range['to']); } echo "\r\n--".self::getBoundary()."--\r\n"; diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index 7db60363ff3..33cc966da2a 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -44,7 +44,7 @@ class OC_Helper { */ public static function humanFileSize(int|float $bytes): string { if ($bytes < 0) { - return "?"; + return '?'; } if ($bytes < 1024) { return "$bytes B"; @@ -127,7 +127,7 @@ class OC_Helper { } $files = scandir($src); foreach ($files as $file) { - if ($file != "." && $file != "..") { + if ($file != '.' && $file != '..') { self::copyr("$src/$file", "$dest/$file"); } } @@ -195,21 +195,21 @@ class OC_Helper { * @param bool $path * @internal param string $program name * @internal param string $optional search path, defaults to $PATH - * @return bool true if executable program found in path + * @return bool true if executable program found in path */ public static function canExecute($name, $path = false) { // path defaults to PATH from environment if not set if ($path === false) { - $path = getenv("PATH"); + $path = getenv('PATH'); } // we look for an executable file of that name - $exts = [""]; - $check_fn = "is_executable"; + $exts = ['']; + $check_fn = 'is_executable'; // Default check will be done with $path directories : $dirs = explode(PATH_SEPARATOR, $path); // WARNING : We have to check if open_basedir is enabled : $obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir'); - if ($obd != "none") { + if ($obd != 'none') { $obd_values = explode(PATH_SEPARATOR, $obd); if (count($obd_values) > 0 and $obd_values[0]) { // open_basedir is in effect ! @@ -516,13 +516,13 @@ class OC_Helper { $free = 0.0; } } catch (\Exception $e) { - if ($path === "") { + if ($path === '') { throw $e; } /** @var LoggerInterface $logger */ $logger = \OC::$server->get(LoggerInterface::class); - $logger->warning("Error while getting quota info, using root quota", ['exception' => $e]); - $rootInfo = self::getStorageInfo(""); + $logger->warning('Error while getting quota info, using root quota', ['exception' => $e]); + $rootInfo = self::getStorageInfo(''); $memcache->set($cacheKey, $rootInfo, 5 * 60); return $rootInfo; } diff --git a/lib/private/legacy/OC_Hook.php b/lib/private/legacy/OC_Hook.php index 2f6686f9126..5c36a253895 100644 --- a/lib/private/legacy/OC_Hook.php +++ b/lib/private/legacy/OC_Hook.php @@ -43,8 +43,8 @@ class OC_Hook { } // Connect the hook handler to the requested emitter self::$registered[$signalClass][$signalName][] = [ - "class" => $slotClass, - "name" => $slotName + 'class' => $slotClass, + 'name' => $slotName ]; // No chance for failure ;-) @@ -79,7 +79,7 @@ class OC_Hook { // Call all slots foreach (self::$registered[$signalClass][$signalName] as $i) { try { - call_user_func([ $i["class"], $i["name"] ], $params); + call_user_func([ $i['class'], $i['name'] ], $params); } catch (Exception $e) { self::$thrownExceptions[] = $e; \OC::$server->getLogger()->logException($e); diff --git a/lib/private/legacy/OC_Template.php b/lib/private/legacy/OC_Template.php index 5caa733b115..422709cec7d 100644 --- a/lib/private/legacy/OC_Template.php +++ b/lib/private/legacy/OC_Template.php @@ -43,6 +43,7 @@ class OC_Template extends \OC\Template\Base { $theme = OC_Util::getTheme(); $requestToken = (OC::$server->getSession() && $registerCall) ? \OCP\Util::callRegister() : ''; + $cspNonce = \OCP\Server::get(\OC\Security\CSP\ContentSecurityPolicyNonceManager::class)->getNonce(); $parts = explode('/', $app); // fix translation when app is something like core/lostpassword $l10n = \OC::$server->getL10N($parts[0]); @@ -56,7 +57,13 @@ class OC_Template extends \OC\Template\Base { $this->path = $path; $this->app = $app; - parent::__construct($template, $requestToken, $l10n, $themeDefaults); + parent::__construct( + $template, + $requestToken, + $l10n, + $themeDefaults, + $cspNonce, + ); } @@ -88,7 +95,7 @@ class OC_Template extends \OC\Template\Base { * @param string $tag tag name of the element * @param array $attributes array of attributes for the element * @param string $text the text content for the element. If $text is null then the - * element will be written as empty element. So use "" to get a closing tag. + * element will be written as empty element. So use "" to get a closing tag. */ public function addHeader($tag, $attributes, $text = null) { $this->headers[] = [ @@ -165,7 +172,7 @@ class OC_Template extends \OC\Template\Base { * @return boolean|null */ public static function printUserPage($application, $name, $parameters = []) { - $content = new OC_Template($application, $name, "user"); + $content = new OC_Template($application, $name, 'user'); foreach ($parameters as $key => $value) { $content->assign($key, $value); } @@ -180,7 +187,7 @@ class OC_Template extends \OC\Template\Base { * @return bool */ public static function printAdminPage($application, $name, $parameters = []) { - $content = new OC_Template($application, $name, "admin"); + $content = new OC_Template($application, $name, 'admin'); foreach ($parameters as $key => $value) { $content->assign($key, $value); } diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index d2978f6ad21..b8a00de84cc 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -142,7 +142,7 @@ class OC_User { public static function loginWithApache(\OCP\Authentication\IApacheBackend $backend) { $uid = $backend->getCurrentUserId(); $run = true; - OC_Hook::emit("OC_User", "pre_login", ["run" => &$run, "uid" => $uid, 'backend' => $backend]); + OC_Hook::emit('OC_User', 'pre_login', ['run' => &$run, 'uid' => $uid, 'backend' => $backend]); if ($uid) { if (self::getUser() !== $uid) { @@ -213,9 +213,9 @@ class OC_User { * Verify with Apache whether user is authenticated. * * @return boolean|null - * true: authenticated - * false: not authenticated - * null: not handled / no backend available + * true: authenticated + * false: not authenticated + * null: not handled / no backend available */ public static function handleApacheAuth() { $backend = self::findFirstActiveUsedBackend(); diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index 3b5222fee64..84bb0b645d5 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -296,7 +296,7 @@ class OC_Util { private static function generatePath($application, $directory, $file) { if (is_null($file)) { $file = $application; - $application = ""; + $application = ''; } if (!empty($application)) { return "$application/$directory/$file"; @@ -322,7 +322,7 @@ class OC_Util { if ($application !== 'core' && $file !== null) { self::addTranslations($application); } - self::addExternalResource($application, $prepend, $path, "script"); + self::addExternalResource($application, $prepend, $path, 'script'); } /** @@ -335,7 +335,7 @@ class OC_Util { */ public static function addVendorScript($application, $file = null, $prepend = false) { $path = OC_Util::generatePath($application, 'vendor', $file); - self::addExternalResource($application, $prepend, $path, "script"); + self::addExternalResource($application, $prepend, $path, 'script'); } /** @@ -356,7 +356,7 @@ class OC_Util { } else { $path = "l10n/$languageCode"; } - self::addExternalResource($application, $prepend, $path, "script"); + self::addExternalResource($application, $prepend, $path, 'script'); } /** @@ -369,7 +369,7 @@ class OC_Util { */ public static function addStyle($application, $file = null, $prepend = false) { $path = OC_Util::generatePath($application, 'css', $file); - self::addExternalResource($application, $prepend, $path, "style"); + self::addExternalResource($application, $prepend, $path, 'style'); } /** @@ -382,7 +382,7 @@ class OC_Util { */ public static function addVendorStyle($application, $file = null, $prepend = false) { $path = OC_Util::generatePath($application, 'vendor', $file); - self::addExternalResource($application, $prepend, $path, "style"); + self::addExternalResource($application, $prepend, $path, 'style'); } /** @@ -394,8 +394,8 @@ class OC_Util { * @param string $type (script or style) * @return void */ - private static function addExternalResource($application, $prepend, $path, $type = "script") { - if ($type === "style") { + private static function addExternalResource($application, $prepend, $path, $type = 'script') { + if ($type === 'style') { if (!in_array($path, self::$styles)) { if ($prepend === true) { array_unshift(self::$styles, $path); @@ -403,7 +403,7 @@ class OC_Util { self::$styles[] = $path; } } - } elseif ($type === "script") { + } elseif ($type === 'script') { if (!in_array($path, self::$scripts)) { if ($prepend === true) { array_unshift(self::$scripts, $path); @@ -991,7 +991,7 @@ class OC_Util { * @return string the theme */ public static function getTheme() { - $theme = \OC::$server->getSystemConfig()->getValue("theme", ''); + $theme = \OC::$server->getSystemConfig()->getValue('theme', ''); if ($theme === '') { if (is_dir(OC::$SERVERROOT . '/themes/default')) { diff --git a/lib/private/legacy/template/functions.php b/lib/private/legacy/template/functions.php index 84ada2aa6d8..87b91639fd3 100644 --- a/lib/private/legacy/template/functions.php +++ b/lib/private/legacy/template/functions.php @@ -102,7 +102,7 @@ function print_unescaped($string) { * * @param string $app the appname * @param string|string[] $file the filename, - * if an array is given it will add all scripts + * if an array is given it will add all scripts */ function script($app, $file = null) { if (is_array($file)) { @@ -118,7 +118,7 @@ function script($app, $file = null) { * Shortcut for adding vendor scripts to a page * @param string $app the appname * @param string|string[] $file the filename, - * if an array is given it will add all scripts + * if an array is given it will add all scripts */ function vendor_script($app, $file = null) { if (is_array($file)) { @@ -134,7 +134,7 @@ function vendor_script($app, $file = null) { * Shortcut for adding styles to a page * @param string $app the appname * @param string|string[] $file the filename, - * if an array is given it will add all styles + * if an array is given it will add all styles */ function style($app, $file = null) { if (is_array($file)) { @@ -150,7 +150,7 @@ function style($app, $file = null) { * Shortcut for adding vendor styles to a page * @param string $app the appname * @param string|string[] $file the filename, - * if an array is given it will add all styles + * if an array is given it will add all styles */ function vendor_style($app, $file = null) { if (is_array($file)) { @@ -165,7 +165,7 @@ function vendor_style($app, $file = null) { /** * Shortcut for adding translations to a page * @param string $app the appname - * if an array is given it will add all styles + * if an array is given it will add all styles */ function translation($app) { OC_Util::addTranslations($app); @@ -175,7 +175,7 @@ function translation($app) { * Shortcut for HTML imports * @param string $app the appname * @param string|string[] $file the path relative to the app's component folder, - * if an array is given it will add all components + * if an array is given it will add all components */ function component($app, $file) { if (is_array($file)) { |