diff options
author | Jasper Weyne <jasperweyne@gmail.com> | 2022-08-11 08:54:08 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-11 08:54:08 +0200 |
commit | 44f6c931e7c9c74ea4f448d3cdfbaa89f3b7c379 (patch) | |
tree | 710a8c1bd1c20c685991de146aa9ef149ec1de7a /lib/private | |
parent | 0633a1d9f5a7ef06d577ae6556d09db9e94f5684 (diff) | |
parent | a61331f4560468e6d433cf32e008b157b06e7ea9 (diff) | |
download | nextcloud-server-44f6c931e7c9c74ea4f448d3cdfbaa89f3b7c379.tar.gz nextcloud-server-44f6c931e7c9c74ea4f448d3cdfbaa89f3b7c379.zip |
Merge branch 'master' into patch-2
Diffstat (limited to 'lib/private')
118 files changed, 1302 insertions, 878 deletions
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index b80c7887591..20e0add1ccb 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -42,7 +42,7 @@ use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumberFormat; use libphonenumber\PhoneNumberUtil; use OC\Profile\TProfileHelper; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCA\Settings\BackgroundJobs\VerifyUserData; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; @@ -220,7 +220,7 @@ class AccountManager implements IAccountManager { foreach ($properties as $property) { if (strlen($property->getValue()) > 2048) { if ($throwOnData) { - throw new InvalidArgumentException(); + throw new InvalidArgumentException($property->getName()); } else { $property->setValue(''); } diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php index f282baee146..2a0e8f53b14 100644 --- a/lib/private/AllConfig.php +++ b/lib/private/AllConfig.php @@ -32,7 +32,7 @@ */ namespace OC; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IConfig; use OCP\IDBConnection; @@ -353,8 +353,8 @@ class AllConfig implements IConfig { $qb = $this->connection->getQueryBuilder(); $qb->delete('preferences') ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))) - ->where($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR))) - ->where($qb->expr()->eq('configkey', $qb->createNamedParameter($key, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key, IQueryBuilder::PARAM_STR))) ->executeStatement(); if (isset($this->userCache[$userId][$appName])) { diff --git a/lib/private/App/AppStore/Bundles/HubBundle.php b/lib/private/App/AppStore/Bundles/HubBundle.php index a52de1dfbd4..d5d236a1855 100644 --- a/lib/private/App/AppStore/Bundles/HubBundle.php +++ b/lib/private/App/AppStore/Bundles/HubBundle.php @@ -39,8 +39,8 @@ class HubBundle extends Bundle { 'mail', ]; - $architecture = php_uname('m'); - if (PHP_OS_FAMILY === 'Linux' && in_array($architecture, ['x86_64', 'aarch64'])) { + $architecture = function_exists('php_uname') ? php_uname('m') : null; + if (isset($architecture) && PHP_OS_FAMILY === 'Linux' && in_array($architecture, ['x86_64', 'aarch64'])) { $hubApps[] = 'richdocuments'; $hubApps[] = 'richdocumentscode' . ($architecture === 'aarch64' ? '_arm64' : ''); } diff --git a/lib/private/App/CompareVersion.php b/lib/private/App/CompareVersion.php index d155945fff1..a349c7aa6f2 100644 --- a/lib/private/App/CompareVersion.php +++ b/lib/private/App/CompareVersion.php @@ -41,7 +41,7 @@ class CompareVersion { * so '13.0.1', '13.0' and '13' are valid. * * @param string $actual version as major.minor.patch notation - * @param string $required version where major is requried and minor and patch are optional + * @param string $required version where major is required and minor and patch are optional * @param string $comparator passed to `version_compare` * @return bool whether the requirement is fulfilled * @throws InvalidArgumentException if versions specified in an invalid format diff --git a/lib/private/App/Platform.php b/lib/private/App/Platform.php index 12097abbc78..15966d85c34 100644 --- a/lib/private/App/Platform.php +++ b/lib/private/App/Platform.php @@ -35,40 +35,26 @@ use OCP\IConfig; * @package OC\App */ class Platform { + private IConfig $config; - /** - * @param IConfig $config - */ public function __construct(IConfig $config) { $this->config = $config; } - /** - * @return string - */ - public function getPhpVersion() { + public function getPhpVersion(): string { return phpversion(); } - /** - * @return int - */ - public function getIntSize() { + public function getIntSize(): int { return PHP_INT_SIZE; } - /** - * @return string - */ - public function getOcVersion() { + public function getOcVersion(): string { $v = \OCP\Util::getVersion(); return implode('.', $v); } - /** - * @return string - */ - public function getDatabase() { + public function getDatabase(): string { $dbType = $this->config->getSystemValue('dbtype', 'sqlite'); if ($dbType === 'sqlite3') { $dbType = 'sqlite'; @@ -77,23 +63,19 @@ class Platform { return $dbType; } - /** - * @return string - */ - public function getOS() { + public function getOS(): string { return php_uname('s'); } /** * @param $command - * @return bool */ - public function isCommandKnown($command) { + public function isCommandKnown($command): bool { $path = \OC_Helper::findBinaryPath($command); return ($path !== null); } - public function getLibraryVersion($name) { + public function getLibraryVersion(string $name): ?string { $repo = new PlatformRepository(); return $repo->findLibrary($name); } diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index 94fac5260e1..4166c2ead03 100644 --- a/lib/private/App/PlatformRepository.php +++ b/lib/private/App/PlatformRepository.php @@ -31,11 +31,13 @@ namespace OC\App; * @package OC\App */ class PlatformRepository { + private array $packages; + public function __construct() { $this->packages = $this->initialize(); } - protected function initialize() { + protected function initialize(): array { $loadedExtensions = get_loaded_extensions(); $packages = []; @@ -121,15 +123,11 @@ class PlatformRepository { return $packages; } - private function buildPackageName($name) { + private function buildPackageName(string $name): string { return str_replace(' ', '-', $name); } - /** - * @param $name - * @return string - */ - public function findLibrary($name) { + public function findLibrary(string $name): ?string { $extName = $this->buildPackageName($name); if (isset($this->packages[$extName])) { return $this->packages[$extName]; @@ -137,19 +135,17 @@ class PlatformRepository { return null; } - private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + private static string $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; /** * Normalizes a version string to be able to perform comparisons on it * * https://github.com/composer/composer/blob/master/src/Composer/Package/Version/VersionParser.php#L94 * - * @param string $version * @param string $fullVersion optional complete version string to give more context * @throws \UnexpectedValueException - * @return string */ - public function normalizeVersion($version, $fullVersion = null) { + public function normalizeVersion(string $version, ?string $fullVersion = null): string { $version = trim($version); if (null === $fullVersion) { $fullVersion = $version; @@ -204,10 +200,7 @@ class PlatformRepository { throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage); } - /** - * @param string $stability - */ - private function expandStability($stability) { + private function expandStability(string $stability): string { $stability = strtolower($stability); switch ($stability) { case 'a': diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index feebb32d5bc..170acba0689 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -237,8 +237,6 @@ class App { /** * Shortcut for calling a controller method and printing the result. * Similar to App:main except that no headers will be sent. - * This should be used for example when registering sections via - * \OC\AppFramework\Core\API::registerAdmin() * * @param string $controllerName the name of the controller under which it is * stored in the DI container diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php index b6378830732..3ab6ac4c8b0 100644 --- a/lib/private/AppFramework/Bootstrap/Coordinator.php +++ b/lib/private/AppFramework/Bootstrap/Coordinator.php @@ -151,7 +151,7 @@ class Coordinator { */ $this->registrationContext->delegateCapabilityRegistrations($apps); $this->registrationContext->delegateCrashReporterRegistrations($apps, $this->registry); - $this->registrationContext->delegateDashboardPanelRegistrations($apps, $this->dashboardManager); + $this->registrationContext->delegateDashboardPanelRegistrations($this->dashboardManager); $this->registrationContext->delegateEventListenerRegistrations($this->eventDispatcher); $this->registrationContext->delegateContainerRegistrations($apps); $this->registrationContext->delegateMiddlewareRegistrations($apps); diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 7b4d24036e8..c98f968c999 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -121,6 +121,9 @@ class RegistrationContext { /** @var ServiceRegistration<ICalendarProvider>[] */ private $calendarProviders = []; + /** @var ParameterRegistration[] */ + private $sensitiveMethods = []; + /** @var LoggerInterface */ private $logger; @@ -304,6 +307,14 @@ class RegistrationContext { $migratorClass ); } + + public function registerSensitiveMethods(string $class, array $methods): void { + $this->context->registerSensitiveMethods( + $this->appId, + $class, + $methods + ); + } }; } @@ -430,6 +441,11 @@ class RegistrationContext { $this->userMigrators[] = new ServiceRegistration($appId, $migratorClass); } + public function registerSensitiveMethods(string $appId, string $class, array $methods): void { + $methods = array_filter($methods, 'is_string'); + $this->sensitiveMethods[] = new ParameterRegistration($appId, $class, $methods); + } + /** * @param App[] $apps */ @@ -475,10 +491,10 @@ class RegistrationContext { /** * @param App[] $apps */ - public function delegateDashboardPanelRegistrations(array $apps, IManager $dashboardManager): void { + public function delegateDashboardPanelRegistrations(IManager $dashboardManager): void { while (($panel = array_shift($this->dashboardPanels)) !== null) { try { - $dashboardManager->lazyRegisterWidget($panel->getService()); + $dashboardManager->lazyRegisterWidget($panel->getService(), $panel->getAppId()); } catch (Throwable $e) { $appId = $panel->getAppId(); $this->logger->error("Error during dashboard registration of $appId: " . $e->getMessage(), [ @@ -712,4 +728,11 @@ class RegistrationContext { public function getUserMigrators(): array { return $this->userMigrators; } + + /** + * @return ParameterRegistration[] + */ + public function getSensitiveMethods(): array { + return $this->sensitiveMethods; + } } diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index 21d61bc95aa..c1a203a7165 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -118,7 +118,7 @@ class Dispatcher { $out = [null, [], null]; try { - // prefill reflector with everything thats needed for the + // prefill reflector with everything that's needed for the // middlewares $this->reflector->reflect($controller, $methodName); @@ -156,7 +156,7 @@ class Dispatcher { // if an exception appears, the middleware checks if it can handle the // exception and creates a response. If no response is created, it is - // assumed that theres no middleware who can handle it and the error is + // assumed that there's no middleware who can handle it and the error is // thrown again } catch (\Exception $exception) { $response = $this->middlewareDispatcher->afterException( diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 010d889070e..496a845dd4a 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -25,6 +25,7 @@ declare(strict_types=1); * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Thomas Tanghus <thomas@tanghus.net> * @author Vincent Petry <vincent@nextcloud.com> + * @author Simon Leiner <simon@leiner.me> * * @license AGPL-3.0 * @@ -50,6 +51,7 @@ use OCP\IConfig; use OCP\IRequest; use OCP\IRequestId; use OCP\Security\ICrypto; +use Symfony\Component\HttpFoundation\IpUtils; /** * Class for accessing variables in the request. @@ -342,7 +344,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Returns all params that were received, be it from the request - * (as GET or POST) or throuh the URL by the route + * (as GET or POST) or through the URL by the route * @return array the array with all parameters */ public function getParams(): array { @@ -573,41 +575,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * Checks if given $remoteAddress matches given $trustedProxy. - * If $trustedProxy is an IPv4 IP range given in CIDR notation, true will be returned if - * $remoteAddress is an IPv4 address within that IP range. - * Otherwise $remoteAddress will be compared to $trustedProxy literally and the result - * will be returned. - * @return boolean true if $remoteAddress matches $trustedProxy, false otherwise - */ - protected function matchesTrustedProxy($trustedProxy, $remoteAddress) { - $cidrre = '/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\/([0-9]{1,2})$/'; - - if (preg_match($cidrre, $trustedProxy, $match)) { - $net = $match[1]; - $shiftbits = min(32, max(0, 32 - intval($match[2]))); - $netnum = ip2long($net) >> $shiftbits; - $ipnum = ip2long($remoteAddress) >> $shiftbits; - - return $ipnum === $netnum; - } - - return $trustedProxy === $remoteAddress; - } - - /** * Checks if given $remoteAddress matches any entry in the given array $trustedProxies. * For details regarding what "match" means, refer to `matchesTrustedProxy`. * @return boolean true if $remoteAddress matches any entry in $trustedProxies, false otherwise */ protected function isTrustedProxy($trustedProxies, $remoteAddress) { - foreach ($trustedProxies as $tp) { - if ($this->matchesTrustedProxy($tp, $remoteAddress)) { - return true; - } - } - - return false; + return IpUtils::checkIp($remoteAddress, $trustedProxies); } /** diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php index 950ef8a13a3..adf17e53caa 100644 --- a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php +++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php @@ -46,7 +46,7 @@ class MiddlewareDispatcher { private $middlewares; /** - * @var int counter which tells us what middlware was executed once an + * @var int counter which tells us what middleware was executed once an * exception occurs */ private $middlewareCounter; diff --git a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php index 2fb05159d09..5e657be0763 100644 --- a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php +++ b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php @@ -72,12 +72,12 @@ class UserDeletedFilesCleanupListener implements IEventListener { } $storage = $this->homeStorageCache[$event->getUser()->getUID()]; $cache = $storage->getCache(); + $storage->rmdir(''); if ($cache instanceof Cache) { $cache->clear(); } else { throw new \Exception("Home storage has invalid cache"); } - $storage->rmdir(''); } } } diff --git a/lib/private/Authentication/LoginCredentials/Store.php b/lib/private/Authentication/LoginCredentials/Store.php index 0ab4c9a37cc..d3db0444664 100644 --- a/lib/private/Authentication/LoginCredentials/Store.php +++ b/lib/private/Authentication/LoginCredentials/Store.php @@ -100,7 +100,7 @@ class Store implements IStore { } catch (SessionNotAvailableException $ex) { $this->logger->debug('could not get login credentials because session is unavailable', ['app' => 'core', 'exception' => $ex]); } catch (InvalidTokenException $ex) { - $this->logger->debug('could not get login credentials because the token is invalid: ' . $ex->getMessage(), ['app' => 'core', 'exception' => $ex]); + $this->logger->debug('could not get login credentials because the token is invalid: ' . $ex->getMessage(), ['app' => 'core']); $trySession = true; } catch (PasswordlessTokenException $ex) { $this->logger->debug('could not get login credentials because the token has no password', ['app' => 'core', 'exception' => $ex]); diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 0a145bfd7e6..33e0ad46263 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -158,7 +158,7 @@ interface IProvider { public function setPassword(IToken $token, string $tokenId, string $password); /** - * Rotate the token. Usefull for for example oauth tokens + * Rotate the token. Useful for for example oauth tokens * * @param IToken $token * @param string $oldTokenId diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index a1d75828e27..0f1767e845b 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -34,7 +34,7 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\TokenPasswordExpiredException; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Exceptions\WipeTokenException; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; @@ -346,7 +346,7 @@ class PublicKeyTokenProvider implements IProvider { $config = array_merge([ 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, + 'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048, ], $this->config->getSystemValue('openssl', [])); // Generate new key @@ -368,7 +368,10 @@ class PublicKeyTokenProvider implements IProvider { $dbToken->setPublicKey($publicKey); $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); - if (!is_null($password)) { + if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { + if (strlen($password) > 469) { + throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php'); + } $dbToken->setPassword($this->encryptPassword($password, $publicKey)); } @@ -398,7 +401,7 @@ class PublicKeyTokenProvider implements IProvider { $this->cache->clear(); // prevent setting an empty pw as result of pw-less-login - if ($password === '') { + if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { return; } @@ -406,9 +409,12 @@ class PublicKeyTokenProvider implements IProvider { $tokens = $this->mapper->getTokenByUser($uid); foreach ($tokens as $t) { $publicKey = $t->getPublicKey(); - $t->setPassword($this->encryptPassword($password, $publicKey)); - $t->setPasswordInvalid(false); - $this->updateToken($t); + $encryptedPassword = $this->encryptPassword($password, $publicKey); + if ($t->getPassword() !== $encryptedPassword) { + $t->setPassword($encryptedPassword); + $t->setPasswordInvalid(false); + $this->updateToken($t); + } } } diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index f0e14ad8b2f..0eb8f8816d8 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -37,7 +37,7 @@ declare(strict_types=1); namespace OC\Avatar; use Imagick; -use OC\Color; +use OCP\Color; use OCP\Files\NotFoundException; use OCP\IAvatar; use Psr\Log\LoggerInterface; @@ -46,9 +46,7 @@ use Psr\Log\LoggerInterface; * This class gets and sets users avatars. */ abstract class Avatar implements IAvatar { - - /** @var LoggerInterface */ - protected $logger; + protected LoggerInterface $logger; /** * https://github.com/sebdesign/cap-height -- for 500px height @@ -57,10 +55,8 @@ abstract class Avatar implements IAvatar { * (0.4 letter-to-total-height ratio, 500*0.4=200), so: 200/0.715 = 280px. * Since we start from the baseline (text-anchor) we need to * shift the y axis by 100px (half the caps height): 500/2+100=350 - * - * @var string */ - private $svgTemplate = '<?xml version="1.0" encoding="UTF-8" standalone="no"?> + private string $svgTemplate = '<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="{size}" height="{size}" version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg"> <rect width="100%" height="100%" fill="#{fill}"></rect> <text x="50%" y="350" style="font-weight:normal;font-size:280px;font-family:\'Noto Sans\';text-anchor:middle;fill:#fff">{letter}</text> @@ -72,15 +68,11 @@ abstract class Avatar implements IAvatar { /** * Returns the user display name. - * - * @return string */ abstract public function getDisplayName(): string; /** * Returns the first letter of the display name, or "?" if no name given. - * - * @return string */ private function getAvatarText(): string { $displayName = $this->getDisplayName(); @@ -96,9 +88,7 @@ abstract class Avatar implements IAvatar { /** * @inheritdoc */ - public function get($size = 64) { - $size = (int) $size; - + public function get(int $size = 64) { try { $file = $this->getFile($size); } catch (NotFoundException $e) { @@ -124,7 +114,7 @@ abstract class Avatar implements IAvatar { protected function getAvatarVector(int $size): string { $userDisplayName = $this->getDisplayName(); $bgRGB = $this->avatarBackgroundColor($userDisplayName); - $bgHEX = sprintf("%02x%02x%02x", $bgRGB->r, $bgRGB->g, $bgRGB->b); + $bgHEX = sprintf("%02x%02x%02x", $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); $text = $this->getAvatarText(); $toReplace = ['{size}', '{fill}', '{letter}']; return str_replace($toReplace, [$size, $bgHEX, $text], $this->svgTemplate); @@ -132,13 +122,10 @@ abstract class Avatar implements IAvatar { /** * Generate png avatar from svg with Imagick - * - * @param int $size - * @return string|boolean */ - protected function generateAvatarFromSvg(int $size) { + protected function generateAvatarFromSvg(int $size): ?string { if (!extension_loaded('imagick')) { - return false; + return null; } try { $font = __DIR__ . '/../../core/fonts/NotoSans-Regular.ttf'; @@ -149,30 +136,25 @@ abstract class Avatar implements IAvatar { $avatar->setImageFormat('png'); $image = new \OCP\Image(); $image->loadFromData((string)$avatar); - $data = $image->data(); - return $data === null ? false : $data; + return $image->data(); } catch (\Exception $e) { - return false; + return null; } } /** * Generate png avatar with GD - * - * @param string $userDisplayName - * @param int $size - * @return string */ - protected function generateAvatar($userDisplayName, $size) { + protected function generateAvatar(string $userDisplayName, int $size): string { $text = $this->getAvatarText(); $backgroundColor = $this->avatarBackgroundColor($userDisplayName); $im = imagecreatetruecolor($size, $size); $background = imagecolorallocate( $im, - $backgroundColor->r, - $backgroundColor->g, - $backgroundColor->b + $backgroundColor->red(), + $backgroundColor->green(), + $backgroundColor->blue() ); $white = imagecolorallocate($im, 255, 255, 255); imagefilledrectangle($im, 0, 0, $size, $size, $background); @@ -209,7 +191,7 @@ abstract class Avatar implements IAvatar { string $text, string $font, int $size, - $angle = 0 + int $angle = 0 ): array { // Image width & height $xi = imagesx($image); @@ -229,37 +211,6 @@ abstract class Avatar implements IAvatar { return [$x, $y]; } - /** - * Calculate steps between two Colors - * @param object Color $steps start color - * @param object Color $ends end color - * @return array [r,g,b] steps for each color to go from $steps to $ends - */ - private function stepCalc($steps, $ends) { - $step = []; - $step[0] = ($ends[1]->r - $ends[0]->r) / $steps; - $step[1] = ($ends[1]->g - $ends[0]->g) / $steps; - $step[2] = ($ends[1]->b - $ends[0]->b) / $steps; - return $step; - } - - /** - * Convert a string to an integer evenly - * @param string $hash the text to parse - * @param int $maximum the maximum range - * @return int[] between 0 and $maximum - */ - private function mixPalette($steps, $color1, $color2) { - $palette = [$color1]; - $step = $this->stepCalc($steps, [$color1, $color2]); - for ($i = 1; $i < $steps; $i++) { - $r = intval($color1->r + ($step[0] * $i)); - $g = intval($color1->g + ($step[1] * $i)); - $b = intval($color1->b + ($step[2] * $i)); - $palette[] = new Color($r, $g, $b); - } - return $palette; - } /** * Convert a string to an integer evenly @@ -267,7 +218,7 @@ abstract class Avatar implements IAvatar { * @param int $maximum the maximum range * @return int between 0 and $maximum */ - private function hashToInt($hash, $maximum) { + private function hashToInt(string $hash, int $maximum): int { $final = 0; $result = []; @@ -285,10 +236,9 @@ abstract class Avatar implements IAvatar { } /** - * @param string $hash - * @return Color Object containting r g b int in the range [0, 255] + * @return Color Object containing r g b int in the range [0, 255] */ - public function avatarBackgroundColor(string $hash) { + public function avatarBackgroundColor(string $hash): Color { // Normalize hash $hash = strtolower($hash); @@ -308,9 +258,9 @@ abstract class Avatar implements IAvatar { // 3 colors * 6 will result in 18 generated colors $steps = 6; - $palette1 = $this->mixPalette($steps, $red, $yellow); - $palette2 = $this->mixPalette($steps, $yellow, $blue); - $palette3 = $this->mixPalette($steps, $blue, $red); + $palette1 = Color::mixPalette($steps, $red, $yellow); + $palette2 = Color::mixPalette($steps, $yellow, $blue); + $palette3 = Color::mixPalette($steps, $blue, $red); $finalPalette = array_merge($palette1, $palette2, $palette3); diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php index 77138085dc9..ec9bed40850 100644 --- a/lib/private/Avatar/AvatarManager.php +++ b/lib/private/Avatar/AvatarManager.php @@ -136,20 +136,23 @@ class AvatarManager implements IAvatarManager { $avatarScope = ''; } - if ( + switch ($avatarScope) { // v2-private scope hides the avatar from public access and from unknown users - $avatarScope === IAccountManager::SCOPE_PRIVATE - && ( - // accessing from public link - $requestingUser === null - // logged in, but unknown to user - || !$this->knownUserService->isKnownToUser($requestingUser->getUID(), $userId) - )) { - // use a placeholder avatar which caches the generated images - return new PlaceholderAvatar($folder, $user, $this->logger); + case IAccountManager::SCOPE_PRIVATE: + if ($requestingUser !== null && $this->knownUserService->isKnownToUser($requestingUser->getUID(), $userId)) { + return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); + } + break; + case IAccountManager::SCOPE_LOCAL: + case IAccountManager::SCOPE_FEDERATED: + case IAccountManager::SCOPE_PUBLISHED: + return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); + default: + // use a placeholder avatar which caches the generated images + return new PlaceholderAvatar($folder, $user, $this->logger); } - return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); + return new PlaceholderAvatar($folder, $user, $this->logger); } /** diff --git a/lib/private/Avatar/GuestAvatar.php b/lib/private/Avatar/GuestAvatar.php index f0297b3a93c..79d7e6ee094 100644 --- a/lib/private/Avatar/GuestAvatar.php +++ b/lib/private/Avatar/GuestAvatar.php @@ -26,6 +26,7 @@ declare(strict_types=1); */ namespace OC\Avatar; +use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\InMemoryFile; use Psr\Log\LoggerInterface; @@ -35,16 +36,13 @@ use Psr\Log\LoggerInterface; class GuestAvatar extends Avatar { /** * Holds the guest user display name. - * - * @var string */ - private $userDisplayName; + private string $userDisplayName; /** * GuestAvatar constructor. * * @param string $userDisplayName The guest user display name - * @param LoggerInterface $logger The logger */ public function __construct(string $userDisplayName, LoggerInterface $logger) { parent::__construct($logger); @@ -53,17 +51,14 @@ class GuestAvatar extends Avatar { /** * Tests if the user has an avatar. - * - * @return true Guests always have an avatar. */ - public function exists() { + public function exists(): bool { + // Guests always have an avatar. return true; } /** * Returns the guest user display name. - * - * @return string */ public function getDisplayName(): string { return $this->userDisplayName; @@ -75,24 +70,21 @@ class GuestAvatar extends Avatar { * @param \OCP\IImage|resource|string $data * @return void */ - public function set($data) { + public function set($data): void { // unimplemented for guest user avatars } /** * Removing avatars isn't implemented for guests. */ - public function remove() { + public function remove(bool $silent = false): void { // unimplemented for guest user avatars } /** * Generates an avatar for the guest. - * - * @param int $size The desired image size. - * @return InMemoryFile */ - public function getFile($size) { + public function getFile(int $size): ISimpleFile { $avatar = $this->generateAvatar($this->userDisplayName, $size); return new InMemoryFile('avatar.png', $avatar); } @@ -103,9 +95,8 @@ class GuestAvatar extends Avatar { * @param string $feature The changed feature * @param mixed $oldValue The previous value * @param mixed $newValue The new value - * @return void */ - public function userChanged($feature, $oldValue, $newValue) { + public function userChanged(string $feature, $oldValue, $newValue): void { if ($feature === 'displayName') { $this->userDisplayName = $newValue; } @@ -113,8 +104,6 @@ class GuestAvatar extends Avatar { /** * Guests don't have custom avatars. - * - * @return bool */ public function isCustomAvatar(): bool { return false; diff --git a/lib/private/Avatar/PlaceholderAvatar.php b/lib/private/Avatar/PlaceholderAvatar.php index df7a490cbe4..504e5c1457d 100644 --- a/lib/private/Avatar/PlaceholderAvatar.php +++ b/lib/private/Avatar/PlaceholderAvatar.php @@ -44,11 +44,8 @@ use Psr\Log\LoggerInterface; * for faster retrieval, unlike the GuestAvatar. */ class PlaceholderAvatar extends Avatar { - /** @var ISimpleFolder */ - private $folder; - - /** @var User */ - private $user; + private ISimpleFolder $folder; + private User $user; /** * UserAvatar constructor. @@ -71,10 +68,8 @@ class PlaceholderAvatar extends Avatar { /** * Check if an avatar exists for the user - * - * @return bool */ - public function exists() { + public function exists(): bool { return true; } @@ -87,14 +82,14 @@ class PlaceholderAvatar extends Avatar { * @throws NotSquareException if the image is not square * @return void */ - public function set($data) { + public function set($data): void { // unimplemented for placeholder avatars } /** * Removes the users avatar. */ - public function remove(bool $silent = false) { + public function remove(bool $silent = false): void { $avatars = $this->folder->getDirectoryListing(); foreach ($avatars as $avatar) { @@ -113,9 +108,7 @@ class PlaceholderAvatar extends Avatar { * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function getFile($size) { - $size = (int) $size; - + public function getFile(int $size): ISimpleFile { $ext = 'png'; if ($size === -1) { @@ -149,8 +142,6 @@ class PlaceholderAvatar extends Avatar { /** * Returns the user display name. - * - * @return string */ public function getDisplayName(): string { return $this->user->getDisplayName(); @@ -165,14 +156,12 @@ class PlaceholderAvatar extends Avatar { * @throws NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function userChanged($feature, $oldValue, $newValue) { + public function userChanged(string $feature, $oldValue, $newValue): void { $this->remove(); } /** * Check if the avatar of a user is a custom uploaded one - * - * @return bool */ public function isCustomAvatar(): bool { return false; diff --git a/lib/private/Avatar/UserAvatar.php b/lib/private/Avatar/UserAvatar.php index 6cca5a1c355..f5a1d7e77b1 100644 --- a/lib/private/Avatar/UserAvatar.php +++ b/lib/private/Avatar/UserAvatar.php @@ -44,17 +44,10 @@ use Psr\Log\LoggerInterface; * This class represents a registered user's avatar. */ class UserAvatar extends Avatar { - /** @var IConfig */ - private $config; - - /** @var ISimpleFolder */ - private $folder; - - /** @var IL10N */ - private $l; - - /** @var User */ - private $user; + private IConfig $config; + private ISimpleFolder $folder; + private IL10N $l; + private User $user; /** * UserAvatar constructor. @@ -68,7 +61,7 @@ class UserAvatar extends Avatar { public function __construct( ISimpleFolder $folder, IL10N $l, - $user, + User $user, LoggerInterface $logger, IConfig $config) { parent::__construct($logger); @@ -80,10 +73,8 @@ class UserAvatar extends Avatar { /** * Check if an avatar exists for the user - * - * @return bool */ - public function exists() { + public function exists(): bool { return $this->folder->fileExists('avatar.jpg') || $this->folder->fileExists('avatar.png'); } @@ -96,7 +87,7 @@ class UserAvatar extends Avatar { * @throws NotSquareException if the image is not square * @return void */ - public function set($data) { + public function set($data): void { $img = $this->getAvatarImage($data); $data = $img->data(); @@ -124,7 +115,7 @@ class UserAvatar extends Avatar { * @param IImage|resource|string|\GdImage $data An image object, imagedata or path to the avatar * @return IImage */ - private function getAvatarImage($data) { + private function getAvatarImage($data): IImage { if ($data instanceof IImage) { return $data; } @@ -156,11 +147,8 @@ class UserAvatar extends Avatar { /** * Returns the avatar image type. - * - * @param IImage $avatar - * @return string */ - private function getAvatarImageType(IImage $avatar) { + private function getAvatarImageType(IImage $avatar): string { $type = substr($avatar->mimeType(), -3); if ($type === 'peg') { $type = 'jpg'; @@ -179,7 +167,7 @@ class UserAvatar extends Avatar { * @throws \Exception if the provided image is not valid * @throws NotSquareException if the image is not square */ - private function validateAvatar(IImage $avatar) { + private function validateAvatar(IImage $avatar): void { $type = $this->getAvatarImageType($avatar); if ($type !== 'jpg' && $type !== 'png') { @@ -197,15 +185,14 @@ class UserAvatar extends Avatar { /** * Removes the users avatar. - * @return void * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function remove(bool $silent = false) { + public function remove(bool $silent = false): void { $avatars = $this->folder->getDirectoryListing(); $this->config->setUserValue($this->user->getUID(), 'avatar', 'version', - (int) $this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1); + (string)((int)$this->config->getUserValue($this->user->getUID(), 'avatar', 'version', '0') + 1)); foreach ($avatars as $avatar) { $avatar->delete(); @@ -219,10 +206,9 @@ class UserAvatar extends Avatar { /** * Get the extension of the avatar. If there is no avatar throw Exception * - * @return string * @throws NotFoundException */ - private function getExtension() { + private function getExtension(): string { if ($this->folder->fileExists('avatar.jpg')) { return 'jpg'; } elseif ($this->folder->fileExists('avatar.png')) { @@ -242,9 +228,7 @@ class UserAvatar extends Avatar { * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function getFile($size) { - $size = (int) $size; - + public function getFile(int $size): ISimpleFile { try { $ext = $this->getExtension(); } catch (NotFoundException $e) { @@ -304,8 +288,6 @@ class UserAvatar extends Avatar { /** * Returns the user display name. - * - * @return string */ public function getDisplayName(): string { return $this->user->getDisplayName(); @@ -320,7 +302,7 @@ class UserAvatar extends Avatar { * @throws NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function userChanged($feature, $oldValue, $newValue) { + public function userChanged(string $feature, $oldValue, $newValue): void { // If the avatar is not generated (so an uploaded image) we skip this if (!$this->folder->fileExists('generated')) { return; @@ -331,8 +313,6 @@ class UserAvatar extends Avatar { /** * Check if the avatar of a user is a custom uploaded one - * - * @return bool */ public function isCustomAvatar(): bool { return $this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', 'false') !== 'true'; diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php index 7ab86df8455..20176e45125 100644 --- a/lib/private/BackgroundJob/JobList.php +++ b/lib/private/BackgroundJob/JobList.php @@ -40,21 +40,10 @@ use OCP\IConfig; use OCP\IDBConnection; class JobList implements IJobList { + protected IDBConnection $connection; + protected IConfig $config; + protected ITimeFactory $timeFactory; - /** @var IDBConnection */ - protected $connection; - - /**@var IConfig */ - protected $config; - - /**@var ITimeFactory */ - protected $timeFactory; - - /** - * @param IDBConnection $connection - * @param IConfig $config - * @param ITimeFactory $timeFactory - */ public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) { $this->connection = $connection; $this->config = $config; @@ -62,10 +51,10 @@ class JobList implements IJobList { } /** - * @param IJob|string $job + * @param IJob|class-string<IJob> $job * @param mixed $argument */ - public function add($job, $argument = null) { + public function add($job, $argument = null): void { if ($job instanceof IJob) { $class = get_class($job); } else { @@ -101,7 +90,7 @@ class JobList implements IJobList { * @param IJob|string $job * @param mixed $argument */ - public function remove($job, $argument = null) { + public function remove($job, $argument = null): void { if ($job instanceof IJob) { $class = get_class($job); } else { @@ -133,24 +122,20 @@ class JobList implements IJobList { } } - /** - * @param int $id - */ - protected function removeById($id) { + protected function removeById(int $id): void { $query = $this->connection->getQueryBuilder(); $query->delete('jobs') ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } /** * check if a job is in the list * - * @param IJob|string $job + * @param IJob|class-string<IJob> $job * @param mixed $argument - * @return bool */ - public function has($job, $argument) { + public function has($job, $argument): bool { if ($job instanceof IJob) { $class = get_class($job); } else { @@ -165,7 +150,7 @@ class JobList implements IJobList { ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argument)))) ->setMaxResults(1); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -177,13 +162,33 @@ class JobList implements IJobList { * * @return IJob[] * @deprecated 9.0.0 - This method is dangerous since it can cause load and - * memory problems when creating too many instances. + * memory problems when creating too many instances. Use getJobs instead. + */ + public function getAll(): array { + return $this->getJobs(null, null, 0); + } + + /** + * @param IJob|class-string<IJob>|null $job + * @return IJob[] */ - public function getAll() { + public function getJobs($job, ?int $limit, int $offset): array { $query = $this->connection->getQueryBuilder(); $query->select('*') - ->from('jobs'); - $result = $query->execute(); + ->from('jobs') + ->setMaxResults($limit) + ->setFirstResult($offset); + + if ($job !== null) { + if ($job instanceof IJob) { + $class = get_class($job); + } else { + $class = $job; + } + $query->where($query->expr()->eq('class', $query->createNamedParameter($class))); + } + + $result = $query->executeQuery(); $jobs = []; while ($row = $result->fetch()) { @@ -199,9 +204,6 @@ class JobList implements IJobList { /** * get the next job in the list - * - * @param bool $onlyTimeSensitive - * @return IJob|null */ public function getNext(bool $onlyTimeSensitive = false): ?IJob { $query = $this->connection->getQueryBuilder(); @@ -224,7 +226,7 @@ class JobList implements IJobList { ->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at'))) ->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked'))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -232,7 +234,7 @@ class JobList implements IJobList { $update->setParameter('jobid', $row['id']); $update->setParameter('reserved_at', $row['reserved_at']); $update->setParameter('last_checked', $row['last_checked']); - $count = $update->execute(); + $count = $update->executeStatement(); if ($count === 0) { // Background job already executed elsewhere, try again. @@ -247,7 +249,7 @@ class JobList implements IJobList { ->set('reserved_at', $reset->expr()->literal(0, IQueryBuilder::PARAM_INT)) ->set('last_checked', $reset->createNamedParameter($this->timeFactory->getTime() + 12 * 3600, IQueryBuilder::PARAM_INT)) ->where($reset->expr()->eq('id', $reset->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT))); - $reset->execute(); + $reset->executeStatement(); // Background job from disabled app, try again. return $this->getNext($onlyTimeSensitive); @@ -259,11 +261,7 @@ class JobList implements IJobList { } } - /** - * @param int $id - * @return IJob|null - */ - public function getById($id) { + public function getById(int $id): ?IJob { $row = $this->getDetailsById($id); if ($row) { @@ -292,15 +290,14 @@ class JobList implements IJobList { /** * get the job object from a row in the db * - * @param array $row - * @return IJob|null + * @param array{class:class-string<IJob>, id:mixed, last_run:mixed, argument:string} $row */ - private function buildJob($row) { + private function buildJob(array $row): ?IJob { try { try { // Try to load the job as a service /** @var IJob $job */ - $job = \OC::$server->query($row['class']); + $job = \OCP\Server::get($row['class']); } catch (QueryException $e) { if (class_exists($row['class'])) { $class = $row['class']; @@ -327,33 +324,27 @@ class JobList implements IJobList { /** * set the job that was last ran - * - * @param IJob $job */ - public function setLastJob(IJob $job) { + public function setLastJob(IJob $job): void { $this->unlockJob($job); - $this->config->setAppValue('backgroundjob', 'lastjob', $job->getId()); + $this->config->setAppValue('backgroundjob', 'lastjob', (string)$job->getId()); } /** * Remove the reservation for a job - * - * @param IJob $job */ - public function unlockJob(IJob $job) { + public function unlockJob(IJob $job): void { $query = $this->connection->getQueryBuilder(); $query->update('jobs') ->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT)) ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } /** * set the lastRun of $job to now - * - * @param IJob $job */ - public function setLastRun(IJob $job) { + public function setLastRun(IJob $job): void { $query = $this->connection->getQueryBuilder(); $query->update('jobs') ->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) @@ -364,25 +355,23 @@ class JobList implements IJobList { $query->set('time_sensitive', $query->createNamedParameter(IJob::TIME_INSENSITIVE)); } - $query->execute(); + $query->executeStatement(); } /** - * @param IJob $job - * @param $timeTaken + * @param int $timeTaken */ - public function setExecutionTime(IJob $job, $timeTaken) { + public function setExecutionTime(IJob $job, $timeTaken): void { $query = $this->connection->getQueryBuilder(); $query->update('jobs') ->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT)) ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); - $query->execute(); + $query->executeStatement(); } /** * Reset the $job so it executes on the next trigger * - * @param IJob $job * @since 23.0.0 */ public function resetBackgroundJob(IJob $job): void { diff --git a/lib/private/Cache/CappedMemoryCache.php b/lib/private/Cache/CappedMemoryCache.php index 6063b5e7110..31e8ef3e720 100644 --- a/lib/private/Cache/CappedMemoryCache.php +++ b/lib/private/Cache/CappedMemoryCache.php @@ -28,6 +28,7 @@ use OCP\ICache; * * Uses a simple FIFO expiry mechanism * @template T + * @deprecated use OCP\Cache\CappedMemoryCache instead */ class CappedMemoryCache implements ICache, \ArrayAccess { private $capacity; diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index 12304a66ce9..9beecdaa6cb 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -95,7 +95,7 @@ class UserPlugin implements ISearchPlugin { $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; $this->shareeEnumerationFullMatchUserId = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes'; $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; - $this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no') === 'yes'; + $this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; } public function search($search, $limit, $offset, ISearchResult $searchResult) { diff --git a/lib/private/Command/ClosureJob.php b/lib/private/Command/ClosureJob.php index 498fe6d1d96..5639852e4db 100644 --- a/lib/private/Command/ClosureJob.php +++ b/lib/private/Command/ClosureJob.php @@ -23,10 +23,13 @@ namespace OC\Command; use OC\BackgroundJob\QueuedJob; +use Laravel\SerializableClosure\SerializableClosure as LaravelClosure; +use Opis\Closure\SerializableClosure as OpisClosure; class ClosureJob extends QueuedJob { protected function run($serializedCallable) { - $callable = \Opis\Closure\unserialize($serializedCallable); + $callable = unserialize($serializedCallable, [LaravelClosure::class, OpisClosure::class]); + $callable = $callable->getClosure(); if (is_callable($callable)) { $callable(); } else { diff --git a/lib/private/Command/CommandJob.php b/lib/private/Command/CommandJob.php index 6fa0c6d7ceb..5b267162c81 100644 --- a/lib/private/Command/CommandJob.php +++ b/lib/private/Command/CommandJob.php @@ -30,7 +30,7 @@ use OCP\Command\ICommand; */ class CommandJob extends QueuedJob { protected function run($serializedCommand) { - $command = \Opis\Closure\unserialize($serializedCommand); + $command = unserialize($serializedCommand); if ($command instanceof ICommand) { $command->handle(); } else { diff --git a/lib/private/Command/CronBus.php b/lib/private/Command/CronBus.php index 89a739617d0..8749ad0bff5 100644 --- a/lib/private/Command/CronBus.php +++ b/lib/private/Command/CronBus.php @@ -26,6 +26,7 @@ namespace OC\Command; use OCP\Command\ICommand; +use Laravel\SerializableClosure\SerializableClosure; class CronBus extends AsyncBus { /** @@ -67,9 +68,9 @@ class CronBus extends AsyncBus { */ private function serializeCommand($command) { if ($command instanceof \Closure) { - return \Opis\Closure\serialize($command); + return serialize(new SerializableClosure($command)); } elseif (is_callable($command) or $command instanceof ICommand) { - return \Opis\Closure\serialize($command); + return serialize($command); } else { throw new \InvalidArgumentException('Invalid command'); } diff --git a/lib/private/Comments/Comment.php b/lib/private/Comments/Comment.php index 2b338efc75f..c481e36f95b 100644 --- a/lib/private/Comments/Comment.php +++ b/lib/private/Comments/Comment.php @@ -45,6 +45,7 @@ class Comment implements IComment { 'creationDT' => null, 'latestChildDT' => null, 'reactions' => null, + 'expire_date' => null, ]; /** @@ -350,13 +351,9 @@ class Comment implements IComment { } /** - * sets the date of the most recent child - * - * @param \DateTime $dateTime - * @return IComment - * @since 9.0.0 + * @inheritDoc */ - public function setLatestChildDateTime(\DateTime $dateTime = null) { + public function setLatestChildDateTime(?\DateTime $dateTime = null) { $this->data['latestChildDT'] = $dateTime; return $this; } @@ -447,6 +444,21 @@ class Comment implements IComment { } /** + * @inheritDoc + */ + public function setExpireDate(?\DateTime $dateTime): IComment { + $this->data['expire_date'] = $dateTime; + return $this; + } + + /** + * @inheritDoc + */ + public function getExpireDate(): ?\DateTime { + return $this->data['expire_date']; + } + + /** * sets the comment data based on an array with keys as taken from the * database. * diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index b7532222c33..53603e51e56 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -107,6 +107,9 @@ class Manager implements ICommentsManager { if (!is_null($data['latest_child_timestamp'])) { $data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']); } + if (!is_null($data['expire_date'])) { + $data['expire_date'] = new \DateTime($data['expire_date']); + } $data['children_count'] = (int)$data['children_count']; $data['reference_id'] = $data['reference_id'] ?? null; if ($this->supportReactions()) { @@ -167,7 +170,6 @@ class Manager implements ICommentsManager { if ($comment->getId() === '') { $comment->setChildrenCount(0); - $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC'))); $comment->setLatestChildDateTime(null); } @@ -1203,6 +1205,7 @@ class Manager implements ICommentsManager { 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'), 'object_type' => $qb->createNamedParameter($comment->getObjectType()), 'object_id' => $qb->createNamedParameter($comment->getObjectId()), + 'expire_date' => $qb->createNamedParameter($comment->getExpireDate(), 'datetime'), ]; if ($tryWritingReferenceId) { @@ -1642,4 +1645,25 @@ class Manager implements ICommentsManager { $this->initialStateService->provideInitialState('comments', 'max-message-length', IComment::MAX_MESSAGE_LENGTH); Util::addScript('comments', 'comments-app'); } + + /** + * @inheritDoc + */ + public function deleteCommentsExpiredAtObject(string $objectType, string $objectId = ''): bool { + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('comments') + ->where($qb->expr()->lte('expire_date', + $qb->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATE))) + ->andWhere($qb->expr()->eq('object_type', $qb->createNamedParameter($objectType))); + + if ($objectId !== '') { + $qb->andWhere($qb->expr()->eq('object_id', $qb->createNamedParameter($objectId))); + } + + $affectedRows = $qb->executeStatement(); + + $this->commentsCache = []; + + return $affectedRows > 0; + } } diff --git a/lib/private/Config.php b/lib/private/Config.php index b044d0731a3..37708357339 100644 --- a/lib/private/Config.php +++ b/lib/private/Config.php @@ -231,6 +231,14 @@ class Config { unset($CONFIG); include $file; + if (!defined('PHPUNIT_RUN') && headers_sent()) { + // syntax issues in the config file like leading spaces causing PHP to send output + $errorMessage = sprintf('Config file has leading content, please remove everything before "<?php" in %s', basename($file)); + if (!defined('OC_CONSOLE')) { + print(\OCP\Util::sanitizeHTML($errorMessage)); + } + throw new \Exception($errorMessage); + } if (isset($CONFIG) && is_array($CONFIG)) { $this->cache = array_merge($this->cache, $CONFIG); } diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php index 12d54b48fa9..fc48f57e499 100644 --- a/lib/private/Console/Application.php +++ b/lib/private/Console/Application.php @@ -34,6 +34,7 @@ use OC\MemoryInfo; use OC\NeedsUpdateException; use OC_App; use OCP\AppFramework\QueryException; +use OCP\App\IAppManager; use OCP\Console\ConsoleEvent; use OCP\IConfig; use OCP\IRequest; @@ -117,13 +118,14 @@ class Application { $this->writeMaintenanceModeInfo($input, $output); } else { OC_App::loadApps(); - foreach (\OC::$server->getAppManager()->getInstalledApps() as $app) { + $appManager = \OCP\Server::get(IAppManager::class); + foreach ($appManager->getInstalledApps() as $app) { $appPath = \OC_App::getAppPath($app); if ($appPath === false) { continue; } // load commands using info.xml - $info = \OC_App::getAppInfo($app); + $info = $appManager->getAppInfo($app); if (isset($info['commands'])) { $this->loadCommandsFromInfoXml($info['commands']); } diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index 020e8604910..dd4bd973fa9 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -123,7 +123,7 @@ class ContactsStore implements IContactsStore { * 2. if the `shareapi_exclude_groups` config option is enabled and the * current user is in an excluded group it will filter all local users. * 3. if the `shareapi_only_share_with_group_members` config option is - * enabled it will filter all users which doens't have a common group + * enabled it will filter all users which doesn't have a common group * with the current user. * * @param IUser $self @@ -150,7 +150,7 @@ class ContactsStore implements IContactsStore { $selfGroups = $this->groupManager->getUserGroupIds($self); if ($excludedGroups) { - $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list'); + $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); $decodedExcludeGroups = json_decode($excludedGroups, true); $excludeGroupsList = $decodedExcludeGroups ?? []; diff --git a/lib/private/ContactsManager.php b/lib/private/ContactsManager.php index 937fb94a09a..68783e3f79b 100644 --- a/lib/private/ContactsManager.php +++ b/lib/private/ContactsManager.php @@ -6,6 +6,7 @@ * @author Joas Schilling <coding@schilljs.com> * @author John Molakvoæ <skjnldsv@protonmail.com> * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Citharel <nextcloud@tcit.fr> * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Tobia De Koninck <tobia@ledfan.be> * @@ -85,7 +86,7 @@ class ContactsManager implements IManager { /** * This function can be used to delete the contact identified by the given id * - * @param object $id the unique identifier to a contact + * @param int $id the unique identifier to a contact * @param string $address_book_key identifier of the address book in which the contact shall be deleted * @return bool successful or not */ diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php index 13bbe8dc5d0..4b7e4d3a040 100644 --- a/lib/private/DB/MigrationService.php +++ b/lib/private/DB/MigrationService.php @@ -45,35 +45,25 @@ use OCP\Migration\IOutput; use Psr\Log\LoggerInterface; class MigrationService { - - /** @var boolean */ - private $migrationTableCreated; - /** @var array */ - private $migrations; - /** @var IOutput */ - private $output; - /** @var Connection */ - private $connection; - /** @var string */ - private $appName; - /** @var bool */ - private $checkOracle; + private bool $migrationTableCreated; + private array $migrations; + private string $migrationsPath; + private string $migrationsNamespace; + private IOutput $output; + private Connection $connection; + private string $appName; + private bool $checkOracle; /** - * MigrationService constructor. - * - * @param $appName - * @param Connection $connection - * @param AppLocator $appLocator - * @param IOutput|null $output * @throws \Exception */ - public function __construct($appName, Connection $connection, IOutput $output = null, AppLocator $appLocator = null) { + public function __construct($appName, Connection $connection, ?IOutput $output = null, ?AppLocator $appLocator = null) { $this->appName = $appName; $this->connection = $connection; - $this->output = $output; - if (null === $this->output) { + if ($output === null) { $this->output = new SimpleOutput(\OC::$server->get(LoggerInterface::class), $appName); + } else { + $this->output = $output; } if ($appName === 'core') { @@ -104,6 +94,7 @@ class MigrationService { } } } + $this->migrationTableCreated = false; } /** diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php index ae4f19f5d18..333984bde71 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php @@ -114,12 +114,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function comparison($x, string $operator, $y, $type = null): string { + public function comparison($x, string $operator, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->comparison($x, $operator, $y); + return new QueryFunction($this->expressionBuilder->comparison($x, $operator, $y)); } /** @@ -137,12 +137,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function eq($x, $y, $type = null): string { + public function eq($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->eq($x, $y); + return new QueryFunction($this->expressionBuilder->eq($x, $y)); } /** @@ -159,12 +159,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function neq($x, $y, $type = null): string { + public function neq($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->neq($x, $y); + return new QueryFunction($this->expressionBuilder->neq($x, $y)); } /** @@ -181,12 +181,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function lt($x, $y, $type = null): string { + public function lt($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->lt($x, $y); + return new QueryFunction($this->expressionBuilder->lt($x, $y)); } /** @@ -203,12 +203,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function lte($x, $y, $type = null): string { + public function lte($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->lte($x, $y); + return new QueryFunction($this->expressionBuilder->lte($x, $y)); } /** @@ -225,12 +225,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function gt($x, $y, $type = null): string { + public function gt($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->gt($x, $y); + return new QueryFunction($this->expressionBuilder->gt($x, $y)); } /** @@ -247,12 +247,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function gte($x, $y, $type = null): string { + public function gte($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->gte($x, $y); + return new QueryFunction($this->expressionBuilder->gte($x, $y)); } /** @@ -260,11 +260,11 @@ class ExpressionBuilder implements IExpressionBuilder { * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be restricted by IS NULL. * - * @return string + * @return IQueryFunction */ - public function isNull($x): string { + public function isNull($x): IQueryFunction { $x = $this->helper->quoteColumnName($x); - return $this->expressionBuilder->isNull($x); + return new QueryFunction($this->expressionBuilder->isNull($x)); } /** @@ -272,11 +272,11 @@ class ExpressionBuilder implements IExpressionBuilder { * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be restricted by IS NOT NULL. * - * @return string + * @return IQueryFunction */ - public function isNotNull($x): string { + public function isNotNull($x): IQueryFunction { $x = $this->helper->quoteColumnName($x); - return $this->expressionBuilder->isNotNull($x); + return new QueryFunction($this->expressionBuilder->isNotNull($x)); } /** @@ -287,12 +287,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function like($x, $y, $type = null): string { + public function like($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->like($x, $y); + return new QueryFunction($this->expressionBuilder->like($x, $y)); } /** @@ -303,11 +303,11 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction * @since 9.0.0 */ - public function iLike($x, $y, $type = null): string { - return $this->expressionBuilder->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y)); + public function iLike($x, $y, $type = null): IQueryFunction { + return new QueryFunction($this->expressionBuilder->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y))); } /** @@ -318,12 +318,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function notLike($x, $y, $type = null): string { + public function notLike($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->notLike($x, $y); + return new QueryFunction($this->expressionBuilder->notLike($x, $y)); } /** @@ -334,12 +334,12 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function in($x, $y, $type = null): string { + public function in($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnNames($y); - return $this->expressionBuilder->in($x, $y); + return new QueryFunction($this->expressionBuilder->in($x, $y)); } /** @@ -350,34 +350,34 @@ class ExpressionBuilder implements IExpressionBuilder { * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants * required when comparing text fields for oci compatibility * - * @return string + * @return IQueryFunction */ - public function notIn($x, $y, $type = null): string { + public function notIn($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnNames($y); - return $this->expressionBuilder->notIn($x, $y); + return new QueryFunction($this->expressionBuilder->notIn($x, $y)); } /** * Creates a $x = '' statement, because Oracle needs a different check * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison. - * @return string + * @return IQueryFunction * @since 13.0.0 */ - public function emptyString($x): string { - return $this->eq($x, $this->literal('', IQueryBuilder::PARAM_STR)); + public function emptyString($x): IQueryFunction { + return new QueryFunction($this->eq($x, $this->literal('', IQueryBuilder::PARAM_STR))); } /** * Creates a `$x <> ''` statement, because Oracle needs a different check * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison. - * @return string + * @return IQueryFunction * @since 13.0.0 */ - public function nonEmptyString($x): string { - return $this->neq($x, $this->literal('', IQueryBuilder::PARAM_STR)); + public function nonEmptyString($x): IQueryFunction { + return new QueryFunction($this->neq($x, $this->literal('', IQueryBuilder::PARAM_STR))); } /** diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php index 3bb54d4b26e..74209d0c3da 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php @@ -49,10 +49,10 @@ class MySqlExpressionBuilder extends ExpressionBuilder { /** * @inheritdoc */ - public function iLike($x, $y, $type = null): string { + public function iLike($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->comparison($x, ' COLLATE ' . $this->collation . ' LIKE', $y); + return new QueryFunction($this->expressionBuilder->comparison($x, ' COLLATE ' . $this->collation . ' LIKE', $y)); } /** diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php index f9b58d7d8ed..20d68b30b33 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php @@ -49,101 +49,101 @@ class OCIExpressionBuilder extends ExpressionBuilder { /** * @inheritdoc */ - public function comparison($x, string $operator, $y, $type = null): string { + public function comparison($x, string $operator, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->comparison($x, $operator, $y); + return new QueryFunction($this->expressionBuilder->comparison($x, $operator, $y)); } /** * @inheritdoc */ - public function eq($x, $y, $type = null): string { + public function eq($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->eq($x, $y); + return new QueryFunction($this->expressionBuilder->eq($x, $y)); } /** * @inheritdoc */ - public function neq($x, $y, $type = null): string { + public function neq($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->neq($x, $y); + return new QueryFunction($this->expressionBuilder->neq($x, $y)); } /** * @inheritdoc */ - public function lt($x, $y, $type = null): string { + public function lt($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->lt($x, $y); + return new QueryFunction($this->expressionBuilder->lt($x, $y)); } /** * @inheritdoc */ - public function lte($x, $y, $type = null): string { + public function lte($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->lte($x, $y); + return new QueryFunction($this->expressionBuilder->lte($x, $y)); } /** * @inheritdoc */ - public function gt($x, $y, $type = null): string { + public function gt($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->gt($x, $y); + return new QueryFunction($this->expressionBuilder->gt($x, $y)); } /** * @inheritdoc */ - public function gte($x, $y, $type = null): string { + public function gte($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->gte($x, $y); + return new QueryFunction($this->expressionBuilder->gte($x, $y)); } /** * @inheritdoc */ - public function in($x, $y, $type = null): string { + public function in($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->in($x, $y); + return new QueryFunction($this->expressionBuilder->in($x, $y)); } /** * @inheritdoc */ - public function notIn($x, $y, $type = null): string { + public function notIn($x, $y, $type = null): IQueryFunction { $x = $this->prepareColumn($x, $type); $y = $this->prepareColumn($y, $type); - return $this->expressionBuilder->notIn($x, $y); + return new QueryFunction($this->expressionBuilder->notIn($x, $y)); } /** * Creates a $x = '' statement, because Oracle needs a different check * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison. - * @return string + * @return IQueryFunction * @since 13.0.0 */ - public function emptyString($x): string { + public function emptyString($x): IQueryFunction { return $this->isNull($x); } @@ -151,10 +151,10 @@ class OCIExpressionBuilder extends ExpressionBuilder { * Creates a `$x <> ''` statement, because Oracle needs a different check * * @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison. - * @return string + * @return IQueryFunction * @since 13.0.0 */ - public function nonEmptyString($x): string { + public function nonEmptyString($x): IQueryFunction { return $this->isNotNull($x); } @@ -182,14 +182,14 @@ class OCIExpressionBuilder extends ExpressionBuilder { /** * @inheritdoc */ - public function like($x, $y, $type = null): string { - return parent::like($x, $y, $type) . " ESCAPE '\\'"; + public function like($x, $y, $type = null): IQueryFunction { + return new QueryFunction(parent::like($x, $y, $type) . " ESCAPE '\\'"); } /** * @inheritdoc */ - public function iLike($x, $y, $type = null): string { + public function iLike($x, $y, $type = null): IQueryFunction { return $this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y)); } } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php index 0fba5363a28..cbebe97ae87 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php @@ -52,9 +52,9 @@ class PgSqlExpressionBuilder extends ExpressionBuilder { /** * @inheritdoc */ - public function iLike($x, $y, $type = null): string { + public function iLike($x, $y, $type = null): IQueryFunction { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->comparison($x, 'ILIKE', $y); + return new QueryFunction($this->expressionBuilder->comparison($x, 'ILIKE', $y)); } } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php index 289aa09b003..5425138fa6c 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php @@ -23,15 +23,18 @@ */ namespace OC\DB\QueryBuilder\ExpressionBuilder; +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\IQueryFunction; + class SqliteExpressionBuilder extends ExpressionBuilder { /** * @inheritdoc */ - public function like($x, $y, $type = null): string { - return parent::like($x, $y, $type) . " ESCAPE '\\'"; + public function like($x, $y, $type = null): IQueryFunction { + return new QueryFunction(parent::like($x, $y, $type) . " ESCAPE '\\'"); } - public function iLike($x, $y, $type = null): string { - return $this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y), $type); + public function iLike($x, $y, $type = null): IQueryFunction { + return new QueryFunction($this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y), $type)); } } diff --git a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php index e0a7549a0ad..408a879d624 100644 --- a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php +++ b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php @@ -121,4 +121,15 @@ class FunctionBuilder implements IFunctionBuilder { public function least($x, $y): IQueryFunction { return new QueryFunction('LEAST(' . $this->helper->quoteColumnName($x) . ', ' . $this->helper->quoteColumnName($y) . ')'); } + + public function case(array $whens, $else): IQueryFunction { + if (count($whens) < 1) { + return new QueryFunction($this->helper->quoteColumnName($else)); + } + + $whenParts = array_map(function (array $when) { + return 'WHEN ' . $this->helper->quoteColumnName($when['when']) . ' THEN ' . $this->helper->quoteColumnName($when['then']); + }, $whens); + return new QueryFunction('CASE ' . implode(' ', $whenParts) . ' ELSE ' . $this->helper->quoteColumnName($else) . ' END'); + } } diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 71dd18fd68b..d991cbd1dd5 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -715,11 +715,12 @@ class QueryBuilder implements IQueryBuilder { * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. - * @param string|ICompositeExpression|null $condition The condition for the join. + * @param string|IQueryFunction|ICompositeExpression|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function join($fromAlias, $join, $alias, $condition = null) { + $condition = $condition !== null ? (string)$condition : null; $this->queryBuilder->join( $this->quoteAlias($fromAlias), $this->getTableName($join), @@ -743,11 +744,12 @@ class QueryBuilder implements IQueryBuilder { * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. - * @param string|ICompositeExpression|null $condition The condition for the join. + * @param string|IQueryFunction|ICompositeExpression|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function innerJoin($fromAlias, $join, $alias, $condition = null) { + $condition = $condition !== null ? (string)$condition : null; $this->queryBuilder->innerJoin( $this->quoteAlias($fromAlias), $this->getTableName($join), @@ -771,11 +773,12 @@ class QueryBuilder implements IQueryBuilder { * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. - * @param string|ICompositeExpression|null $condition The condition for the join. + * @param string|IQueryFunction|ICompositeExpression|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function leftJoin($fromAlias, $join, $alias, $condition = null) { + $condition = $condition !== null ? (string)$condition : null; $this->queryBuilder->leftJoin( $this->quoteAlias($fromAlias), $this->getTableName($join), @@ -799,11 +802,12 @@ class QueryBuilder implements IQueryBuilder { * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. - * @param string|ICompositeExpression|null $condition The condition for the join. + * @param string|IQueryFunction|ICompositeExpression|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function rightJoin($fromAlias, $join, $alias, $condition = null) { + $condition = $condition !== null ? (string)$condition : null; $this->queryBuilder->rightJoin( $this->quoteAlias($fromAlias), $this->getTableName($join), @@ -848,7 +852,7 @@ class QueryBuilder implements IQueryBuilder { * ->from('users', 'u') * ->where('u.id = ?'); * - * // You can optionally programatically build and/or expressions + * // You can optionally programmatically build and/or expressions * $qb = $conn->getQueryBuilder(); * * $or = $qb->expr()->orx(); diff --git a/lib/private/Dashboard/Manager.php b/lib/private/Dashboard/Manager.php index 09525693b4f..2aeedf3174e 100644 --- a/lib/private/Dashboard/Manager.php +++ b/lib/private/Dashboard/Manager.php @@ -27,10 +27,11 @@ declare(strict_types=1); namespace OC\Dashboard; use InvalidArgumentException; -use OCP\AppFramework\QueryException; +use OCP\App\IAppManager; use OCP\Dashboard\IManager; use OCP\Dashboard\IWidget; -use OCP\IServerContainer; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; use Throwable; use Psr\Log\LoggerInterface; @@ -42,10 +43,10 @@ class Manager implements IManager { /** @var IWidget[] */ private $widgets = []; - /** @var IServerContainer */ - private $serverContainer; + private ContainerInterface $serverContainer; + private ?IAppManager $appManager = null; - public function __construct(IServerContainer $serverContainer) { + public function __construct(ContainerInterface $serverContainer) { $this->serverContainer = $serverContainer; } @@ -57,17 +58,25 @@ class Manager implements IManager { $this->widgets[$widget->getId()] = $widget; } - public function lazyRegisterWidget(string $widgetClass): void { - $this->lazyWidgets[] = $widgetClass; + public function lazyRegisterWidget(string $widgetClass, string $appId): void { + $this->lazyWidgets[] = ['class' => $widgetClass, 'appId' => $appId]; } public function loadLazyPanels(): void { - $classes = $this->lazyWidgets; - foreach ($classes as $class) { + if ($this->appManager === null) { + $this->appManager = $this->serverContainer->get(IAppManager::class); + } + $services = $this->lazyWidgets; + foreach ($services as $service) { + /** @psalm-suppress InvalidCatch */ try { + if (!$this->appManager->isEnabledForUser($service['appId'])) { + // all apps are registered, but some may not be enabled for the user + continue; + } /** @var IWidget $widget */ - $widget = $this->serverContainer->query($class); - } catch (QueryException $e) { + $widget = $this->serverContainer->get($service['class']); + } catch (ContainerExceptionInterface $e) { /* * There is a circular dependency between the logger and the registry, so * we can not inject it. Thus the static call. @@ -90,7 +99,7 @@ class Manager implements IManager { */ \OC::$server->get(LoggerInterface::class)->critical( 'Could not register lazy dashboard widget: ' . $e->getMessage(), - ['excepiton' => $e] + ['exception' => $e] ); } @@ -111,7 +120,7 @@ class Manager implements IManager { } catch (Throwable $e) { \OC::$server->get(LoggerInterface::class)->critical( 'Error during dashboard widget loading: ' . $e->getMessage(), - ['excepiton' => $e] + ['exception' => $e] ); } } diff --git a/lib/private/Diagnostics/QueryLogger.php b/lib/private/Diagnostics/QueryLogger.php index 40d68d94ae3..5f401751077 100644 --- a/lib/private/Diagnostics/QueryLogger.php +++ b/lib/private/Diagnostics/QueryLogger.php @@ -24,7 +24,7 @@ */ namespace OC\Diagnostics; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCP\Diagnostics\IQueryLogger; class QueryLogger implements IQueryLogger { diff --git a/lib/private/Encryption/File.php b/lib/private/Encryption/File.php index 2d7e23a8883..87bc35bc159 100644 --- a/lib/private/Encryption/File.php +++ b/lib/private/Encryption/File.php @@ -27,29 +27,24 @@ */ namespace OC\Encryption; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCA\Files_External\Service\GlobalStoragesService; +use OCP\App\IAppManager; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Share\IManager; class File implements \OCP\Encryption\IFile { - - /** @var Util */ - protected $util; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IManager */ - private $shareManager; + protected Util $util; + private IRootFolder $rootFolder; + private IManager $shareManager; /** - * cache results of already checked folders - * + * Cache results of already checked folders * @var CappedMemoryCache<array> */ protected CappedMemoryCache $cache; + private ?IAppManager $appManager = null; public function __construct(Util $util, IRootFolder $rootFolder, @@ -60,6 +55,14 @@ class File implements \OCP\Encryption\IFile { $this->shareManager = $shareManager; } + public function getAppManager(): IAppManager { + // Lazy evaluate app manager as it initialize the db too early otherwise + if ($this->appManager) { + return $this->appManager; + } + $this->appManager = \OCP\Server::get(IAppManager::class); + return $this->appManager; + } /** * Get list of users with access to the file @@ -110,7 +113,7 @@ class File implements \OCP\Encryption\IFile { } // check if it is a group mount - if (\OCP\App::isEnabled("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 a2d6b990a88..5081bcccf94 100644 --- a/lib/private/Encryption/HookManager.php +++ b/lib/private/Encryption/HookManager.php @@ -25,39 +25,51 @@ namespace OC\Encryption; use OC\Files\Filesystem; use OC\Files\View; +use OC\Files\SetupManager; use Psr\Log\LoggerInterface; class HookManager { - /** - * @var Update - */ - private static $updater; + private static ?Update $updater = null; - public static function postShared($params) { + public static function postShared($params): void { self::getUpdate()->postShared($params); } - public static function postUnshared($params) { - self::getUpdate()->postUnshared($params); + public static function postUnshared($params): void { + // In case the unsharing happens in a background job, we don't have + // a session and we load instead the user from the UserManager + $path = Filesystem::getPath($params['fileSource']); + $owner = Filesystem::getOwner($path); + self::getUpdate($owner)->postUnshared($params); } - public static function postRename($params) { + public static function postRename($params): void { self::getUpdate()->postRename($params); } - public static function postRestore($params) { + public static function postRestore($params): void { self::getUpdate()->postRestore($params); } - /** - * @return Update - */ - private static function getUpdate() { + private static function getUpdate(?string $owner = null): Update { if (is_null(self::$updater)) { $user = \OC::$server->getUserSession()->getUser(); + if (!$user && $owner) { + $user = \OC::$server->getUserManager()->get($owner); + } + if (!$user) { + throw new \Exception("Inconsistent data, File unshared, but owner not found. Should not happen"); + } + $uid = ''; if ($user) { $uid = $user->getUID(); } + + $setupManager = \OC::$server->get(SetupManager::class); + if (!$setupManager->isSetupComplete($user)) { + $setupManager->setupForUser($user); + } + self::$updater = new Update( new View(), new Util( diff --git a/lib/private/Encryption/Util.php b/lib/private/Encryption/Util.php index 174af2e8b89..410ea19da81 100644 --- a/lib/private/Encryption/Util.php +++ b/lib/private/Encryption/Util.php @@ -34,9 +34,12 @@ use OC\Files\Filesystem; use OC\Files\View; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\GlobalStoragesService; +use OCP\App\IAppManager; use OCP\Encryption\IEncryptionModule; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IUser; +use OCP\IUserManager; class Util { public const HEADER_START = 'HBEGIN'; @@ -65,29 +68,23 @@ class Util { /** @var array */ protected $ocHeaderKeys; - /** @var \OC\User\Manager */ - protected $userManager; - /** @var IConfig */ protected $config; /** @var array paths excluded from encryption */ protected $excludedPaths; - - /** @var \OC\Group\Manager $manager */ - protected $groupManager; + protected IGroupManager $groupManager; + protected IUserManager $userManager; /** * * @param View $rootView - * @param \OC\User\Manager $userManager - * @param \OC\Group\Manager $groupManager * @param IConfig $config */ public function __construct( View $rootView, - \OC\User\Manager $userManager, - \OC\Group\Manager $groupManager, + IUserManager $userManager, + IGroupManager $groupManager, IConfig $config) { $this->ocHeaderKeys = [ self::HEADER_ENCRYPTION_MODULE_KEY @@ -275,7 +272,7 @@ class Util { } else { $result = array_merge($result, $users); - $groupManager = \OC::$server->getGroupManager(); + $groupManager = $this->groupManager; foreach ($groups as $group) { $groupObject = $groupManager->get($group); if ($groupObject) { @@ -299,7 +296,8 @@ class Util { * @return boolean */ public function isSystemWideMountPoint($path, $uid) { - if (\OCP\App::isEnabled("files_external")) { + // No DI here as this initialise the db too soon + if (\OCP\Server::get(IAppManager::class)->isEnabledForUser("files_external")) { /** @var GlobalStoragesService $storageService */ $storageService = \OC::$server->get(GlobalStoragesService::class); $storages = $storageService->getAllStorages(); @@ -377,32 +375,29 @@ class Util { } /** - * check if recovery key is enabled for user - * - * @param string $uid - * @return boolean + * Check if recovery key is enabled for user */ - public function recoveryEnabled($uid) { + public function recoveryEnabled(string $uid): bool { $enabled = $this->config->getUserValue($uid, 'encryption', 'recovery_enabled', '0'); return $enabled === '1'; } /** - * set new key storage root + * Set new key storage root * * @param string $root new key store root relative to the data folder */ - public function setKeyStorageRoot($root) { + public function setKeyStorageRoot(string $root): void { $this->config->setAppValue('core', 'encryption_key_storage_root', $root); } /** - * get key storage root + * Get key storage root * * @return string key storage root */ - public function getKeyStorageRoot() { + public function getKeyStorageRoot(): string { return $this->config->getAppValue('core', 'encryption_key_storage_root', ''); } } diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index c25d4a40363..f077e36d97d 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -150,11 +150,12 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager return (is_array($result)) ? $result : []; } } catch (\Exception $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + // if flat re-sharing is not supported by the remote server // we re-throw the exception and fall back to the old behaviour. // (flat re-shares has been introduced in Nextcloud 9.1) if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { - $this->logger->debug($e->getMessage(), ['exception' => $e]); throw $e; } } diff --git a/lib/private/Files/AppData/AppData.php b/lib/private/Files/AppData/AppData.php index 471de799c2f..237fcb42e03 100644 --- a/lib/private/Files/AppData/AppData.php +++ b/lib/private/Files/AppData/AppData.php @@ -26,7 +26,7 @@ declare(strict_types=1); */ namespace OC\Files\AppData; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\SimpleFS\SimpleFolder; use OC\SystemConfig; use OCP\Files\Folder; diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 949079dfa22..f23635aa01b 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -37,6 +37,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ + namespace OC\Files\Cache; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; @@ -63,7 +64,7 @@ use Psr\Log\LoggerInterface; /** * Metadata cache for a storage * - * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms: + * The cache stores the metadata for all files and folders in a storage and is kept up to date through the following mechanisms: * * - Scanner: scans the storage and updates the cache where needed * - Watcher: checks for changes made to the filesystem outside of the Nextcloud instance and rescans files and folder when a change is detected @@ -188,6 +189,7 @@ class Cache implements ICache { $data['fileid'] = (int)$data['fileid']; $data['parent'] = (int)$data['parent']; $data['size'] = 0 + $data['size']; + $data['unencrypted_size'] = 0 + ($data['unencrypted_size'] ?? 0); $data['mtime'] = (int)$data['mtime']; $data['storage_mtime'] = (int)$data['storage_mtime']; $data['encryptedVersion'] = (int)$data['encrypted']; @@ -428,7 +430,7 @@ class Cache implements ICache { protected function normalizeData(array $data): array { $fields = [ 'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted', - 'etag', 'permissions', 'checksum', 'storage']; + 'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size']; $extensionFields = ['metadata_etag', 'creation_time', 'upload_time']; $doNotCopyStorageMTime = false; @@ -538,7 +540,7 @@ class Cache implements ICache { public function remove($file) { $entry = $this->get($file); - if ($entry) { + if ($entry instanceof ICacheEntry) { $query = $this->getQueryBuilder(); $query->delete('filecache') ->whereFileId($entry->getId()); @@ -580,7 +582,7 @@ class Cache implements ICache { $parentIds = [$entry->getId()]; $queue = [$entry->getId()]; - // we walk depth first trough the file tree, removing all filecache_extended attributes while we walk + // we walk depth first through the file tree, removing all filecache_extended attributes while we walk // and collecting all folder ids to later use to delete the filecache entries while ($entryId = array_pop($queue)) { $children = $this->getFolderContentsById($entryId); @@ -873,8 +875,16 @@ class Cache implements ICache { $id = $entry['fileid']; $query = $this->getQueryBuilder(); - $query->selectAlias($query->func()->sum('size'), 'f1') - ->selectAlias($query->func()->min('size'), 'f2') + $query->selectAlias($query->func()->sum('size'), 'size_sum') + ->selectAlias($query->func()->min('size'), 'size_min') + // in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size + ->selectAlias($query->func()->sum( + $query->func()->case([ + ['when' => $query->expr()->eq('unencrypted_size', $query->expr()->literal(0, IQueryBuilder::PARAM_INT)), 'then' => 'size'], + ], 'unencrypted_size') + ), 'unencrypted_sum') + ->selectAlias($query->func()->min('unencrypted_size'), 'unencrypted_min') + ->selectAlias($query->func()->max('unencrypted_size'), 'unencrypted_max') ->from('filecache') ->whereStorageId($this->getNumericStorageId()) ->whereParent($id); @@ -884,7 +894,7 @@ class Cache implements ICache { $result->closeCursor(); if ($row) { - [$sum, $min] = array_values($row); + ['size_sum' => $sum, 'size_min' => $min, 'unencrypted_sum' => $unencryptedSum, 'unencrypted_min' => $unencryptedMin, 'unencrypted_max' => $unencryptedMax] = $row; $sum = 0 + $sum; $min = 0 + $min; if ($min === -1) { @@ -892,8 +902,23 @@ class Cache implements ICache { } else { $totalSize = $sum; } + if ($unencryptedMin === -1 || $min === -1) { + $unencryptedTotal = $unencryptedMin; + } else { + $unencryptedTotal = $unencryptedSum; + } if ($entry['size'] !== $totalSize) { - $this->update($id, ['size' => $totalSize]); + // only set unencrypted size for a folder if any child entries have it set + if ($unencryptedMax > 0) { + $this->update($id, [ + 'size' => $totalSize, + 'unencrypted_size' => $unencryptedTotal, + ]); + } else { + $this->update($id, [ + 'size' => $totalSize, + ]); + } } } } @@ -927,7 +952,7 @@ class Cache implements ICache { * use the one with the highest id gives the best result with the background scanner, since that is most * likely the folder where we stopped scanning previously * - * @return string|bool the path of the folder or false when no folder matched + * @return string|false the path of the folder or false when no folder matched */ public function getIncomplete() { $query = $this->getQueryBuilder(); diff --git a/lib/private/Files/Cache/CacheEntry.php b/lib/private/Files/Cache/CacheEntry.php index 12f0273fb6e..8ac76acf6d1 100644 --- a/lib/private/Files/Cache/CacheEntry.php +++ b/lib/private/Files/Cache/CacheEntry.php @@ -132,4 +132,12 @@ class CacheEntry implements ICacheEntry { public function __clone() { $this->data = array_merge([], $this->data); } + + public function getUnencryptedSize(): int { + if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { + return $this->data['unencrypted_size']; + } else { + return $this->data['size']; + } + } } diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php index b448424c1a8..496a8361d77 100644 --- a/lib/private/Files/Cache/CacheQueryBuilder.php +++ b/lib/private/Files/Cache/CacheQueryBuilder.php @@ -41,12 +41,16 @@ class CacheQueryBuilder extends QueryBuilder { parent::__construct($connection, $systemConfig, $logger); } - public function selectFileCache(string $alias = null) { + public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) { $name = $alias ? $alias : 'filecache'; $this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime', - 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'metadata_etag', 'creation_time', 'upload_time') - ->from('filecache', $name) - ->leftJoin($name, 'filecache_extended', 'fe', $this->expr()->eq("$name.fileid", 'fe.fileid')); + 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'unencrypted_size') + ->from('filecache', $name); + + if ($joinExtendedCache) { + $this->addSelect('metadata_etag', 'creation_time', 'upload_time'); + $this->leftJoin($name, 'filecache_extended', 'fe', $this->expr()->eq("$name.fileid", 'fe.fileid')); + } $this->alias = $name; diff --git a/lib/private/Files/Cache/Propagator.php b/lib/private/Files/Cache/Propagator.php index 270b2b013f5..a0953baa785 100644 --- a/lib/private/Files/Cache/Propagator.php +++ b/lib/private/Files/Cache/Propagator.php @@ -24,6 +24,7 @@ namespace OC\Files\Cache; +use OC\Files\Storage\Wrapper\Encryption; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\IPropagator; use OCP\Files\Storage\IReliableEtagStorage; @@ -65,7 +66,7 @@ class Propagator implements IPropagator { * @param int $sizeDifference number of bytes the file has grown */ public function propagateChange($internalPath, $time, $sizeDifference = 0) { - // Do not propogate changes in ignored paths + // Do not propagate changes in ignored paths foreach ($this->ignore as $ignore) { if (strpos($internalPath, $ignore) === 0) { return; @@ -113,6 +114,20 @@ class Propagator implements IPropagator { ->andWhere($builder->expr()->in('path_hash', $hashParams)) ->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT))); + if ($this->storage->instanceOfStorage(Encryption::class)) { + // in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size + $builder->set('unencrypted_size', $builder->func()->greatest( + $builder->func()->add( + $builder->func()->case([ + ['when' => $builder->expr()->eq('unencrypted_size', $builder->expr()->literal(0, IQueryBuilder::PARAM_INT)), 'then' => 'size'] + ], 'unencrypted_size'), + $builder->createNamedParameter($sizeDifference) + ), + $builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT) + )); + } + + $a = $builder->getSQL(); $builder->execute(); } } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index e7bccbf521c..3529ede9746 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -103,7 +103,7 @@ class QuerySearchHelper { $builder = $this->getQueryBuilder(); - $query = $builder->selectFileCache('file'); + $query = $builder->selectFileCache('file', false); if ($this->searchBuilder->shouldJoinTags($searchQuery->getSearchOperation())) { $user = $searchQuery->getUser(); @@ -158,7 +158,7 @@ class QuerySearchHelper { $result->closeCursor(); - // loop trough all caches for each result to see if the result matches that storage + // loop through all caches for each result to see if the result matches that storage // results are grouped by the same array keys as the caches argument to allow the caller to distringuish the source of the results $results = array_fill_keys(array_keys($caches), []); foreach ($rawEntries as $rawEntry) { diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index fb9e5500658..f77c9b71dd7 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -40,7 +40,7 @@ use Psr\Log\LoggerInterface; * a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share') * and a numeric storage id which is referenced in the file cache * - * A mapping between the two storage ids is stored in the database and accessible trough this class + * A mapping between the two storage ids is stored in the database and accessible through this class * * @package OC\Files\Cache */ @@ -135,7 +135,7 @@ class Storage { * Get the numeric of the storage with the provided string id * * @param $storageId - * @return int|null either the numeric storage id or null if the storage id is not knwon + * @return int|null either the numeric storage id or null if the storage id is not known */ public static function getNumericStorageId($storageId) { $storageId = self::adjustStorageId($storageId); diff --git a/lib/private/Files/Cache/StorageGlobal.php b/lib/private/Files/Cache/StorageGlobal.php index a898c435415..74cbd5abdb2 100644 --- a/lib/private/Files/Cache/StorageGlobal.php +++ b/lib/private/Files/Cache/StorageGlobal.php @@ -33,7 +33,7 @@ use OCP\IDBConnection; * a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share') * and a numeric storage id which is referenced in the file cache * - * A mapping between the two storage ids is stored in the database and accessible trough this class + * A mapping between the two storage ids is stored in the database and accessible through this class * * @package OC\Files\Cache */ diff --git a/lib/private/Files/Cache/Updater.php b/lib/private/Files/Cache/Updater.php index 98fb51fe264..f8c187996e6 100644 --- a/lib/private/Files/Cache/Updater.php +++ b/lib/private/Files/Cache/Updater.php @@ -73,14 +73,14 @@ class Updater implements IUpdater { } /** - * Disable updating the cache trough this updater + * Disable updating the cache through this updater */ public function disable() { $this->enabled = false; } /** - * Re-enable the updating of the cache trough this updater + * Re-enable the updating of the cache through this updater */ public function enable() { $this->enabled = true; diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index 7183a6c0d2a..4053042edd9 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -267,7 +267,7 @@ class CacheJail extends CacheWrapper { * use the one with the highest id gives the best result with the background scanner, since that is most * likely the folder where we stopped scanning previously * - * @return string|bool the path of the folder or false when no folder matched + * @return string|false the path of the folder or false when no folder matched */ public function getIncomplete() { // not supported diff --git a/lib/private/Files/Cache/Wrapper/CacheWrapper.php b/lib/private/Files/Cache/Wrapper/CacheWrapper.php index e5300dc75f5..66ae83fd144 100644 --- a/lib/private/Files/Cache/Wrapper/CacheWrapper.php +++ b/lib/private/Files/Cache/Wrapper/CacheWrapper.php @@ -267,7 +267,7 @@ class CacheWrapper extends Cache { * use the one with the highest id gives the best result with the background scanner, since that is most * likely the folder where we stopped scanning previously * - * @return string|bool the path of the folder or false when no folder matched + * @return string|false the path of the folder or false when no folder matched */ public function getIncomplete() { return $this->getCache()->getIncomplete(); diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index c326eeb0b6c..685057a7860 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -28,7 +28,7 @@ */ namespace OC\Files\Config; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCA\Files_Sharing\SharedMount; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Config\ICachedMountFileInfo; @@ -42,7 +42,7 @@ use OCP\IUserManager; use Psr\Log\LoggerInterface; /** - * Cache mounts points per user in the cache so we can easilly look them up + * Cache mounts points per user in the cache so we can easily look them up */ class UserMountCache implements IUserMountCache { private IDBConnection $connection; diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index 6389544184f..47c893ebbf1 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -101,7 +101,11 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { $this->data = $data; $this->mount = $mount; $this->owner = $owner; - $this->rawSize = $this->data['size'] ?? 0; + if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] !== 0) { + $this->rawSize = $this->data['unencrypted_size']; + } else { + $this->rawSize = $this->data['size'] ?? 0; + } } public function offsetSet($offset, $value): void { @@ -208,7 +212,12 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { public function getSize($includeMounts = true) { if ($includeMounts) { $this->updateEntryfromSubMounts(); - return isset($this->data['size']) ? 0 + $this->data['size'] : 0; + + if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) { + return $this->data['unencrypted_size']; + } else { + return isset($this->data['size']) ? 0 + $this->data['size'] : 0; + } } else { return $this->rawSize; } @@ -386,14 +395,26 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { * @param string $entryPath full path of the child entry */ public function addSubEntry($data, $entryPath) { - $this->data['size'] += isset($data['size']) ? $data['size'] : 0; + if (!$data) { + return; + } + $hasUnencryptedSize = isset($data['unencrypted_size']) && $data['unencrypted_size'] > 0; + if ($hasUnencryptedSize) { + $subSize = $data['unencrypted_size']; + } else { + $subSize = $data['size'] ?: 0; + } + $this->data['size'] += $subSize; + if ($hasUnencryptedSize) { + $this->data['unencrypted_size'] += $subSize; + } if (isset($data['mtime'])) { $this->data['mtime'] = max($this->data['mtime'], $data['mtime']); } if (isset($data['etag'])) { // prefix the etag with the relative path of the subentry to propagate etag on mount moves $relativeEntryPath = substr($entryPath, strlen($this->getPath())); - // attach the permissions to propagate etag on permision changes of submounts + // attach the permissions to propagate etag on permission changes of submounts $permissions = isset($data['permissions']) ? $data['permissions'] : 0; $this->childEtags[] = $relativeEntryPath . '/' . $data['etag'] . $permissions; } diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index 20b44e2736a..9542666b03c 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -37,7 +37,7 @@ */ namespace OC\Files; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\Mount\MountPoint; use OC\User\NoUserException; use OCP\EventDispatcher\IEventDispatcher; diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php index 69285018d17..9ba0e504058 100644 --- a/lib/private/Files/Mount/Manager.php +++ b/lib/private/Files/Mount/Manager.php @@ -29,7 +29,7 @@ declare(strict_types=1); namespace OC\Files\Mount; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\Filesystem; use OC\Files\SetupManager; use OC\Files\SetupManagerFactory; diff --git a/lib/private/Files/Node/File.php b/lib/private/Files/Node/File.php index e125715e6a8..d8a6741dc6e 100644 --- a/lib/private/Files/Node/File.php +++ b/lib/private/Files/Node/File.php @@ -131,7 +131,6 @@ class File extends Node implements \OCP\Files\File { $this->view->unlink($this->path); $nonExisting = new NonExistingFile($this->root, $this->view, $this->path, $fileInfo); $this->sendHooks(['postDelete'], [$nonExisting]); - $this->exists = false; $this->fileInfo = null; } else { throw new NotPermittedException(); diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 9c15f0edf41..42562c99bcb 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -262,7 +262,7 @@ class Folder extends Node implements \OCP\Files\Folder { $searchHelper = \OC::$server->get(QuerySearchHelper::class); $resultsPerCache = $searchHelper->searchInCaches($query, $caches); - // loop trough all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all + // loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all $files = array_merge(...array_map(function (array $results, $relativeMountPoint) use ($mountByMountPoint) { $mount = $mountByMountPoint[$relativeMountPoint]; return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) { @@ -388,7 +388,6 @@ class Folder extends Node implements \OCP\Files\Folder { $this->view->rmdir($this->path); $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo); $this->sendHooks(['postDelete'], [$nonExisting]); - $this->exists = false; } else { throw new NotPermittedException('No delete permission for path'); } diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 6dd65a4291d..ca930c1002c 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -32,7 +32,7 @@ namespace OC\Files\Node; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\FileInfo; use OC\Files\Mount\Manager; use OC\Files\Mount\MountPoint; @@ -427,7 +427,7 @@ class Root extends Folder implements IRootFolder { $mountsContainingFile = $mountCache->getMountsForFileId($id, $user); } - // when a user has access trough the same storage trough multiple paths + // when a user has access through the same storage through multiple paths // (such as an external storage that is both mounted for a user and shared to the user) // the mount cache will only hold a single entry for the storage // this can lead to issues as the different ways the user has access to a storage can have different permissions diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index e00e3332b82..bdda1f8bee8 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -29,6 +29,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OC\Files\ObjectStore; use Aws\ClientResolver; @@ -122,15 +123,6 @@ trait S3ConnectionTrait { ) ); - // 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 */ - $certManager = \OC::$server->get(ICertificateManager::class); - $certPath = $certManager->getAbsoluteBundlePath(); - } else { - $certPath = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; - } - $options = [ 'version' => isset($this->params['version']) ? $this->params['version'] : 'latest', 'credentials' => $provider, @@ -140,7 +132,7 @@ trait S3ConnectionTrait { 'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()), 'csm' => false, 'use_arn_region' => false, - 'http' => ['verify' => $certPath], + 'http' => ['verify' => $this->getCertificateBundlePath()], 'use_aws_shared_config_files' => false, ]; if ($this->getProxy()) { @@ -154,7 +146,7 @@ trait S3ConnectionTrait { if (!$this->connection::isBucketDnsCompatible($this->bucket)) { $logger = \OC::$server->get(LoggerInterface::class); $logger->debug('Bucket "' . $this->bucket . '" This bucket name is not dns compatible, it may contain invalid characters.', - ['app' => 'objectstore']); + ['app' => 'objectstore']); } if ($this->params['verify_bucket_exists'] && !$this->connection->doesBucketExist($this->bucket)) { @@ -205,7 +197,7 @@ trait S3ConnectionTrait { /** * This function creates a credential provider based on user parameter file */ - protected function paramCredentialProvider() : callable { + protected function paramCredentialProvider(): callable { return function () { $key = empty($this->params['key']) ? null : $this->params['key']; $secret = empty($this->params['secret']) ? null : $this->params['secret']; @@ -220,4 +212,19 @@ trait S3ConnectionTrait { return new RejectedPromise(new CredentialsException($msg)); }; } + + protected function getCertificateBundlePath(): ?string { + 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 */ + $certManager = \OC::$server->get(ICertificateManager::class); + return $certManager->getAbsoluteBundlePath(); + } else { + return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; + } + } else { + return null; + } + } } diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 4e54a26e98a..9d692e01a23 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -43,6 +43,8 @@ trait S3ObjectTrait { */ abstract protected function getConnection(); + abstract protected function getCertificateBundlePath(): ?string; + /** * @param string $urn the unified resource name used to identify the object * @return resource stream with the read data @@ -67,8 +69,14 @@ trait S3ObjectTrait { 'http' => [ 'protocol_version' => $request->getProtocolVersion(), 'header' => $headers, - ], + ] ]; + $bundle = $this->getCertificateBundlePath(); + if ($bundle) { + $opts['ssl'] = [ + 'cafile' => $bundle + ]; + } if ($this->getProxy()) { $opts['http']['proxy'] = $this->getProxy(); diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 040ba6b898f..5782a5a72a6 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -82,6 +82,7 @@ class SetupManager { private IConfig $config; private bool $listeningForProviders; private array $fullSetupRequired = []; + private bool $setupBuiltinWrappersDone = false; public function __construct( IEventLogger $eventLogger, @@ -121,6 +122,15 @@ class SetupManager { } private function setupBuiltinWrappers() { + if ($this->setupBuiltinWrappersDone) { + return; + } + $this->setupBuiltinWrappersDone = true; + + // load all filesystem apps before, so no setup-hook gets lost + OC_App::loadApps(['filesystem']); + $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); + Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) { if ($storage->instanceOfStorage(Common::class)) { $storage->setMountOptions($mount->getOptions()); @@ -188,6 +198,8 @@ class SetupManager { } return $storage; }); + + Filesystem::logWarningWhenAddingStorageWrapper($prevLogging); } /** @@ -223,6 +235,9 @@ class SetupManager { return; } $this->setupUsers[] = $user->getUID(); + + $this->setupBuiltinWrappers(); + $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]); @@ -321,14 +336,8 @@ class SetupManager { $this->eventLogger->start('setup_root_fs', 'Setup root filesystem'); - // load all filesystem apps before, so no setup-hook gets lost - OC_App::loadApps(['filesystem']); - $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); - $this->setupBuiltinWrappers(); - Filesystem::logWarningWhenAddingStorageWrapper($prevLogging); - $rootMounts = $this->mountProviderCollection->getRootMounts(); foreach ($rootMounts as $rootMountProvider) { $this->mountManager->addMount($rootMountProvider); @@ -380,13 +389,9 @@ class SetupManager { return; } - // for the user's home folder, it's always the home mount - if (rtrim($path) === "/" . $user->getUID() . "/files") { - if ($includeChildren) { - $this->setupForUser($user); - } else { - $this->oneTimeUserSetup($user); - } + // for the user's home folder, and includes children we need everything always + if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) { + $this->setupForUser($user); return; } @@ -403,6 +408,10 @@ class SetupManager { return; } + if (!$this->isSetupStarted($user)) { + $this->oneTimeUserSetup($user); + } + $mounts = []; if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { $setupProviders[] = $cachedMount->getMountProvider(); @@ -554,10 +563,10 @@ class SetupManager { }); $genericEvents = [ - '\OCA\Circles::onCircleCreation', - '\OCA\Circles::onCircleDestruction', - '\OCA\Circles::onMemberNew', - '\OCA\Circles::onMemberLeaving', + 'OCA\Circles\Events\CreatingCircleEvent', + 'OCA\Circles\Events\DestroyingCircleEvent', + 'OCA\Circles\Events\AddingCircleMemberEvent', + 'OCA\Circles\Events\RemovingCircleMemberEvent', ]; foreach ($genericEvents as $genericEvent) { diff --git a/lib/private/Files/SimpleFS/NewSimpleFile.php b/lib/private/Files/SimpleFS/NewSimpleFile.php index 76fc69ebbe7..b2a183b7d29 100644 --- a/lib/private/Files/SimpleFS/NewSimpleFile.php +++ b/lib/private/Files/SimpleFS/NewSimpleFile.php @@ -34,15 +34,12 @@ use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; class NewSimpleFile implements ISimpleFile { - private $parentFolder; - private $name; - /** @var File|null */ - private $file = null; + private Folder $parentFolder; + private string $name; + private ?File $file = null; /** * File constructor. - * - * @param File $file */ public function __construct(Folder $parentFolder, string $name) { $this->parentFolder = $parentFolder; @@ -51,19 +48,15 @@ class NewSimpleFile implements ISimpleFile { /** * Get the name - * - * @return string */ - public function getName() { + public function getName(): string { return $this->name; } /** * Get the size in bytes - * - * @return int */ - public function getSize() { + public function getSize(): int { if ($this->file) { return $this->file->getSize(); } else { @@ -73,10 +66,8 @@ class NewSimpleFile implements ISimpleFile { /** * Get the ETag - * - * @return string */ - public function getETag() { + public function getETag(): string { if ($this->file) { return $this->file->getEtag(); } else { @@ -86,10 +77,8 @@ class NewSimpleFile implements ISimpleFile { /** * Get the last modification time - * - * @return int */ - public function getMTime() { + public function getMTime(): int { if ($this->file) { return $this->file->getMTime(); } else { @@ -100,11 +89,10 @@ class NewSimpleFile implements ISimpleFile { /** * Get the content * - * @return string * @throws NotFoundException * @throws NotPermittedException */ - public function getContent() { + public function getContent(): string { if ($this->file) { $result = $this->file->getContent(); @@ -125,7 +113,7 @@ class NewSimpleFile implements ISimpleFile { * @throws NotPermittedException * @throws NotFoundException */ - public function putContent($data) { + public function putContent($data): void { try { if ($this->file) { $this->file->putContent($data); @@ -139,7 +127,7 @@ class NewSimpleFile implements ISimpleFile { /** * Sometimes there are some issues with the AppData. Most of them are from - * user error. But we should handle them gracefull anyway. + * user error. But we should handle them gracefully anyway. * * If for some reason the current file can't be found. We remove it. * Then traverse up and check all folders if they exists. This so that the @@ -147,7 +135,7 @@ class NewSimpleFile implements ISimpleFile { * * @throws NotFoundException */ - private function checkFile() { + private function checkFile(): void { $cur = $this->file; while ($cur->stat() === false) { @@ -171,7 +159,7 @@ class NewSimpleFile implements ISimpleFile { * * @throws NotPermittedException */ - public function delete() { + public function delete(): void { if ($this->file) { $this->file->delete(); } @@ -182,7 +170,7 @@ class NewSimpleFile implements ISimpleFile { * * @return string */ - public function getMimeType() { + public function getMimeType(): string { if ($this->file) { return $this->file->getMimeType(); } else { diff --git a/lib/private/Files/SimpleFS/SimpleFile.php b/lib/private/Files/SimpleFS/SimpleFile.php index 21a2fd92dcb..a2571ac50e8 100644 --- a/lib/private/Files/SimpleFS/SimpleFile.php +++ b/lib/private/Files/SimpleFS/SimpleFile.php @@ -30,52 +30,37 @@ use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; class SimpleFile implements ISimpleFile { + private File $file; - /** @var File $file */ - private $file; - - /** - * File constructor. - * - * @param File $file - */ public function __construct(File $file) { $this->file = $file; } /** * Get the name - * - * @return string */ - public function getName() { + public function getName(): string { return $this->file->getName(); } /** * Get the size in bytes - * - * @return int */ - public function getSize() { + public function getSize(): int { return $this->file->getSize(); } /** * Get the ETag - * - * @return string */ - public function getETag() { + public function getETag(): string { return $this->file->getEtag(); } /** * Get the last modification time - * - * @return int */ - public function getMTime() { + public function getMTime(): int { return $this->file->getMTime(); } @@ -84,9 +69,8 @@ class SimpleFile implements ISimpleFile { * * @throws NotPermittedException * @throws NotFoundException - * @return string */ - public function getContent() { + public function getContent(): string { $result = $this->file->getContent(); if ($result === false) { @@ -103,9 +87,9 @@ class SimpleFile implements ISimpleFile { * @throws NotPermittedException * @throws NotFoundException */ - public function putContent($data) { + public function putContent($data): void { try { - return $this->file->putContent($data); + $this->file->putContent($data); } catch (NotFoundException $e) { $this->checkFile(); } @@ -113,7 +97,7 @@ class SimpleFile implements ISimpleFile { /** * Sometimes there are some issues with the AppData. Most of them are from - * user error. But we should handle them gracefull anyway. + * user error. But we should handle them gracefully anyway. * * If for some reason the current file can't be found. We remove it. * Then traverse up and check all folders if they exists. This so that the @@ -121,7 +105,7 @@ class SimpleFile implements ISimpleFile { * * @throws NotFoundException */ - private function checkFile() { + private function checkFile(): void { $cur = $this->file; while ($cur->stat() === false) { @@ -145,16 +129,14 @@ class SimpleFile implements ISimpleFile { * * @throws NotPermittedException */ - public function delete() { + public function delete(): void { $this->file->delete(); } /** * Get the MimeType - * - * @return string */ - public function getMimeType() { + public function getMimeType(): string { return $this->file->getMimeType(); } @@ -179,7 +161,7 @@ class SimpleFile implements ISimpleFile { /** * Open the file as stream for writing, resulting resource can be operated as stream like the result from php's own fopen * - * @return resource + * @return resource|false * @throws \OCP\Files\NotPermittedException * @since 14.0.0 */ diff --git a/lib/private/Files/SimpleFS/SimpleFolder.php b/lib/private/Files/SimpleFS/SimpleFolder.php index cd2a712019e..263c25a8873 100644 --- a/lib/private/Files/SimpleFS/SimpleFolder.php +++ b/lib/private/Files/SimpleFS/SimpleFolder.php @@ -29,6 +29,7 @@ use OCP\Files\Folder; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Files\SimpleFS\ISimpleFile; class SimpleFolder implements ISimpleFolder { @@ -44,11 +45,11 @@ class SimpleFolder implements ISimpleFolder { $this->folder = $folder; } - public function getName() { + public function getName(): string { return $this->folder->getName(); } - public function getDirectoryListing() { + public function getDirectoryListing(): array { $listing = $this->folder->getDirectoryListing(); $fileListing = array_map(function (Node $file) { @@ -63,15 +64,15 @@ class SimpleFolder implements ISimpleFolder { return array_values($fileListing); } - public function delete() { + public function delete(): void { $this->folder->delete(); } - public function fileExists($name) { + public function fileExists(string $name): bool { return $this->folder->nodeExists($name); } - public function getFile($name) { + public function getFile(string $name): ISimpleFile { $file = $this->folder->get($name); if (!($file instanceof File)) { @@ -81,7 +82,7 @@ class SimpleFolder implements ISimpleFolder { return new SimpleFile($file); } - public function newFile($name, $content = null) { + public function newFile(string $name, $content = null): ISimpleFile { if ($content === null) { // delay creating the file until it's written to return new NewSimpleFile($this->folder, $name); diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 3c970ee75f5..a7bc44e10e2 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -228,6 +228,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { while ($file = readdir($dir)) { if (!Filesystem::isIgnoredDir($file)) { if (!$this->copy($path1 . '/' . $file, $path2 . '/' . $file)) { + closedir($dir); return false; } } diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index ee8a8c7d161..4996572a40e 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -15,6 +15,7 @@ * @author Jörn Friedrich Dreyer <jfd@butonic.de> * @author Klaas Freitag <freitag@owncloud.com> * @author Lukas Reschke <lukas@statuscode.ch> + * @author Martin Brugnara <martin@0x6d62.eu> * @author Michael Gapczynski <GapczynskiM@gmail.com> * @author Morris Jobke <hey@morrisjobke.de> * @author Robin Appelman <robin@icewind.nl> @@ -66,6 +67,8 @@ class Local extends \OC\Files\Storage\Common { private IMimeTypeDetector $mimeTypeDetector; + private $defUMask; + public function __construct($arguments) { if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) { throw new \InvalidArgumentException('No data directory set for local storage'); @@ -84,6 +87,7 @@ class Local extends \OC\Files\Storage\Common { $this->dataDirLength = strlen($this->realDataDir); $this->config = \OC::$server->get(IConfig::class); $this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class); + $this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022); } public function __destruct() { @@ -95,7 +99,7 @@ class Local extends \OC\Files\Storage\Common { public function mkdir($path) { $sourcePath = $this->getSourcePath($path); - $oldMask = umask(022); + $oldMask = umask($this->defUMask); $result = @mkdir($sourcePath, 0777, true); umask($oldMask); return $result; @@ -273,7 +277,7 @@ class Local extends \OC\Files\Storage\Common { if ($this->file_exists($path) and !$this->isUpdatable($path)) { return false; } - $oldMask = umask(022); + $oldMask = umask($this->defUMask); if (!is_null($mtime)) { $result = @touch($this->getSourcePath($path), $mtime); } else { @@ -292,7 +296,7 @@ class Local extends \OC\Files\Storage\Common { } public function file_put_contents($path, $data) { - $oldMask = umask(022); + $oldMask = umask($this->defUMask); $result = file_put_contents($this->getSourcePath($path), $data); umask($oldMask); return $result; @@ -365,7 +369,7 @@ class Local extends \OC\Files\Storage\Common { if ($this->is_dir($path1)) { return parent::copy($path1, $path2); } else { - $oldMask = umask(022); + $oldMask = umask($this->defUMask); $result = copy($this->getSourcePath($path1), $this->getSourcePath($path2)); umask($oldMask); return $result; @@ -373,7 +377,7 @@ class Local extends \OC\Files\Storage\Common { } public function fopen($path, $mode) { - $oldMask = umask(022); + $oldMask = umask($this->defUMask); $result = fopen($this->getSourcePath($path), $mode); umask($oldMask); return $result; diff --git a/lib/private/Files/Storage/Wrapper/Encoding.php b/lib/private/Files/Storage/Wrapper/Encoding.php index d6201dc8877..ac9cc248ce6 100644 --- a/lib/private/Files/Storage/Wrapper/Encoding.php +++ b/lib/private/Files/Storage/Wrapper/Encoding.php @@ -28,7 +28,7 @@ */ namespace OC\Files\Storage\Wrapper; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\Filesystem; use OCP\Files\Storage\IStorage; use OCP\ICache; diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 4cfe932cc9f..d5bf929101f 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -33,6 +33,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ + namespace OC\Files\Storage\Wrapper; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; @@ -41,6 +42,7 @@ use OC\Encryption\Util; use OC\Files\Cache\CacheEntry; use OC\Files\Filesystem; use OC\Files\Mount\Manager; +use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\LocalTempFileTrait; use OC\Memcache\ArrayCache; use OCP\Encryption\Exceptions\GenericEncryptionException; @@ -139,28 +141,36 @@ class Encryption extends Wrapper { $size = $this->unencryptedSize[$fullPath]; // update file cache if ($info instanceof ICacheEntry) { - $info = $info->getData(); $info['encrypted'] = $info['encryptedVersion']; } else { if (!is_array($info)) { $info = []; } $info['encrypted'] = true; + $info = new CacheEntry($info); } - $info['size'] = $size; - $this->getCache()->put($path, $info); + if ($size !== $info->getUnencryptedSize()) { + $this->getCache()->update($info->getId(), [ + 'unencrypted_size' => $size + ]); + } return $size; } if (isset($info['fileid']) && $info['encrypted']) { - return $this->verifyUnencryptedSize($path, $info['size']); + return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize()); } return $this->storage->filesize($path); } + /** + * @param string $path + * @param array $data + * @return array + */ private function modifyMetaData(string $path, array $data): array { $fullPath = $this->getFullPath($path); $info = $this->getCache()->get($path); @@ -170,7 +180,7 @@ class Encryption extends Wrapper { $data['size'] = $this->unencryptedSize[$fullPath]; } else { if (isset($info['fileid']) && $info['encrypted']) { - $data['size'] = $this->verifyUnencryptedSize($path, $info['size']); + $data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize()); $data['encrypted'] = true; } } @@ -478,7 +488,7 @@ class Encryption extends Wrapper { * * @return int unencrypted size */ - protected function verifyUnencryptedSize($path, $unencryptedSize) { + protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int { $size = $this->storage->filesize($path); $result = $unencryptedSize; @@ -510,7 +520,7 @@ class Encryption extends Wrapper { * * @return int calculated unencrypted size */ - protected function fixUnencryptedSize($path, $size, $unencryptedSize) { + protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int { $headerSize = $this->getHeaderSize($path); $header = $this->getHeader($path); $encryptionModule = $this->getEncryptionModule($path); @@ -581,7 +591,9 @@ class Encryption extends Wrapper { $cache = $this->storage->getCache(); if ($cache) { $entry = $cache->get($path); - $cache->update($entry['fileid'], ['size' => $newUnencryptedSize]); + $cache->update($entry['fileid'], [ + 'unencrypted_size' => $newUnencryptedSize + ]); } return $newUnencryptedSize; @@ -621,7 +633,12 @@ class Encryption extends Wrapper { * @param bool $preserveMtime * @return bool */ - public function moveFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) { + public function moveFromStorage( + Storage\IStorage $sourceStorage, + $sourceInternalPath, + $targetInternalPath, + $preserveMtime = true + ) { if ($sourceStorage === $this) { return $this->rename($sourceInternalPath, $targetInternalPath); } @@ -656,7 +673,13 @@ class Encryption extends Wrapper { * @param bool $isRename * @return bool */ - public function copyFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) { + public function copyFromStorage( + Storage\IStorage $sourceStorage, + $sourceInternalPath, + $targetInternalPath, + $preserveMtime = false, + $isRename = false + ) { // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed: // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage @@ -676,7 +699,13 @@ class Encryption extends Wrapper { * @param bool $isRename * @param bool $keepEncryptionVersion */ - private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, $keepEncryptionVersion) { + private function updateEncryptedVersion( + Storage\IStorage $sourceStorage, + $sourceInternalPath, + $targetInternalPath, + $isRename, + $keepEncryptionVersion + ) { $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath); $cacheInformation = [ 'encrypted' => $isEncrypted, @@ -725,7 +754,13 @@ class Encryption extends Wrapper { * @return bool * @throws \Exception */ - private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) { + private function copyBetweenStorage( + Storage\IStorage $sourceStorage, + $sourceInternalPath, + $targetInternalPath, + $preserveMtime, + $isRename + ) { // for versions we have nothing to do, because versions should always use the // key from the original file. Just create a 1:1 copy and done @@ -743,7 +778,7 @@ class Encryption extends Wrapper { if (isset($info['encrypted']) && $info['encrypted'] === true) { $this->updateUnencryptedSize( $this->getFullPath($targetInternalPath), - $info['size'] + $info->getUnencryptedSize() ); } $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true); @@ -808,13 +843,6 @@ class Encryption extends Wrapper { return (bool)$result; } - /** - * get the path to a local version of the file. - * The local version of the file can be temporary and doesn't have to be persistent across requests - * - * @param string $path - * @return string - */ public function getLocalFile($path) { if ($this->encryptionManager->isEnabled()) { $cachedFile = $this->getCachedFile($path); @@ -825,11 +853,6 @@ class Encryption extends Wrapper { return $this->storage->getLocalFile($path); } - /** - * Returns the wrapped storage's value for isLocal() - * - * @return bool wrapped storage's isLocal() value - */ public function isLocal() { if ($this->encryptionManager->isEnabled()) { return false; @@ -837,15 +860,11 @@ class Encryption extends Wrapper { return $this->storage->isLocal(); } - /** - * see https://www.php.net/manual/en/function.stat.php - * only the following keys are required in the result: size and mtime - * - * @param string $path - * @return array - */ public function stat($path) { $stat = $this->storage->stat($path); + if (!$stat) { + return false; + } $fileSize = $this->filesize($path); $stat['size'] = $fileSize; $stat[7] = $fileSize; @@ -853,14 +872,6 @@ class Encryption extends Wrapper { return $stat; } - /** - * see https://www.php.net/manual/en/function.hash.php - * - * @param string $type - * @param string $path - * @param bool $raw - * @return string - */ public function hash($type, $path, $raw = false) { $fh = $this->fopen($path, 'rb'); $ctx = hash_init($type); @@ -1068,6 +1079,13 @@ class Encryption extends Wrapper { [$count, $result] = \OC_Helper::streamCopy($stream, $target); fclose($stream); fclose($target); + + // object store, stores the size after write and doesn't update this during scan + // manually store the unencrypted size + if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) { + $this->getCache()->put($path, ['unencrypted_size' => $count]); + } + return $count; } } diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index af797c7720d..820a681bd07 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -24,6 +24,7 @@ namespace OC\Files\Stream; use Icewind\Streams\File; +use Icewind\Streams\Wrapper; /** * A stream wrapper that uses http range requests to provide a seekable stream for http reading @@ -92,6 +93,18 @@ class SeekableHttpStream implements File { } $responseHead = stream_get_meta_data($this->current)['wrapper_data']; + + while ($responseHead instanceof Wrapper) { + $wrapperOptions = stream_context_get_options($responseHead->context); + foreach ($wrapperOptions as $options) { + if (isset($options['source']) && is_resource($options['source'])) { + $responseHead = stream_get_meta_data($options['source'])['wrapper_data']; + continue 2; + } + } + throw new \Exception("Failed to get source stream from stream wrapper of " . get_class($responseHead)); + } + $rangeHeaders = array_values(array_filter($responseHead, function ($v) { return preg_match('#^content-range:#i', $v) === 1; })); diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 2b6732e2ba0..d12869fbdaa 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1164,7 +1164,7 @@ class View { try { $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE); } catch (LockedException $e) { - // release the shared lock we acquired before quiting + // release the shared lock we acquired before quitting $this->unlockFile($path, ILockingProvider::LOCK_SHARED); throw $e; } @@ -1725,7 +1725,7 @@ class View { /** * Get the path of a file by id, relative to the view * - * Note that the resulting path is not guarantied to be unique for the id, multiple paths can point to the same file + * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file * * @param int $id * @param int|null $storageId diff --git a/lib/private/HintException.php b/lib/private/HintException.php index 735832266cf..20f7142d1c0 100644 --- a/lib/private/HintException.php +++ b/lib/private/HintException.php @@ -31,7 +31,7 @@ namespace OC; * An Exception class with the intention to be presented to the end user * * @package OC - * @depreacted 23.0.0 Use \OCP\HintException + * @deprecated 23.0.0 Use \OCP\HintException */ class HintException extends \OCP\HintException { } diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php index 3ba85a2dd9f..4bf7fd02400 100644 --- a/lib/private/Http/Client/Client.php +++ b/lib/private/Http/Client/Client.php @@ -128,7 +128,7 @@ class Client implements IClient { } /** - * Returns a null or an associative array specifiying the proxy URI for + * Returns a null or an associative array specifying the proxy URI for * 'http' and 'https' schemes, in addition to a 'no' key value pair * providing a list of host names that should not be proxied to. * diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index 2789b1b5935..f4fea503ab9 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -27,6 +27,7 @@ namespace OC\Http\Client; use OCP\Http\Client\LocalServerException; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\IpUtils; class LocalAddressChecker { private LoggerInterface $logger; @@ -36,7 +37,16 @@ class LocalAddressChecker { } public function ThrowIfLocalIp(string $ip) : void { - if ((bool)filter_var($ip, FILTER_VALIDATE_IP) && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + $localRanges = [ + '100.64.0.0/10', // See RFC 6598 + '192.0.0.0/24', // See RFC 6890 + ]; + if ( + (bool)filter_var($ip, FILTER_VALIDATE_IP) && + ( + !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || + IpUtils::checkIp($ip, $localRanges) + )) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } @@ -46,7 +56,9 @@ class LocalAddressChecker { $delimiter = strrpos($ip, ':'); // Get last colon $ipv4Address = substr($ip, $delimiter + 1); - if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + if ( + !filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || + IpUtils::checkIp($ip, $localRanges)) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } @@ -66,8 +78,10 @@ class LocalAddressChecker { $host = substr($host, 1, -1); } - // Disallow localhost and local network - if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') { + // Disallow local network top-level domains from RFC 6762 + $localTopLevelDomains = ['local','localhost','intranet','internal','private','corp','home','lan']; + $topLevelDomain = substr((strrchr($host, '.') ?: ''), 1); + if (in_array($topLevelDomain, $localTopLevelDomains)) { $this->logger->warning("Host $host was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } diff --git a/lib/private/IntegrityCheck/Checker.php b/lib/private/IntegrityCheck/Checker.php index 273eba35446..ba555cff438 100644 --- a/lib/private/IntegrityCheck/Checker.php +++ b/lib/private/IntegrityCheck/Checker.php @@ -201,7 +201,8 @@ class Checker { if ($filename === $this->environmentHelper->getServerRoot() . '/core/js/mimetypelist.js') { $oldMimetypeList = new GenerateMimetypeFileBuilder(); $newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->getAllAliases()); - if ($newFile === file_get_contents($filename)) { + $oldFile = $this->fileAccessHelper->file_get_contents($filename); + if ($newFile === $oldFile) { $hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->getOnlyDefaultAliases())); continue; } diff --git a/lib/private/L10N/L10N.php b/lib/private/L10N/L10N.php index 09c0f1cdb35..82ef3350b1f 100644 --- a/lib/private/L10N/L10N.php +++ b/lib/private/L10N/L10N.php @@ -122,7 +122,7 @@ class L10N implements IL10N { * */ public function n(string $text_singular, string $text_plural, int $count, array $parameters = []): string { - $identifier = "_${text_singular}_::_${text_plural}_"; + $identifier = "_{$text_singular}_::_{$text_plural}_"; if (isset($this->translations[$identifier])) { return (string) new L10NString($this, $identifier, $parameters, $count); } diff --git a/lib/private/Log.php b/lib/private/Log.php index 0415967f0f0..4ab647bc6c1 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -15,6 +15,7 @@ declare(strict_types=1); * @author Olivier Paroz <github@oparoz.com> * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> + * @author Thomas Citharel <nextcloud@tcit.fr> * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Victor Dubiniuk <dubiniuk@owncloud.com> * @@ -35,8 +36,11 @@ declare(strict_types=1); */ namespace OC; +use Exception; use Nextcloud\LogNormalizer\Normalizer; +use OC\AppFramework\Bootstrap\Coordinator; use OCP\Log\IDataLogger; +use Throwable; use function array_merge; use OC\Log\ExceptionSerializer; use OCP\ILogger; @@ -207,11 +211,11 @@ class Log implements ILogger, IDataLogger { array_walk($context, [$this->normalizer, 'format']); $app = $context['app'] ?? 'no app in context'; - $message = $this->interpolateMessage($context, $message); + $entry = $this->interpolateMessage($context, $message); try { if ($level >= $minLevel) { - $this->writeLog($app, $message, $level); + $this->writeLog($app, $entry, $level); if ($this->crashReporters !== null) { $messageContext = array_merge( @@ -220,14 +224,14 @@ class Log implements ILogger, IDataLogger { 'level' => $level ] ); - $this->crashReporters->delegateMessage($message, $messageContext); + $this->crashReporters->delegateMessage($entry['message'], $messageContext); } } else { if ($this->crashReporters !== null) { - $this->crashReporters->delegateBreadcrumb($message, 'log', $context); + $this->crashReporters->delegateBreadcrumb($entry['message'], 'log', $context); } } - } catch (\Throwable $e) { + } catch (Throwable $e) { // make sure we dont hard crash if logging fails } } @@ -299,24 +303,27 @@ class Log implements ILogger, IDataLogger { /** * Logs an exception very detailed * - * @param \Exception|\Throwable $exception + * @param Exception|Throwable $exception * @param array $context * @return void * @since 8.2.0 */ - public function logException(\Throwable $exception, array $context = []) { + public function logException(Throwable $exception, array $context = []) { $app = $context['app'] ?? 'no app in context'; $level = $context['level'] ?? ILogger::ERROR; // if an error is raised before the autoloader is properly setup, we can't serialize exceptions try { - $serializer = new ExceptionSerializer($this->config); - } catch (\Throwable $e) { + $serializer = $this->getSerializer(); + } catch (Throwable $e) { $this->error("Failed to load ExceptionSerializer serializer while trying to log " . $exception->getMessage()); return; } - $data = $serializer->serializeException($exception); - $data['CustomMessage'] = $this->interpolateMessage($context, $context['message'] ?? '--'); + $data = $context; + unset($data['app']); + unset($data['level']); + $data = array_merge($serializer->serializeException($exception), $data); + $data = $this->interpolateMessage($data, $context['message'] ?? '--', 'CustomMessage'); $minLevel = $this->getLogLevel($context); @@ -334,7 +341,7 @@ class Log implements ILogger, IDataLogger { if (!is_null($this->crashReporters)) { $this->crashReporters->delegateReport($exception, $context); } - } catch (\Throwable $e) { + } catch (Throwable $e) { // make sure we dont hard crash if logging fails } } @@ -357,7 +364,7 @@ class Log implements ILogger, IDataLogger { } $context['level'] = $level; - } catch (\Throwable $e) { + } catch (Throwable $e) { // make sure we dont hard crash if logging fails } } @@ -381,16 +388,42 @@ class Log implements ILogger, IDataLogger { /** * Interpolate $message as defined in PSR-3 * - * @param array $context - * @param string $message - * - * @return string + * Returns an array containing the context without the interpolated + * parameters placeholders and the message as the 'message' - or + * user-defined - key. */ - private function interpolateMessage(array $context, string $message): string { + private function interpolateMessage(array $context, string $message, string $messageKey = 'message'): array { $replace = []; + $usedContextKeys = []; foreach ($context as $key => $val) { - $replace['{' . $key . '}'] = $val; + $fullKey = '{' . $key . '}'; + $replace[$fullKey] = $val; + if (strpos($message, $fullKey) !== false) { + $usedContextKeys[$key] = true; + } + } + return array_merge(array_diff_key($context, $usedContextKeys), [$messageKey => strtr($message, $replace)]); + } + + /** + * @throws Throwable + */ + protected function getSerializer(): ExceptionSerializer { + $serializer = new ExceptionSerializer($this->config); + try { + /** @var Coordinator $coordinator */ + $coordinator = \OCP\Server::get(Coordinator::class); + foreach ($coordinator->getRegistrationContext()->getSensitiveMethods() as $registration) { + $serializer->enlistSensitiveMethods($registration->getName(), $registration->getValue()); + } + // For not every app might be initialized at this time, we cannot assume that the return value + // of getSensitiveMethods() is complete. Running delegates in Coordinator::registerApps() is + // not possible due to dependencies on the one hand. On the other it would work only with + // adding public methods to the PsrLoggerAdapter and this class. + // Thus, serializer cannot be a property. + } catch (Throwable $t) { + // ignore app-defined sensitive methods in this case - they weren't loaded anyway } - return strtr($message, $replace); + return $serializer; } } diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php index dab134b26a4..aaf6a39235e 100644 --- a/lib/private/Log/ExceptionSerializer.php +++ b/lib/private/Log/ExceptionSerializer.php @@ -42,6 +42,8 @@ use OCA\Encryption\Session; use OCP\HintException; class ExceptionSerializer { + public const SENSITIVE_VALUE_PLACEHOLDER = '*** sensitive parameters replaced ***'; + public const methodsWithSensitiveParameters = [ // Session/User 'completeLogin', @@ -107,7 +109,7 @@ class ExceptionSerializer { $this->systemConfig = $systemConfig; } - public const methodsWithSensitiveParametersByClass = [ + protected array $methodsWithSensitiveParametersByClass = [ SetupController::class => [ 'run', 'display', @@ -180,7 +182,7 @@ class ExceptionSerializer { if (isset($traceLine['args'])) { $sensitiveValues = array_merge($sensitiveValues, $traceLine['args']); } - $traceLine['args'] = ['*** sensitive parameters replaced ***']; + $traceLine['args'] = [self::SENSITIVE_VALUE_PLACEHOLDER]; return $traceLine; } @@ -188,8 +190,8 @@ class ExceptionSerializer { $sensitiveValues = []; $trace = array_map(function (array $traceLine) use (&$sensitiveValues) { $className = $traceLine['class'] ?? ''; - if ($className && isset(self::methodsWithSensitiveParametersByClass[$className]) - && in_array($traceLine['function'], self::methodsWithSensitiveParametersByClass[$className], true)) { + if ($className && isset($this->methodsWithSensitiveParametersByClass[$className]) + && in_array($traceLine['function'], $this->methodsWithSensitiveParametersByClass[$className], true)) { return $this->editTrace($sensitiveValues, $traceLine); } foreach (self::methodsWithSensitiveParameters as $sensitiveMethod) { @@ -208,14 +210,16 @@ class ExceptionSerializer { } private function removeValuesFromArgs($args, $values) { - foreach ($args as &$arg) { + $workArgs = []; + foreach ($args as $arg) { if (in_array($arg, $values, true)) { - $arg = '*** sensitive parameter replaced ***'; + $arg = self::SENSITIVE_VALUE_PLACEHOLDER; } elseif (is_array($arg)) { $arg = $this->removeValuesFromArgs($arg, $values); } + $workArgs[] = $arg; } - return $args; + return $workArgs; } private function encodeTrace($trace) { @@ -285,4 +289,11 @@ class ExceptionSerializer { return $data; } + + public function enlistSensitiveMethods(string $class, array $methods): void { + if (!isset($this->methodsWithSensitiveParametersByClass[$class])) { + $this->methodsWithSensitiveParametersByClass[$class] = []; + } + $this->methodsWithSensitiveParametersByClass[$class] = array_merge($this->methodsWithSensitiveParametersByClass[$class], $methods); + } } diff --git a/lib/private/Log/LogDetails.php b/lib/private/Log/LogDetails.php index 3353ea3f4cc..b3544572708 100644 --- a/lib/private/Log/LogDetails.php +++ b/lib/private/Log/LogDetails.php @@ -5,6 +5,7 @@ * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author Julius Härtl <jus@bitgrid.net> + * @author Thomas Citharel <nextcloud@tcit.fr> * * @license GNU AGPL version 3 or any later version * @@ -90,8 +91,9 @@ abstract class LogDetails { $entry['exception'] = $message; $entry['message'] = $message['CustomMessage'] !== '--' ? $message['CustomMessage'] : $message['Message']; } else { - $entry['data'] = $message; $entry['message'] = $message['message'] ?? '(no message provided)'; + unset($message['message']); + $entry['data'] = $message; } } diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 2c091deb2a1..7ec2b46bad3 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -497,7 +497,7 @@ EOF; */ /** @var string $label */ $label = ($plainMetaInfo !== false)? $plainMetaInfo : ''; - $this->plainBody .= sprintf("%${plainIndent}s %s\n", + $this->plainBody .= sprintf("%{$plainIndent}s %s\n", $label, str_replace("\n", "\n" . str_repeat(' ', $plainIndent + 1), $plainText)); } diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php index 2f3480498be..991d1b202ec 100644 --- a/lib/private/Mail/Mailer.php +++ b/lib/private/Mail/Mailer.php @@ -196,6 +196,9 @@ class Mailer implements IMailer { // Debugging logging $logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject()); + if (!empty($failedRecipients)) { + $logMessage .= sprintf(' (failed for "%s")', print_r($failedRecipients, true)); + } $this->logger->debug($logMessage, ['app' => 'core']); if ($debugMode && isset($mailLogger)) { $this->logger->debug($mailLogger->dump(), ['app' => 'core']); diff --git a/lib/private/Memcache/Memcached.php b/lib/private/Memcache/Memcached.php index db4aa7ba9cc..7d512d4d1ae 100644 --- a/lib/private/Memcache/Memcached.php +++ b/lib/private/Memcache/Memcached.php @@ -63,7 +63,7 @@ class Memcached extends Cache implements IMemcache { \Memcached::OPT_LIBKETAMA_COMPATIBLE => true, // Enable Binary Protocol - //\Memcached::OPT_BINARY_PROTOCOL => true, + \Memcached::OPT_BINARY_PROTOCOL => true, ]; /** * By default enable igbinary serializer if available @@ -119,10 +119,7 @@ class Memcached extends Cache implements IMemcache { } else { $result = self::$cache->set($this->getNameSpace() . $key, $value); } - if ($result !== true) { - $this->verifyReturnCode(); - } - return $result; + return $result || $this->isSuccess(); } public function hasKey($key) { @@ -132,10 +129,7 @@ class Memcached extends Cache implements IMemcache { public function remove($key) { $result = self::$cache->delete($this->getNameSpace() . $key); - if (self::$cache->getResultCode() !== \Memcached::RES_NOTFOUND) { - $this->verifyReturnCode(); - } - return $result; + return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND; } public function clear($prefix = '') { @@ -151,14 +145,10 @@ class Memcached extends Cache implements IMemcache { * @param mixed $value * @param int $ttl Time To Live in seconds. Defaults to 60*60*24 * @return bool - * @throws \Exception */ public function add($key, $value, $ttl = 0) { $result = self::$cache->add($this->getPrefix() . $key, $value, $ttl); - if (self::$cache->getResultCode() !== \Memcached::RES_NOTSTORED) { - $this->verifyReturnCode(); - } - return $result; + return $result || $this->isSuccess(); } /** @@ -200,15 +190,7 @@ class Memcached extends Cache implements IMemcache { return extension_loaded('memcached'); } - /** - * @throws \Exception - */ - private function verifyReturnCode() { - $code = self::$cache->getResultCode(); - if ($code === \Memcached::RES_SUCCESS) { - return; - } - $message = self::$cache->getResultMessage(); - throw new \Exception("Error $code interacting with memcached : $message"); + private function isSuccess(): bool { + return self::$cache->getResultCode() === \Memcached::RES_SUCCESS; } } diff --git a/lib/private/MemoryInfo.php b/lib/private/MemoryInfo.php index 074e9f915fe..ed6617d879d 100644 --- a/lib/private/MemoryInfo.php +++ b/lib/private/MemoryInfo.php @@ -68,7 +68,7 @@ class MemoryInfo { $last = strtolower(substr($memoryLimit, -1)); $memoryLimit = (int)substr($memoryLimit, 0, -1); - // intended fall trough + // intended fall through switch ($last) { case 'g': $memoryLimit *= 1024; diff --git a/lib/private/Metadata/IMetadataManager.php b/lib/private/Metadata/IMetadataManager.php index d2d37f15c25..fa0bcc22801 100644 --- a/lib/private/Metadata/IMetadataManager.php +++ b/lib/private/Metadata/IMetadataManager.php @@ -29,7 +29,7 @@ interface IMetadataManager { public function fetchMetadataFor(string $group, array $fileIds): array; /** - * Get the capabilites as an array of mimetype regex to the type provided + * Get the capabilities as an array of mimetype regex to the type provided */ public function getCapabilities(): array; } diff --git a/lib/private/OCS/DiscoveryService.php b/lib/private/OCS/DiscoveryService.php index 1d10bbac870..7ab876811e7 100644 --- a/lib/private/OCS/DiscoveryService.php +++ b/lib/private/OCS/DiscoveryService.php @@ -62,7 +62,7 @@ class DiscoveryService implements IDiscoveryService { * * @param string $remote * @param string $service the service you want to discover - * @param bool $skipCache We won't check if the data is in the cache. This is usefull if a background job is updating the status + * @param bool $skipCache We won't check if the data is in the cache. This is useful if a background job is updating the status * @return array */ public function discover(string $remote, string $service, bool $skipCache = false): array { diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php index edb51458c66..f2eacd1ef25 100644 --- a/lib/private/Profile/ProfileManager.php +++ b/lib/private/Profile/ProfileManager.php @@ -348,13 +348,13 @@ class ProfileManager { * Return the default profile config */ private function getDefaultProfileConfig(IUser $targetUser, ?IUser $visitingUser): array { - // Contruct the default config for actions + // Construct the default config for actions $actionsConfig = []; foreach ($this->getActions($targetUser, $visitingUser) as $action) { $actionsConfig[$action->getId()] = ['visibility' => ProfileConfig::DEFAULT_VISIBILITY]; } - // Contruct the default config for account properties + // Construct the default config for account properties $propertiesConfig = []; foreach (ProfileConfig::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) { $propertiesConfig[$property] = ['visibility' => $visibility]; diff --git a/lib/private/Repair/RemoveLinkShares.php b/lib/private/Repair/RemoveLinkShares.php index 1b0270e928d..e1ce78cdbf3 100644 --- a/lib/private/Repair/RemoveLinkShares.php +++ b/lib/private/Repair/RemoveLinkShares.php @@ -217,7 +217,7 @@ class RemoveLinkShares implements IRepairStep { $output->finishProgress(); $shareResult->closeCursor(); - // Notifiy all admins + // Notify all admins $adminGroup = $this->groupManager->get('admin'); $adminUsers = $adminGroup->getUsers(); foreach ($adminUsers as $user) { diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php index 61512627258..5b216331dc7 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -211,6 +211,15 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } + private function introduceOnlyofficeFormType() { + $updatedMimetypes = [ + "oform" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform", + "docxf" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf", + ]; + + return $this->updateMimetypes($updatedMimetypes); + } + /** * Fix mime types @@ -260,5 +269,9 @@ class RepairMimeTypes implements IRepairStep { if (version_compare($ocVersionFromBeforeUpdate, '23.0.0.2', '<') && $this->introduceFlatOpenDocumentType()) { $out->info('Fixed Flat OpenDocument mime types'); } + + if (version_compare($ocVersionFromBeforeUpdate, '25.0.0.2', '<') && $this->introduceOnlyofficeFormType()) { + $out->info('Fixed ONLYOFFICE Forms OpenXML mime types'); + } } } diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index b957173cacc..7e1acd49800 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -409,7 +409,7 @@ class Router implements IRouter { * register the routes for the app. The application class will be chosen by * camelcasing the appname, e.g.: my_app will be turned into * \OCA\MyApp\AppInfo\Application. If that class does not exist, a default - * App will be intialized. This makes it optional to ship an + * App will be initialized. This makes it optional to ship an * appinfo/application.php by using the built in query resolver * * @param array $routes the application routes diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index c47d102b881..299cab93eb3 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -36,6 +36,7 @@ use OC\Security\Normalizer\IpAddress; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IDBConnection; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\Bruteforce\MaxDelayReached; use Psr\Log\LoggerInterface; @@ -52,11 +53,8 @@ use Psr\Log\LoggerInterface; * * @package OC\Security\Bruteforce */ -class Throttler { +class Throttler implements IThrottler { public const LOGIN_ACTION = 'login'; - public const MAX_DELAY = 25; - public const MAX_DELAY_MS = 25000; // in milliseconds - public const MAX_ATTEMPTS = 10; /** @var IDBConnection */ private $db; @@ -65,8 +63,8 @@ class Throttler { private LoggerInterface $logger; /** @var IConfig */ private $config; - /** @var bool */ - private $hasAttemptsDeleted = false; + /** @var bool[] */ + private $hasAttemptsDeleted = []; public function __construct(IDBConnection $db, ITimeFactory $timeFactory, @@ -225,7 +223,7 @@ class Throttler { $maxAgeHours = 48; } - if ($ip === '' || $this->hasAttemptsDeleted) { + if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) { return 0; } @@ -303,7 +301,7 @@ class Throttler { $qb->executeStatement(); - $this->hasAttemptsDeleted = true; + $this->hasAttemptsDeleted[$action] = true; } /** @@ -311,7 +309,7 @@ class Throttler { * * @param string $ip */ - public function resetDelayForIP($ip) { + public function resetDelayForIP(string $ip): void { $cutoffTime = $this->getCutoffTimestamp(); $qb = $this->db->getQueryBuilder(); diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php index f3329135727..1167b3358d2 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php +++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php @@ -80,10 +80,8 @@ class ContentSecurityPolicyNonceManager { public function browserSupportsCspV3(): bool { $browserWhitelist = [ Request::USER_AGENT_CHROME, - // Firefox 45+ - '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/(4[5-9]|[5-9][0-9])\.[0-9.]+$/', - // Safari 12+ - '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/(?:1[2-9]|[2-9][0-9])\.[0-9]+(?:\.[0-9]+)? Safari\/[0-9.A-Z]+$/', + Request::USER_AGENT_FIREFOX, + Request::USER_AGENT_SAFARI, ]; if ($this->request->isUserAgent($browserWhitelist)) { diff --git a/lib/private/Security/TrustedDomainHelper.php b/lib/private/Security/TrustedDomainHelper.php index 0688ebba5b3..1927af9cb1d 100644 --- a/lib/private/Security/TrustedDomainHelper.php +++ b/lib/private/Security/TrustedDomainHelper.php @@ -97,7 +97,7 @@ class TrustedDomainHelper implements ITrustedDomainHelper { if (preg_match(Request::REGEX_LOCALHOST, $domain) === 1) { return true; } - // Reject misformed domains in any case + // Reject malformed domains in any case if (strpos($domain, '-') === 0 || strpos($domain, '..') !== false) { return false; } diff --git a/lib/private/Server.php b/lib/private/Server.php index 6e6fa430489..b473f4e75c0 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -151,6 +151,8 @@ use OC\SystemTag\ManagerFactory as SystemTagManagerFactory; use OC\Tagging\TagMapper; use OC\Talk\Broker; use OC\Template\JSCombiner; +use OC\User\Listeners\UserChangedListener; +use OC\User\Listeners\UserDeletedListener; use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; @@ -180,7 +182,6 @@ use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; use OCP\Files\Lock\ILockManager; use OCP\Files\Mount\IMountManager; -use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorageFactory; use OCP\Files\Template\ITemplateManager; use OCP\FullTextSearch\IFullTextSearchManager; @@ -217,7 +218,6 @@ use OCP\ISession; use OCP\ITagManager; use OCP\ITempManager; use OCP\IURLGenerator; -use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; @@ -231,6 +231,7 @@ use OCP\Remote\Api\IApiFactory; use OCP\Remote\IInstanceFactory; use OCP\RichObjectStrings\IValidator; use OCP\Route\IRouter; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\IContentSecurityPolicyManager; use OCP\Security\ICredentialsManager; use OCP\Security\ICrypto; @@ -249,6 +250,7 @@ use OCP\User\Events\BeforeUserLoggedOutEvent; use OCP\User\Events\PasswordUpdatedEvent; use OCP\User\Events\PostLoginEvent; use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserLoggedInEvent; use OCP\User\Events\UserLoggedInWithCookieEvent; use OCP\User\Events\UserLoggedOutEvent; @@ -351,7 +353,7 @@ class Server extends ServerContainer implements IServerContainer { return new Profiler($c->get(SystemConfig::class)); }); - $this->registerService(\OCP\Encryption\IManager::class, function (Server $c) { + $this->registerService(\OCP\Encryption\IManager::class, function (Server $c): Encryption\Manager { $view = new View(); $util = new Encryption\Util( $view, @@ -1001,6 +1003,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(ITrustedDomainHelper::class, TrustedDomainHelper::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('Throttler', Throttler::class); + $this->registerAlias(IThrottler::class, Throttler::class); $this->registerService('IntegrityCodeChecker', function (ContainerInterface $c) { // IConfig and IAppManager requires a working database. This code // might however be called when ownCloud is not yet setup. @@ -1473,52 +1476,13 @@ class Server extends ServerContainer implements IServerContainer { return $this->get(\OC\Calendar\Room\Manager::class); } - private function connectDispatcher() { - $dispatcher = $this->get(SymfonyAdapter::class); - - // Delete avatar on user deletion - $dispatcher->addListener('OCP\IUser::preDelete', function (GenericEvent $e) { - $logger = $this->get(LoggerInterface::class); - $manager = $this->getAvatarManager(); - /** @var IUser $user */ - $user = $e->getSubject(); - - try { - $avatar = $manager->getAvatar($user->getUID()); - $avatar->remove(); - } catch (NotFoundException $e) { - // no avatar to remove - } catch (\Exception $e) { - // Ignore exceptions - $logger->info('Could not cleanup avatar of ' . $user->getUID()); - } - }); - - $dispatcher->addListener('OCP\IUser::changeUser', function (GenericEvent $e) { - $manager = $this->getAvatarManager(); - /** @var IUser $user */ - $user = $e->getSubject(); - $feature = $e->getArgument('feature'); - $oldValue = $e->getArgument('oldValue'); - $value = $e->getArgument('value'); - - // We only change the avatar on display name changes - if ($feature !== 'displayName') { - return; - } - - try { - $avatar = $manager->getAvatar($user->getUID()); - $avatar->userChanged($feature, $oldValue, $value); - } catch (NotFoundException $e) { - // no avatar to remove - } - }); - - /** @var IEventDispatcher $eventDispatched */ - $eventDispatched = $this->get(IEventDispatcher::class); - $eventDispatched->addServiceListener(LoginFailed::class, LoginFailedListener::class); - $eventDispatched->addServiceListener(PostLoginEvent::class, UserLoggedInListener::class); + private function connectDispatcher(): void { + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $this->get(IEventDispatcher::class); + $eventDispatcher->addServiceListener(LoginFailed::class, LoginFailedListener::class); + $eventDispatcher->addServiceListener(PostLoginEvent::class, UserLoggedInListener::class); + $eventDispatcher->addServiceListener(UserChangedEvent::class, UserChangedListener::class); + $eventDispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class); } /** diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php index 285b6fd7960..6e0c54c6fab 100644 --- a/lib/private/Session/Internal.php +++ b/lib/private/Session/Internal.php @@ -172,7 +172,7 @@ class Internal extends Session { * @throws \Exception */ public function reopen() { - throw new \Exception('The session cannot be reopened - reopen() is ony to be used in unit testing.'); + throw new \Exception('The session cannot be reopened - reopen() is only to be used in unit testing.'); } /** diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index 7bd8fa7b1ec..e878ed4d9aa 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -80,7 +80,7 @@ class MySQL extends AbstractDatabase { $user = $this->dbUser; //we can't use OC_DB functions here because we need to connect as the administrative user. $characterSet = $this->config->getValue('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8'; - $query = "CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET $characterSet COLLATE ${characterSet}_bin;"; + $query = "CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET $characterSet COLLATE {$characterSet}_bin;"; $connection->executeUpdate($query); } catch (\Exception $ex) { $this->logger->error('Database creation failed.', [ diff --git a/lib/private/Share/Constants.php b/lib/private/Share/Constants.php index 31c734f94aa..3632a2a26d1 100644 --- a/lib/private/Share/Constants.php +++ b/lib/private/Share/Constants.php @@ -79,7 +79,7 @@ class Constants { public const FORMAT_STATUSES = -2; public const FORMAT_SOURCES = -3; // ToDo Check if it is still in use otherwise remove it - public const RESPONSE_FORMAT = 'json'; // default resonse format for ocs calls + public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls public const TOKEN_LENGTH = 15; // old (oc7) length is 32, keep token length in db at least that for compatibility diff --git a/lib/private/Share/Share.php b/lib/private/Share/Share.php index 9018f35ac2a..f47c042df29 100644 --- a/lib/private/Share/Share.php +++ b/lib/private/Share/Share.php @@ -732,7 +732,7 @@ class Share extends Constants { foreach ($result as $key => $r) { // for file/folder shares we need to compare file_source, otherwise we compare item_source // only group shares if they already point to the same target, otherwise the file where shared - // before grouping of shares was added. In this case we don't group them toi avoid confusions + // before grouping of shares was added. In this case we don't group them to avoid confusions if (($fileSharing && $item['file_source'] === $r['file_source'] && $item['file_target'] === $r['file_target']) || (!$fileSharing && $item['item_source'] === $r['item_source'] && $item['item_target'] === $r['item_target'])) { // add the first item to the list of grouped shares @@ -757,7 +757,7 @@ class Share extends Constants { /** * construct select statement * @param int $format - * @param boolean $fileDependent ist it a file/folder share or a generla share + * @param boolean $fileDependent ist it a file/folder share or a general share * @param string $uidOwner * @return string select statement */ diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 520bd17d3cf..70f9b8665f9 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -52,6 +52,7 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Mail\IMailer; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IAttributes; use OCP\Share\IShare; use OCP\Share\IShareProvider; @@ -174,6 +175,8 @@ class DefaultShareProvider implements IShareProvider { if (method_exists($share, 'getParent')) { $qb->setValue('parent', $qb->createNamedParameter($share->getParent())); } + + $qb->setValue('hide_download', $qb->createNamedParameter($share->getHideDownload() ? 1 : 0, IQueryBuilder::PARAM_INT)); } else { throw new \Exception('invalid share type!'); } @@ -193,6 +196,12 @@ class DefaultShareProvider implements IShareProvider { // set the permissions $qb->setValue('permissions', $qb->createNamedParameter($share->getPermissions())); + // set share attributes + $shareAttributes = $this->formatShareAttributes( + $share->getAttributes() + ); + $qb->setValue('attributes', $qb->createNamedParameter($shareAttributes)); + // Set who created this share $qb->setValue('uid_initiator', $qb->createNamedParameter($share->getSharedBy())); @@ -248,6 +257,8 @@ class DefaultShareProvider implements IShareProvider { public function update(\OCP\Share\IShare $share) { $originalShare = $this->getShareById($share->getId()); + $shareAttributes = $this->formatShareAttributes($share->getAttributes()); + if ($share->getShareType() === IShare::TYPE_USER) { /* * We allow updating the recipient on user shares. @@ -259,6 +270,7 @@ class DefaultShareProvider implements IShareProvider { ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) @@ -272,6 +284,7 @@ class DefaultShareProvider implements IShareProvider { ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) @@ -301,6 +314,7 @@ class DefaultShareProvider implements IShareProvider { ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0))) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->execute(); } elseif ($share->getShareType() === IShare::TYPE_LINK) { $qb = $this->dbConn->getQueryBuilder(); @@ -311,6 +325,7 @@ class DefaultShareProvider implements IShareProvider { ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('token', $qb->createNamedParameter($share->getToken())) @@ -611,6 +626,10 @@ class DefaultShareProvider implements IShareProvider { $data = $stmt->fetch(); $stmt->closeCursor(); + $shareAttributes = $this->formatShareAttributes( + $share->getAttributes() + ); + if ($data === false) { // No usergroup share yet. Create one. $qb = $this->dbConn->getQueryBuilder(); @@ -626,6 +645,7 @@ class DefaultShareProvider implements IShareProvider { 'file_source' => $qb->createNamedParameter($share->getNodeId()), 'file_target' => $qb->createNamedParameter($share->getTarget()), 'permissions' => $qb->createNamedParameter($share->getPermissions()), + 'attributes' => $qb->createNamedParameter($shareAttributes), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), ])->execute(); } else { @@ -641,9 +661,12 @@ class DefaultShareProvider implements IShareProvider { return $share; } - public function getSharesInFolder($userId, Folder $node, $reshares) { + public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) { $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') + $qb->select('s.*', + 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', + 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', + 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum') ->from('share', 's') ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), @@ -679,12 +702,21 @@ class DefaultShareProvider implements IShareProvider { }, $childMountNodes); $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())), - $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) - ) - ); + if ($shallow) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())), + $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConn->escapeLikeParameter($node->getInternalPath()) . '/%')), + $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) + ) + ); + } $qb->orderBy('id'); @@ -1061,6 +1093,8 @@ class DefaultShareProvider implements IShareProvider { $share->setToken($data['token']); } + $share = $this->updateShareAttributes($share, $data['attributes']); + $share->setSharedBy($data['uid_initiator']); $share->setShareOwner($data['uid_owner']); @@ -1282,7 +1316,7 @@ class DefaultShareProvider implements IShareProvider { $chunks = array_chunk($ids, 100); foreach ($chunks as $chunk) { /* - * Delete all special shares wit this users for the found group shares + * 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))) @@ -1528,4 +1562,48 @@ class DefaultShareProvider implements IShareProvider { } $cursor->closeCursor(); } + + /** + * Load from database format (JSON string) to IAttributes + * + * @return IShare the modified share + */ + private function updateShareAttributes(IShare $share, ?string $data): IShare { + if ($data !== null && $data !== '') { + $attributes = new ShareAttributes(); + $compressedAttributes = \json_decode($data, true); + if ($compressedAttributes === false || $compressedAttributes === null) { + return $share; + } + foreach ($compressedAttributes as $compressedAttribute) { + $attributes->setAttribute( + $compressedAttribute[0], + $compressedAttribute[1], + $compressedAttribute[2] + ); + } + $share->setAttributes($attributes); + } + + return $share; + } + + /** + * Format IAttributes to database format (JSON string) + */ + private function formatShareAttributes(?IAttributes $attributes): ?string { + if ($attributes === null || empty($attributes->toArray())) { + return null; + } + + $compressedAttributes = []; + foreach ($attributes->toArray() as $attribute) { + $compressedAttributes[] = [ + 0 => $attribute['scope'], + 1 => $attribute['key'], + 2 => $attribute['enabled'] + ]; + } + return \json_encode($compressedAttributes); + } } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index eed86bb41c3..2ef61cf3404 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -41,7 +41,7 @@ */ namespace OC\Share20; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OC\Files\Mount\MoveableMount; use OC\KnownUser\KnownUserService; use OC\Share20\Exception\ProviderException; @@ -650,7 +650,7 @@ class Manager implements IManager { } // Check if public upload is allowed - if (!$this->shareApiLinkAllowPublicUpload() && + if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload() && ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) { throw new \InvalidArgumentException('Public upload is not allowed'); } @@ -1093,6 +1093,7 @@ class Manager implements IManager { 'shareWith' => $share->getSharedWith(), 'uidOwner' => $share->getSharedBy(), 'permissions' => $share->getPermissions(), + 'attributes' => $share->getAttributes() !== null ? $share->getAttributes()->toArray() : null, 'path' => $userFolder->getRelativePath($share->getNode()->getPath()), ]); } @@ -1303,11 +1304,11 @@ class Manager implements IManager { return $provider->move($share, $recipientId); } - public function getSharesInFolder($userId, Folder $node, $reshares = false) { + public function getSharesInFolder($userId, Folder $node, $reshares = false, $shallow = true) { $providers = $this->factory->getAllProviders(); - return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) { - $newShares = $provider->getSharesInFolder($userId, $node, $reshares); + return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares, $shallow) { + $newShares = $provider->getSharesInFolder($userId, $node, $reshares, $shallow); foreach ($newShares as $fid => $data) { if (!isset($shares[$fid])) { $shares[$fid] = []; @@ -1543,7 +1544,7 @@ class Manager implements IManager { * Reduce the permissions for link or email shares if public upload is not enabled */ if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) - && !$this->shareApiLinkAllowPublicUpload()) { + && $share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()) { $share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE)); } @@ -1968,7 +1969,7 @@ class Manager implements IManager { } public function ignoreSecondDisplayName(): bool { - return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no') === 'yes'; + return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; } public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool { @@ -2010,7 +2011,7 @@ class Manager implements IManager { /** * Copied from \OC_Util::isSharingDisabledForUser * - * TODO: Deprecate fuction from OC_Util + * TODO: Deprecate function from OC_Util * * @param string $userId * @return bool diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index 7ed03832e4c..c2d45503696 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -37,6 +37,7 @@ use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\IUserManager; use OCP\Share\Exceptions\IllegalIDChangeException; +use OCP\Share\IAttributes; use OCP\Share\IShare; class Share implements IShare { @@ -65,6 +66,8 @@ class Share implements IShare { private $shareOwner; /** @var int */ private $permissions; + /** @var IAttributes */ + private $attributes; /** @var int */ private $status; /** @var string */ @@ -319,7 +322,7 @@ class Share implements IShare { * @inheritdoc */ public function setPermissions($permissions) { - //TODO checkes + //TODO checks $this->permissions = $permissions; return $this; @@ -335,6 +338,28 @@ class Share implements IShare { /** * @inheritdoc */ + public function newAttributes(): IAttributes { + return new ShareAttributes(); + } + + /** + * @inheritdoc + */ + public function setAttributes(?IAttributes $attributes) { + $this->attributes = $attributes; + return $this; + } + + /** + * @inheritdoc + */ + public function getAttributes(): ?IAttributes { + return $this->attributes; + } + + /** + * @inheritdoc + */ public function setStatus(int $status): IShare { $this->status = $status; return $this; @@ -511,7 +536,7 @@ class Share implements IShare { * Set the parent of this share * * @param int parent - * @return \OCP\Share\IShare + * @return IShare * @deprecated The new shares do not have parents. This is just here for legacy reasons. */ public function setParent($parent) { diff --git a/lib/private/Share20/ShareAttributes.php b/lib/private/Share20/ShareAttributes.php new file mode 100644 index 00000000000..92f034e6783 --- /dev/null +++ b/lib/private/Share20/ShareAttributes.php @@ -0,0 +1,73 @@ +<?php +/** + * @author Piotr Mrowczynski <piotr@owncloud.com> + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OC\Share20; + +use OCP\Share\IAttributes; + +class ShareAttributes implements IAttributes { + + /** @var array */ + private $attributes; + + public function __construct() { + $this->attributes = []; + } + + /** + * @inheritdoc + */ + public function setAttribute($scope, $key, $enabled) { + if (!\array_key_exists($scope, $this->attributes)) { + $this->attributes[$scope] = []; + } + $this->attributes[$scope][$key] = $enabled; + return $this; + } + + /** + * @inheritdoc + */ + public function getAttribute($scope, $key) { + if (\array_key_exists($scope, $this->attributes) && + \array_key_exists($key, $this->attributes[$scope])) { + return $this->attributes[$scope][$key]; + } + return null; + } + + /** + * @inheritdoc + */ + public function toArray() { + $result = []; + foreach ($this->attributes as $scope => $keys) { + foreach ($keys as $key => $enabled) { + $result[] = [ + "scope" => $scope, + "key" => $key, + "enabled" => $enabled + ]; + } + } + + return $result; + } +} diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index a7b9fd35fd8..5f23b471837 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -47,6 +47,7 @@ use OCP\IInitialStateService; use OCP\IL10N; use OCP\ISession; use OCP\IURLGenerator; +use OCP\ILogger; use OCP\IUser; use OCP\User\Backend\IPasswordConfirmationBackend; use OCP\Util; @@ -176,6 +177,9 @@ class JSConfigHelper { 'sharing.maxAutocompleteResults' => max(0, $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT)), 'sharing.minSearchStringLength' => $this->config->getSystemValueInt('sharing.minSearchStringLength', 0), 'blacklist_files_regex' => FileInfo::BLACKLIST_FILES_REGEX, + 'loglevel' => $this->config->getSystemValue('loglevel_frontend', + $this->config->getSystemValue('loglevel', ILogger::WARN) + ), ]; $array = [ diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index a5aabc04b61..37f459ca52d 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -281,6 +281,9 @@ class TemplateLayout extends \OC_Template { } $this->assign('initialStates', $this->initialState->getInitialStates()); + + $this->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content'); + $this->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null); } /** diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 753a4a217d1..6115d4a221e 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -42,6 +42,8 @@ namespace OC; use OC\Route\Router; use OCA\Theming\ThemingDefaults; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IRequest; @@ -65,12 +67,14 @@ class URLGenerator implements IURLGenerator { private $router; /** @var null|string */ private $baseUrl = null; + private ?IAppManager $appManager = null; public function __construct(IConfig $config, IUserSession $userSession, ICacheFactory $cacheFactory, IRequest $request, - Router $router) { + Router $router + ) { $this->config = $config; $this->userSession = $userSession; $this->cacheFactory = $cacheFactory; @@ -78,6 +82,14 @@ class URLGenerator implements IURLGenerator { $this->router = $router; } + private function getAppManager(): IAppManager { + if ($this->appManager !== null) { + return $this->appManager; + } + $this->appManager = \OCP\Server::get(IAppManager::class); + return $this->appManager; + } + /** * Creates an url using a defined route * @@ -132,7 +144,7 @@ class URLGenerator implements IURLGenerator { $frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true'); if ($appName !== '') { - $app_path = \OC_App::getAppPath($appName); + $app_path = $this->getAppManager()->getAppPath($appName); // Check if the app is in the app folder if ($app_path && file_exists($app_path . '/' . $file)) { if (substr($file, -3) === 'php') { @@ -142,7 +154,7 @@ class URLGenerator implements IURLGenerator { } $urlLinkTo .= ($file !== 'index.php') ? '/' . $file : ''; } else { - $urlLinkTo = \OC_App::getAppWebPath($appName) . '/' . $file; + $urlLinkTo = $this->getAppManager()->getAppWebPath($appName) . '/' . $file; } } else { $urlLinkTo = \OC::$WEBROOT . '/' . $appName . '/' . $file; @@ -189,11 +201,20 @@ class URLGenerator implements IURLGenerator { //if a theme has a png but not an svg always use the png $basename = substr(basename($file), 0, -4); - $appPath = \OC_App::getAppPath($appName); + try { + $appPath = $this->getAppManager()->getAppPath($appName); + } catch (AppPathNotFoundException $e) { + if ($appName === 'core' || $appName === '') { + $appName = 'core'; + $appPath = false; + } else { + throw new RuntimeException('image not found: image: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT); + } + } // Check if the app is in the app folder $path = ''; - $themingEnabled = $this->config->getSystemValue('installed', false) && \OCP\App::isEnabled('theming') && \OC_App::isAppLoaded('theming'); + $themingEnabled = $this->config->getSystemValue('installed', false) && $this->getAppManager()->isEnabledForUser('theming'); $themingImagePath = false; if ($themingEnabled) { $themingDefaults = \OC::$server->getThemingDefaults(); @@ -220,10 +241,10 @@ class URLGenerator implements IURLGenerator { } elseif ($themingEnabled && $themingImagePath) { $path = $themingImagePath; } elseif ($appPath && file_exists($appPath . "/img/$file")) { - $path = \OC_App::getAppWebPath($appName) . "/img/$file"; + $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$file"; } elseif ($appPath && !file_exists($appPath . "/img/$basename.svg") && file_exists($appPath . "/img/$basename.png")) { - $path = \OC_App::getAppWebPath($appName) . "/img/$basename.png"; + $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$basename.png"; } elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/$appName/img/$file")) { $path = \OC::$WEBROOT . "/$appName/img/$file"; } elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.svg") @@ -320,7 +341,7 @@ class URLGenerator implements IURLGenerator { * @return string base url of the current request */ public function getBaseUrl(): string { - // BaseUrl can be equal to 'http(s)://' during the first steps of the intial setup. + // 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://") { $this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT; } diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 2c06cffcb19..da989c4db91 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -378,7 +378,7 @@ class Updater extends BasicEmitter { $appManager = \OC::$server->getAppManager(); foreach ($apps as $app) { // check if the app is compatible with this version of Nextcloud - $info = OC_App::getAppInfo($app); + $info = $appManager->getAppInfo($app); if ($info === null || !OC_App::isAppCompatible($version, $info)) { if ($appManager->isShipped($app)) { throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update'); diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php index 4821a2fc632..0b38f04bfe3 100644 --- a/lib/private/User/Database.php +++ b/lib/private/User/Database.php @@ -45,7 +45,7 @@ declare(strict_types=1); */ namespace OC\User; -use OC\Cache\CappedMemoryCache; +use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\Security\Events\ValidatePasswordPolicyEvent; @@ -215,6 +215,10 @@ class Database extends ABackend implements * Change the display name of a user */ public function setDisplayName(string $uid, string $displayName): bool { + if (mb_strlen($displayName) > 64) { + return false; + } + $this->fixDI(); if ($this->userExists($uid)) { diff --git a/lib/private/User/Listeners/UserChangedListener.php b/lib/private/User/Listeners/UserChangedListener.php new file mode 100644 index 00000000000..a561db2423d --- /dev/null +++ b/lib/private/User/Listeners/UserChangedListener.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\User\Listeners; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserChangedEvent; +use OCP\Files\NotFoundException; +use OCP\IAvatarManager; + +/** + * @template-implements IEventListener<UserChangedEvent> + */ +class UserChangedListener implements IEventListener { + private IAvatarManager $avatarManager; + + public function __construct(IAvatarManager $avatarManager) { + $this->avatarManager = $avatarManager; + } + + public function handle(Event $event): void { + if (!($event instanceof UserChangedEvent)) { + return; + } + + $user = $event->getUser(); + $feature = $event->getFeature(); + $oldValue = $event->getOldValue(); + $value = $event->getValue(); + + // We only change the avatar on display name changes + if ($feature === 'displayName') { + try { + $avatar = $this->avatarManager->getAvatar($user->getUID()); + $avatar->userChanged($feature, $oldValue, $value); + } catch (NotFoundException $e) { + // no avatar to remove + } + } + } +} diff --git a/lib/private/User/Listeners/UserDeletedListener.php b/lib/private/User/Listeners/UserDeletedListener.php new file mode 100644 index 00000000000..7c9c46ef371 --- /dev/null +++ b/lib/private/User/Listeners/UserDeletedListener.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\User\Listeners; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserDeletedEvent; +use OCP\Files\NotFoundException; +use OCP\IAvatarManager; +use Psr\Log\LoggerInterface; + +/** + * @template-implements IEventListener<UserDeletedEvent> + */ +class UserDeletedListener implements IEventListener { + private IAvatarManager $avatarManager; + private LoggerInterface $logger; + + public function __construct(LoggerInterface $logger, IAvatarManager $avatarManager) { + $this->avatarManager = $avatarManager; + $this->logger = $logger; + } + + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + return; + } + + $user = $event->getUser(); + + // Delete avatar on user deletion + try { + $avatar = $this->avatarManager->getAvatar($user->getUID()); + $avatar->remove(true); + } catch (NotFoundException $e) { + // no avatar to remove + } catch (\Exception $e) { + // Ignore exceptions + $this->logger->info('Could not cleanup avatar of ' . $user->getUID(), [ + 'exception' => $e, + ]); + } + } +} diff --git a/lib/private/User/User.php b/lib/private/User/User.php index de9af35f541..7f7d6273e30 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -244,10 +244,15 @@ class User implements IUser { * updates the timestamp of the most recent login of this user */ public function updateLastLoginTimestamp() { - $firstTimeLogin = ($this->getLastLogin() === 0); - $this->lastLogin = time(); - $this->config->setUserValue( - $this->uid, 'login', 'lastLogin', (string)$this->lastLogin); + $previousLogin = $this->getLastLogin(); + $now = time(); + $firstTimeLogin = $previousLogin === 0; + + if ($now - $previousLogin > 60) { + $this->lastLogin = time(); + $this->config->setUserValue( + $this->uid, 'login', 'lastLogin', (string)$this->lastLogin); + } return $firstTimeLogin; } @@ -555,15 +560,9 @@ class User implements IUser { return $uid . '@' . $server; } - /** - * @param string $url - * @return string - */ - private function removeProtocolFromUrl($url) { + private function removeProtocolFromUrl(string $url): string { if (strpos($url, 'https://') === 0) { return substr($url, strlen('https://')); - } elseif (strpos($url, 'http://') === 0) { - return substr($url, strlen('http://')); } return $url; diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index f290b7a610c..482fc4e88e7 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -183,7 +183,7 @@ class OC_App { 'app' => $app, ]); try { - self::requireAppFile($app); + self::requireAppFile($appPath); } catch (Throwable $ex) { if ($ex instanceof ServerNotAvailableException) { throw $ex; @@ -679,25 +679,6 @@ class OC_App { } /** - * register an admin form to be shown - * - * @param string $app - * @param string $page - */ - public static function registerAdmin(string $app, string $page) { - self::$adminForms[] = $app . '/' . $page . '.php'; - } - - /** - * register a personal form to be shown - * @param string $app - * @param string $page - */ - public static function registerPersonal(string $app, string $page) { - self::$personalForms[] = $app . '/' . $page . '.php'; - } - - /** * @param array $entry * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface */ diff --git a/lib/private/legacy/OC_Files.php b/lib/private/legacy/OC_Files.php index 02e15fd08d5..6a3a44d6cc0 100644 --- a/lib/private/legacy/OC_Files.php +++ b/lib/private/legacy/OC_Files.php @@ -44,10 +44,12 @@ use bantu\IniGetWrapper\IniGetWrapper; use OC\Files\View; use OC\Streamer; use OCP\Lock\ILockingProvider; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\EventDispatcher\IEventDispatcher; /** * Class for file server access - * */ class OC_Files { public const FILE = 1; @@ -167,6 +169,14 @@ class OC_Files { } } + //Dispatch an event to see if any apps have problem with download + $event = new BeforeZipCreatedEvent($dir, is_array($files) ? $files : [$files]); + $dispatcher = \OCP\Server::get(IEventDispatcher::class); + $dispatcher->dispatchTyped($event); + if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) { + throw new \OC\ForbiddenException($event->getErrorMessage()); + } + $streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles); OC_Util::obEnd(); @@ -222,13 +232,16 @@ class OC_Files { self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); OC::$server->getLogger()->logException($ex); $l = \OC::$server->getL10N('lib'); - \OC_Template::printErrorPage($l->t('Cannot read file'), $ex->getMessage(), 200); + \OC_Template::printErrorPage($l->t('Cannot download file'), $ex->getMessage(), 200); } catch (\Exception $ex) { self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); OC::$server->getLogger()->logException($ex); $l = \OC::$server->getL10N('lib'); $hint = method_exists($ex, 'getHint') ? $ex->getHint() : ''; - \OC_Template::printErrorPage($l->t('Cannot read file'), $hint, 200); + if ($event && $event->getErrorMessage() !== null) { + $hint .= ' ' . $event->getErrorMessage(); + } + \OC_Template::printErrorPage($l->t('Cannot download file'), $hint, 200); } } @@ -287,6 +300,7 @@ class OC_Files { * @param string $name * @param string $dir * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header + * @throws \OC\ForbiddenException */ private static function getSingleFile($view, $dir, $name, $params) { $filename = $dir . '/' . $name; @@ -322,6 +336,19 @@ class OC_Files { $rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize); } + $dispatcher = \OC::$server->query(IEventDispatcher::class); + $event = new BeforeDirectFileDownloadEvent($filename); + $dispatcher->dispatchTyped($event); + + if (!\OC\Files\Filesystem::isReadable($filename) || $event->getErrorMessage()) { + if ($event->getErrorMessage()) { + $msg = $event->getErrorMessage(); + } else { + $msg = 'Access denied'; + } + throw new \OC\ForbiddenException($msg); + } + self::sendHeaders($filename, $name, $rangeArray); if (isset($params['head']) && $params['head']) { diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index b7547be5e82..de066e143b4 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -178,7 +178,11 @@ class OC_User { } $userSession->setLoginName($uid); $request = OC::$server->getRequest(); - $userSession->createSessionToken($request, $uid, $uid); + $password = null; + if ($backend instanceof \OCP\Authentication\IProvideUserSecretBackend) { + $password = $backend->getCurrentUserSecret(); + } + $userSession->createSessionToken($request, $uid, $uid, $password); $userSession->createRememberMeToken($userSession->getUser()); // setup the filesystem OC_Util::setupFS($uid); @@ -191,7 +195,7 @@ class OC_User { 'post_login', [ 'uid' => $uid, - 'password' => null, + 'password' => $password, 'isTokenLogin' => false, ] ); |