diff options
Diffstat (limited to 'lib/private')
81 files changed, 1248 insertions, 820 deletions
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 20e0add1ccb..8fc16d5ce1a 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -131,6 +131,7 @@ class AccountManager implements IAccountManager { self::PROPERTY_AVATAR => self::SCOPE_FEDERATED, self::PROPERTY_PHONE => self::SCOPE_LOCAL, self::PROPERTY_TWITTER => self::SCOPE_LOCAL, + self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL, self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL, self::PROPERTY_ROLE => self::SCOPE_LOCAL, self::PROPERTY_HEADLINE => self::SCOPE_LOCAL, @@ -525,6 +526,7 @@ class AccountManager implements IAccountManager { protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void { static $propertiesVerifiableByLookupServer = [ self::PROPERTY_TWITTER, + self::PROPERTY_FEDIVERSE, self::PROPERTY_WEBSITE, self::PROPERTY_EMAIL, ]; @@ -724,6 +726,13 @@ class AccountManager implements IAccountManager { ], [ + 'name' => self::PROPERTY_FEDIVERSE, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_FEDIVERSE], + 'verified' => self::NOT_VERIFIED, + ], + + [ 'name' => self::PROPERTY_ORGANISATION, 'value' => '', 'scope' => $scopes[self::PROPERTY_ORGANISATION], diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index 4166c2ead03..9b94c0b07bc 100644 --- a/lib/private/App/PlatformRepository.php +++ b/lib/private/App/PlatformRepository.php @@ -50,7 +50,11 @@ class PlatformRepository { $ext = new \ReflectionExtension($name); try { $prettyVersion = $ext->getVersion(); - $prettyVersion = $this->normalizeVersion($prettyVersion); + /** @psalm-suppress TypeDoesNotContainNull + * @psalm-suppress RedundantCondition + * TODO Remove these annotations once psalm fixes the method signature ( https://github.com/vimeo/psalm/pull/8655 ) + */ + $prettyVersion = $this->normalizeVersion($prettyVersion ?? '0'); } catch (\UnexpectedValueException $e) { $prettyVersion = '0'; $prettyVersion = $this->normalizeVersion($prettyVersion); @@ -111,6 +115,9 @@ class PlatformRepository { continue 2; } + if ($prettyVersion === null) { + continue; + } try { $prettyVersion = $this->normalizeVersion($prettyVersion); } catch (\UnexpectedValueException $e) { diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 00e5dddc1f9..efaa2209eca 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -46,6 +46,7 @@ class AppConfig implements IAppConfig { /** @var array[] */ protected $sensitiveValues = [ 'circles' => [ + '/^key_pairs$/', '/^local_gskey$/', ], 'external' => [ @@ -109,9 +110,12 @@ class AppConfig implements IAppConfig { '/^cookie$/', ], 'spreed' => [ - '/^bridge_bot_password/', + '/^bridge_bot_password$/', + '/^hosted-signaling-server-(.*)$/', '/^signaling_servers$/', '/^signaling_ticket_secret$/', + '/^signaling_token_privkey_(.*)$/', + '/^signaling_token_pubkey_(.*)$/', '/^sip_bridge_dialin_info$/', '/^sip_bridge_shared_secret$/', '/^stun_servers$/', @@ -132,6 +136,9 @@ class AppConfig implements IAppConfig { 'user_ldap' => [ '/^(s..)?ldap_agent_password$/', ], + 'user_saml' => [ + '/^idp-x509cert$/', + ], ]; /** @var Connection */ diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index e06d5226a28..462f9b978fd 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -79,6 +79,7 @@ use Psr\Log\LoggerInterface; * @deprecated 20.0.0 */ class DIContainer extends SimpleContainer implements IAppContainer { + private string $appName; /** * @var array @@ -94,9 +95,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { * @param array $urlParams * @param ServerContainer|null $server */ - public function __construct($appName, $urlParams = [], ServerContainer $server = null) { + public function __construct(string $appName, array $urlParams = [], ServerContainer $server = null) { parent::__construct(); - $this['AppName'] = $appName; + $this->appName = $appName; + $this['appName'] = $appName; $this['urlParams'] = $urlParams; $this->registerAlias('Request', IRequest::class); @@ -109,9 +111,12 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->server->registerAppContainer($appName, $this); // aliases - $this->registerAlias('appName', 'AppName'); - $this->registerAlias('webRoot', 'WebRoot'); - $this->registerAlias('userId', 'UserId'); + /** @deprecated inject $appName */ + $this->registerAlias('AppName', 'appName'); + /** @deprecated inject $webRoot*/ + $this->registerAlias('WebRoot', 'webRoot'); + /** @deprecated inject $userId */ + $this->registerAlias('UserId', 'userId'); /** * Core services @@ -158,11 +163,11 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->registerAlias(IAppContainer::class, ContainerInterface::class); // commonly used attributes - $this->registerService('UserId', function (ContainerInterface $c) { + $this->registerService('userId', function (ContainerInterface $c) { return $c->get(IUserSession::class)->getSession()->get('user_id'); }); - $this->registerService('WebRoot', function (ContainerInterface $c) { + $this->registerService('webRoot', function (ContainerInterface $c) { return $c->get(IServerContainer::class)->getWebRoot(); }); @@ -301,7 +306,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { new OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware( $c->get(IRequest::class), $c->get(ISession::class), - $c->get(\OCP\IConfig::class) + $c->get(\OCP\IConfig::class), + $c->get(OC\Security\Bruteforce\Throttler::class) ) ); $dispatcher->registerMiddleware( @@ -433,6 +439,15 @@ class DIContainer extends SimpleContainer implements IAppContainer { } public function query(string $name, bool $autoload = true) { + if ($name === 'AppName' || $name === 'appName') { + return $this->appName; + } + + $isServerClass = str_starts_with($name, 'OCP\\') || str_starts_with($name, 'OC\\'); + if ($isServerClass && !$this->has($name)) { + return $this->getServer()->query($name, $autoload); + } + try { return $this->queryNoFallback($name); } catch (QueryException $firstException) { @@ -457,11 +472,11 @@ class DIContainer extends SimpleContainer implements IAppContainer { if ($this->offsetExists($name)) { return parent::query($name); - } elseif ($this['AppName'] === 'settings' && strpos($name, 'OC\\Settings\\') === 0) { + } elseif ($this->appName === 'settings' && str_starts_with($name, 'OC\\Settings\\')) { return parent::query($name); - } elseif ($this['AppName'] === 'core' && strpos($name, 'OC\\Core\\') === 0) { + } elseif ($this->appName === 'core' && str_starts_with($name, 'OC\\Core\\')) { return parent::query($name); - } elseif (strpos($name, \OC\AppFramework\App::buildAppNamespace($this['AppName']) . '\\') === 0) { + } elseif (str_starts_with($name, \OC\AppFramework\App::buildAppNamespace($this->appName) . '\\')) { return parent::query($name); } diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index c1a203a7165..aaaf2ba6560 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -194,7 +194,7 @@ class Dispatcher { $arguments = []; // valid types that will be casted - $types = ['int', 'integer', 'bool', 'boolean', 'float']; + $types = ['int', 'integer', 'bool', 'boolean', 'float', 'double']; foreach ($this->reflector->getParameters() as $param => $default) { diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 496a845dd4a..286187c696c 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -431,13 +431,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { // 'application/json' must be decoded manually. if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { $params = json_decode(file_get_contents($this->inputStream), true); - if ($params !== null && \count($params) > 0) { + if (\is_array($params) && \count($params) > 0) { $this->items['params'] = $params; if ($this->method === 'POST') { $this->items['post'] = $params; } } - // Handle application/x-www-form-urlencoded for methods other than GET // or post correctly } elseif ($this->method !== 'GET' diff --git a/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php index d3beb4fd3a8..d956ce58912 100644 --- a/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php +++ b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php @@ -24,6 +24,7 @@ namespace OC\AppFramework\Middleware\PublicShare; use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException; +use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\AuthPublicShareController; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Middleware; @@ -34,6 +35,7 @@ use OCP\IRequest; use OCP\ISession; class PublicShareMiddleware extends Middleware { + /** @var IRequest */ private $request; @@ -43,10 +45,14 @@ class PublicShareMiddleware extends Middleware { /** @var IConfig */ private $config; - public function __construct(IRequest $request, ISession $session, IConfig $config) { + /** @var Throttler */ + private $throttler; + + public function __construct(IRequest $request, ISession $session, IConfig $config, Throttler $throttler) { $this->request = $request; $this->session = $session; $this->config = $config; + $this->throttler = $throttler; } public function beforeController($controller, $methodName) { @@ -54,6 +60,11 @@ class PublicShareMiddleware extends Middleware { return; } + $controllerClassPath = explode('\\', get_class($controller)); + $controllerShortClass = end($controllerClassPath); + $bruteforceProtectionAction = $controllerShortClass . '::' . $methodName; + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $bruteforceProtectionAction); + if (!$this->isLinkSharingEnabled()) { throw new NotFoundException('Link sharing is disabled'); } @@ -68,6 +79,8 @@ class PublicShareMiddleware extends Middleware { $controller->setToken($token); if (!$controller->isValidToken()) { + $this->throttle($bruteforceProtectionAction, $token); + $controller->shareNotFound(); throw new NotFoundException(); } @@ -88,6 +101,7 @@ class PublicShareMiddleware extends Middleware { throw new NeedAuthenticationException(); } + $this->throttle($bruteforceProtectionAction, $token); throw new NotFoundException(); } @@ -128,4 +142,10 @@ class PublicShareMiddleware extends Middleware { return true; } + + private function throttle($bruteforceProtectionAction, $token): void { + $ip = $this->request->getRemoteAddress(); + $this->throttler->sleepDelay($ip, $bruteforceProtectionAction); + $this->throttler->registerAttempt($bruteforceProtectionAction, $ip, ['token' => $token]); + } } diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index da1efdec826..925ef67de64 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -108,7 +108,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { return $this->query($resolveName); } catch (QueryException $e2) { // don't lose the error we got while trying to query by type - throw new QueryException($e2->getMessage(), (int) $e2->getCode(), $e); + throw new QueryException($e->getMessage(), (int) $e->getCode(), $e); } } diff --git a/lib/private/Authentication/Events/LoginFailed.php b/lib/private/Authentication/Events/LoginFailed.php index 138e567139a..ef702e40a59 100644 --- a/lib/private/Authentication/Events/LoginFailed.php +++ b/lib/private/Authentication/Events/LoginFailed.php @@ -28,17 +28,21 @@ namespace OC\Authentication\Events; use OCP\EventDispatcher\Event; class LoginFailed extends Event { + private string $loginName; + private ?string $password; - /** @var string */ - private $loginName; - - public function __construct(string $loginName) { + public function __construct(string $loginName, ?string $password) { parent::__construct(); $this->loginName = $loginName; + $this->password = $password; } public function getLoginName(): string { return $this->loginName; } + + public function getPassword(): ?string { + return $this->password; + } } diff --git a/lib/private/Authentication/Listeners/LoginFailedListener.php b/lib/private/Authentication/Listeners/LoginFailedListener.php index 12d5dd4c17e..f75ee51b287 100644 --- a/lib/private/Authentication/Listeners/LoginFailedListener.php +++ b/lib/private/Authentication/Listeners/LoginFailedListener.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OC\Authentication\Listeners; use OC\Authentication\Events\LoginFailed; +use OCP\Authentication\Events\AnyLoginFailedEvent; use OCP\Authentication\Events\LoginFailedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -55,6 +56,8 @@ class LoginFailedListener implements IEventListener { return; } + $this->dispatcher->dispatchTyped(new AnyLoginFailedEvent($event->getLoginName(), $event->getPassword())); + $uid = $event->getLoginName(); Util::emitHook( '\OCA\Files_Sharing\API\Server2Server', diff --git a/lib/private/Authentication/Login/EmailLoginCommand.php b/lib/private/Authentication/Login/EmailLoginCommand.php index e2e55cc12c8..7145ab9e14f 100644 --- a/lib/private/Authentication/Login/EmailLoginCommand.php +++ b/lib/private/Authentication/Login/EmailLoginCommand.php @@ -38,9 +38,21 @@ class EmailLoginCommand extends ALoginCommand { public function process(LoginData $loginData): LoginResult { if ($loginData->getUser() === false) { + if (!filter_var($loginData->getUsername(), FILTER_VALIDATE_EMAIL)) { + return $this->processNextOrFinishSuccessfully($loginData); + } + $users = $this->userManager->getByEmail($loginData->getUsername()); // we only allow login by email if unique if (count($users) === 1) { + + // FIXME: This is a workaround to still stick to configured LDAP login filters + // this can be removed once the email login is properly implemented in the local user backend + // as described in https://github.com/nextcloud/server/issues/5221 + if ($users[0]->getBackendClassName() === 'LDAP') { + return $this->processNextOrFinishSuccessfully($loginData); + } + $username = $users[0]->getUID(); if ($username !== $loginData->getUsername()) { $user = $this->userManager->checkPassword( diff --git a/lib/private/Authentication/Login/LoggedInCheckCommand.php b/lib/private/Authentication/Login/LoggedInCheckCommand.php index 0efd84adbf5..9f80d47a3d4 100644 --- a/lib/private/Authentication/Login/LoggedInCheckCommand.php +++ b/lib/private/Authentication/Login/LoggedInCheckCommand.php @@ -48,11 +48,12 @@ class LoggedInCheckCommand extends ALoginCommand { public function process(LoginData $loginData): LoginResult { if ($loginData->getUser() === false) { $loginName = $loginData->getUsername(); + $password = $loginData->getPassword(); $ip = $loginData->getRequest()->getRemoteAddress(); $this->logger->warning("Login failed: $loginName (Remote IP: $ip)"); - $this->dispatcher->dispatchTyped(new LoginFailed($loginName)); + $this->dispatcher->dispatchTyped(new LoginFailed($loginName, $password)); return LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); } diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index 550ba36dd6b..e85c2d2e377 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -7,6 +7,7 @@ declare(strict_types=1); * * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author Georg Ehrke <oc.list@georgehrke.com> + * @author Anna Larch <anna.larch@gmx.net> * * @license GNU AGPL version 3 or any later version * @@ -33,6 +34,7 @@ use OCP\Calendar\ICalendar; use OCP\Calendar\ICalendarProvider; use OCP\Calendar\ICalendarQuery; use OCP\Calendar\ICreateFromString; +use OCP\Calendar\IHandleImipMessage; use OCP\Calendar\IManager; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -280,7 +282,7 @@ class Manager implements IManager { // Drawback: attendees that have been deleted will still be able to update their partstat foreach ($calendars as $calendar) { // We should not search in writable calendars - if ($calendar instanceof ICreateFromString) { + if ($calendar instanceof IHandleImipMessage) { $o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]); if (!empty($o)) { $found = $calendar; @@ -358,7 +360,7 @@ class Manager implements IManager { // Drawback: attendees that have been deleted will still be able to update their partstat foreach ($calendars as $calendar) { // We should not search in writable calendars - if ($calendar instanceof ICreateFromString) { + if ($calendar instanceof IHandleImipMessage) { $o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]); if (!empty($o)) { $found = $calendar; diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index d34d9fb0087..aa317ec1720 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -101,6 +101,12 @@ class MailPlugin implements ISearchPlugin { return false; } + // Extract the email address from "Foo Bar <foo.bar@example.tld>" and then search with "foo.bar@example.tld" instead + $result = preg_match('/<([^@]+@.+)>$/', $search, $matches); + if ($result && filter_var($matches[1], FILTER_VALIDATE_EMAIL)) { + return $this->search($matches[1], $limit, $offset, $searchResult); + } + $currentUserId = $this->userSession->getUser()->getUID(); $result = $userResults = ['wide' => [], 'exact' => []]; diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index 53603e51e56..3eb9aab6817 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -605,7 +605,7 @@ class Manager implements ICommentsManager { public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array { $objectIds = []; if ($objectId) { - $objectIds[] = $objectIds; + $objectIds[] = $objectId; } return $this->searchForObjects($search, $objectType, $objectIds, $verb, $offset, $limit); } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 73e0f4b4ac2..ceaffbcfa9a 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -50,12 +50,13 @@ use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Statement; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Diagnostics\IEventLogger; use OCP\IRequestId; use OCP\PreConditionNotMetException; +use OCP\Profiler\IProfiler; use OC\DB\QueryBuilder\QueryBuilder; use OC\SystemConfig; use Psr\Log\LoggerInterface; -use OCP\Profiler\IProfiler; class Connection extends \Doctrine\DBAL\Connection { /** @var string */ @@ -124,12 +125,14 @@ class Connection extends \Doctrine\DBAL\Connection { public function connect() { try { if ($this->_conn) { + /** @psalm-suppress InternalMethod */ return parent::connect(); } // Only trigger the event logger for the initial connect call - $eventLogger = \OC::$server->getEventLogger(); + $eventLogger = \OC::$server->get(IEventLogger::class); $eventLogger->start('connect:db', 'db connection opened'); + /** @psalm-suppress InternalMethod */ $status = parent::connect(); $eventLogger->end('connect:db'); @@ -291,7 +294,7 @@ class Connection extends \Doctrine\DBAL\Connection { $sql = $this->adapter->fixupStatement($sql); $this->queriesExecuted++; $this->logQueryToFile($sql); - return parent::executeStatement($sql, $params, $types); + return (int)parent::executeStatement($sql, $params, $types); } protected function logQueryToFile(string $sql): void { @@ -390,7 +393,7 @@ class Connection extends \Doctrine\DBAL\Connection { return $insertQb->createNamedParameter($value, $this->getType($value)); }, array_merge($keys, $values)) ); - return $insertQb->execute(); + return $insertQb->executeStatement(); } catch (NotNullConstraintViolationException $e) { throw $e; } catch (ConstraintViolationException $e) { @@ -416,7 +419,7 @@ class Connection extends \Doctrine\DBAL\Connection { } } $updateQb->where($where); - $affected = $updateQb->execute(); + $affected = $updateQb->executeStatement(); if ($affected === 0 && !empty($updatePreconditionValues)) { throw new PreConditionNotMetException(); diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php index 5dc07be1d2b..97d91e1c100 100644 --- a/lib/private/DB/Migrator.php +++ b/lib/private/DB/Migrator.php @@ -41,7 +41,6 @@ use function preg_match; use OCP\EventDispatcher\IEventDispatcher; class Migrator { - /** @var Connection */ protected $connection; @@ -138,6 +137,7 @@ class Migrator { } } + /** @psalm-suppress InternalMethod */ $comparator = new Comparator(); return $comparator->compare($sourceSchema, $targetSchema); } diff --git a/lib/private/Dashboard/Manager.php b/lib/private/Dashboard/Manager.php index 2aeedf3174e..ba34219a615 100644 --- a/lib/private/Dashboard/Manager.php +++ b/lib/private/Dashboard/Manager.php @@ -28,6 +28,7 @@ namespace OC\Dashboard; use InvalidArgumentException; use OCP\App\IAppManager; +use OCP\Dashboard\IConditionalWidget; use OCP\Dashboard\IManager; use OCP\Dashboard\IWidget; use Psr\Container\ContainerExceptionInterface; @@ -82,15 +83,20 @@ class Manager implements IManager { * we can not inject it. Thus the static call. */ \OC::$server->get(LoggerInterface::class)->critical( - 'Could not load lazy dashboard widget: ' . $e->getMessage(), - ['excepiton' => $e] + 'Could not load lazy dashboard widget: ' . $service['class'], + ['exception' => $e] ); + continue; } /** * Try to register the loaded reporter. Theoretically it could be of a wrong * type, so we might get a TypeError here that we should catch. */ try { + if ($widget instanceof IConditionalWidget && !$widget->isEnabled()) { + continue; + } + $this->registerWidget($widget); } catch (Throwable $e) { /* @@ -98,9 +104,10 @@ class Manager implements IManager { * we can not inject it. Thus the static call. */ \OC::$server->get(LoggerInterface::class)->critical( - 'Could not register lazy dashboard widget: ' . $e->getMessage(), + 'Could not register lazy dashboard widget: ' . $service['class'], ['exception' => $e] ); + continue; } try { @@ -119,9 +126,10 @@ class Manager implements IManager { } } catch (Throwable $e) { \OC::$server->get(LoggerInterface::class)->critical( - 'Error during dashboard widget loading: ' . $e->getMessage(), + 'Error during dashboard widget loading: ' . $service['class'], ['exception' => $e] ); + continue; } } $this->lazyWidgets = []; diff --git a/lib/private/Federation/CloudIdManager.php b/lib/private/Federation/CloudIdManager.php index e4e42cb1293..85aae8e5ec5 100644 --- a/lib/private/Federation/CloudIdManager.php +++ b/lib/private/Federation/CloudIdManager.php @@ -125,6 +125,9 @@ class CloudIdManager implements ICloudIdManager { if ($lastValidAtPos !== false) { $user = substr($id, 0, $lastValidAtPos); $remote = substr($id, $lastValidAtPos + 1); + + $this->userManager->validateUserId($user); + if (!empty($user) && !empty($remote)) { return new CloudId($id, $user, $remote, $this->getDisplayNameFromContact($id)); } diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index f77c9b71dd7..01fc638cef8 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -239,7 +239,7 @@ class Storage { $query = $db->getQueryBuilder(); $query->delete('storages') - ->where($query->expr()->eq('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))); + ->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))); $query->executeStatement(); $query = $db->getQueryBuilder(); diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 268c1d8dd06..bf9ae3c148d 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -66,10 +66,11 @@ class Folder extends Node implements \OCP\Files\Folder { * @throws \OCP\Files\NotPermittedException */ public function getFullPath($path) { + $path = $this->normalizePath($path); if (!$this->isValidPath($path)) { throw new NotPermittedException('Invalid path'); } - return $this->path . $this->normalizePath($path); + return $this->path . $path; } /** @@ -371,12 +372,12 @@ class Folder extends Node implements \OCP\Files\Folder { return [$this->root->createNode( $absolutePath, new \OC\Files\FileInfo( - $absolutePath, - $mount->getStorage(), - $cacheEntry->getPath(), - $cacheEntry, - $mount - ))]; + $absolutePath, + $mount->getStorage(), + $cacheEntry->getPath(), + $cacheEntry, + $mount + ))]; } public function getFreeSpace() { diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php index bfa4d6861ea..8a752ff281d 100644 --- a/lib/private/Files/Node/Node.php +++ b/lib/private/Files/Node/Node.php @@ -99,10 +99,10 @@ class Node implements \OCP\Files\Node { * @throws NotFoundException */ public function getFileInfo() { - if (!Filesystem::isValidPath($this->path)) { - throw new InvalidPathException(); - } if (!$this->fileInfo) { + if (!Filesystem::isValidPath($this->path)) { + throw new InvalidPathException(); + } $fileInfo = $this->view->getFileInfo($this->path); if ($fileInfo instanceof FileInfo) { $this->fileInfo = $fileInfo; diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index b7044c2d894..02b65bf37f7 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -61,6 +61,9 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { private $logger; + /** @var bool */ + protected $validateWrites = true; + public function __construct($params) { if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) { $this->objectStore = $params['objectstore']; @@ -75,6 +78,9 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { if (isset($params['objectPrefix'])) { $this->objectPrefix = $params['objectPrefix']; } + if (isset($params['validateWrites'])) { + $this->validateWrites = (bool)$params['validateWrites']; + } //initialize cache with root directory in cache if (!$this->is_dir('/')) { $this->mkdir('/'); @@ -303,13 +309,23 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { case 'rb': $stat = $this->stat($path); if (is_array($stat)) { + $filesize = $stat['size'] ?? 0; // Reading 0 sized files is a waste of time - if (isset($stat['size']) && $stat['size'] === 0) { + if ($filesize === 0) { return fopen('php://memory', $mode); } try { - return $this->objectStore->readObject($this->getURN($stat['fileid'])); + $handle = $this->objectStore->readObject($this->getURN($stat['fileid'])); + if ($handle === false) { + return false; // keep backward compatibility + } + $streamStat = fstat($handle); + $actualSize = $streamStat['size'] ?? -1; + if ($actualSize > -1 && $actualSize !== $filesize) { + $this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]); + } + return $handle; } catch (NotFoundException $e) { $this->logger->logException($e, [ 'app' => 'objectstore', @@ -522,7 +538,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { if ($exists) { $this->getCache()->update($fileId, $stat); } else { - if ($this->objectStore->objectExists($urn)) { + if (!$this->validateWrites || $this->objectStore->objectExists($urn)) { $this->getCache()->move($uploadPath, $path); } else { $this->getCache()->remove($uploadPath); diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index a88ebbeda82..e6a2cf21cd0 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -29,9 +29,9 @@ namespace OC\Files\ObjectStore; use Aws\S3\Exception\S3MultipartUploadException; use Aws\S3\MultipartUploader; use Aws\S3\S3Client; +use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Utils; use OC\Files\Stream\SeekableHttpStream; -use GuzzleHttp\Psr7; use Psr\Http\Message\StreamInterface; trait S3ObjectTrait { @@ -47,6 +47,7 @@ trait S3ObjectTrait { /** * @param string $urn the unified resource name used to identify the object + * * @return resource stream with the read data * @throws \Exception when something goes wrong, message will be logged * @since 7.0.0 @@ -88,6 +89,7 @@ trait S3ObjectTrait { }); } + /** * Single object put helper * diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index d52a291cd99..ec5cd59d960 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -40,6 +40,7 @@ use OC_Util; use OCP\Constants; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IMountProvider; use OCP\Files\Config\IUserMountCache; @@ -414,9 +415,9 @@ class SetupManager { $mounts = []; if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { - $setupProviders[] = $cachedMount->getMountProvider(); $currentProviders[] = $cachedMount->getMountProvider(); if ($cachedMount->getMountProvider()) { + $setupProviders[] = $cachedMount->getMountProvider(); $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]); } else { $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); @@ -427,16 +428,21 @@ class SetupManager { if ($includeChildren) { $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path); - foreach ($subCachedMounts as $cachedMount) { - if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { - $setupProviders[] = $cachedMount->getMountProvider(); - $currentProviders[] = $cachedMount->getMountProvider(); - if ($cachedMount->getMountProvider()) { + + $needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) { + return $needsFullSetup || $cachedMountInfo->getMountProvider() === ''; + }, false); + + if ($needsFullSetup) { + $this->logger->debug("mount has no provider set, performing full setup"); + $this->setupForUser($user); + return; + } else { + foreach ($subCachedMounts as $cachedMount) { + if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { + $currentProviders[] = $cachedMount->getMountProvider(); + $setupProviders[] = $cachedMount->getMountProvider(); $mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()])); - } else { - $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); - $this->setupForUser($user); - return; } } } diff --git a/lib/private/Files/SimpleFS/NewSimpleFile.php b/lib/private/Files/SimpleFS/NewSimpleFile.php index b2a183b7d29..e2d1ae274b1 100644 --- a/lib/private/Files/SimpleFS/NewSimpleFile.php +++ b/lib/private/Files/SimpleFS/NewSimpleFile.php @@ -136,6 +136,10 @@ class NewSimpleFile implements ISimpleFile { * @throws NotFoundException */ private function checkFile(): void { + if (!$this->file) { + throw new NotFoundException('File not set'); + } + $cur = $this->file; while ($cur->stat() === false) { diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index ff157bfe7f6..9daa3517825 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -170,11 +170,6 @@ class Local extends \OC\Files\Storage\Common { return false; } $statResult = @stat($fullPath); - if (PHP_INT_SIZE === 4 && $statResult && !$this->is_dir($path)) { - $filesize = $this->filesize($path); - $statResult['size'] = $filesize; - $statResult[7] = $filesize; - } if (is_array($statResult)) { $statResult['full_path'] = $fullPath; } @@ -246,10 +241,6 @@ class Local extends \OC\Files\Storage\Common { return 0; } $fullPath = $this->getSourcePath($path); - if (PHP_INT_SIZE === 4) { - $helper = new \OC\LargeFileHelper; - return $helper->getFileSize($fullPath); - } return filesize($fullPath); } @@ -271,10 +262,6 @@ class Local extends \OC\Files\Storage\Common { if (!$this->file_exists($path)) { return false; } - if (PHP_INT_SIZE === 4) { - $helper = new \OC\LargeFileHelper(); - return $helper->getFileMtime($fullPath); - } return filemtime($fullPath); } @@ -421,7 +408,7 @@ class Local extends \OC\Files\Storage\Common { if ($space === false || is_null($space)) { return \OCP\Files\FileInfo::SPACE_UNKNOWN; } - return $space; + return (int)$space; } public function search($query) { diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 0f1838c97c8..9cc8b238ee1 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -465,7 +465,7 @@ class Encryption extends Wrapper { $cacheEntry = $cache->get($this->internalPath); if ($cacheEntry) { $version = $cacheEntry['encryptedVersion'] + 1; - $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]); + $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize]); } } @@ -528,6 +528,7 @@ class Encryption extends Wrapper { */ protected function writeHeader() { $header = $this->util->createHeader($this->newHeader, $this->encryptionModule); + $this->fileUpdated = true; return parent::stream_write($header); } diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index df37fd29f42..51ccaeba998 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -75,8 +75,12 @@ class SeekableHttpStream implements File { /** @var ?resource|closed-resource */ private $current; + /** @var int $offset offset of the current chunk */ private int $offset = 0; + /** @var int $length length of the current chunk */ private int $length = 0; + /** @var int $totalSize size of the full stream */ + private int $totalSize = 0; private bool $needReconnect = false; private function reconnect(int $start): bool { @@ -128,6 +132,9 @@ class SeekableHttpStream implements File { $this->offset = $begin; $this->length = $length; + if ($start === 0) { + $this->totalSize = $length; + } return true; } @@ -211,7 +218,9 @@ class SeekableHttpStream implements File { public function stream_stat() { if ($this->getCurrent()) { - return fstat($this->getCurrent()); + $stat = fstat($this->getCurrent()); + $stat['size'] = $this->totalSize; + return $stat; } else { return false; } diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 0abc870dc6f..16a9381768b 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1191,13 +1191,13 @@ class View { throw $e; } - if ($result && in_array('delete', $hooks)) { + if ($result !== false && in_array('delete', $hooks)) { $this->removeUpdate($storage, $internalPath); } - if ($result && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') { + if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') { $this->writeUpdate($storage, $internalPath); } - if ($result && in_array('touch', $hooks)) { + if ($result !== false && in_array('touch', $hooks)) { $this->writeUpdate($storage, $internalPath, $extraParam); } diff --git a/lib/private/Group/DisplayNameCache.php b/lib/private/Group/DisplayNameCache.php new file mode 100644 index 00000000000..d724b6caf0e --- /dev/null +++ b/lib/private/Group/DisplayNameCache.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Anna Larch <anna.larch@gmx.net> + * @author Anna Larch <anna.larch@gmx.net> + * + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\Group; + +use OCP\Cache\CappedMemoryCache; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\GroupChangedEvent; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IGroupManager; + +/** + * Class that cache the relation Group ID -> Display name + * + * This saves fetching the group from the backend for "just" the display name + */ +class DisplayNameCache implements IEventListener { + private CappedMemoryCache $cache; + private ICache $memCache; + private IGroupManager $groupManager; + + public function __construct(ICacheFactory $cacheFactory, IGroupManager $groupManager) { + $this->cache = new CappedMemoryCache(); + $this->memCache = $cacheFactory->createDistributed('groupDisplayNameMappingCache'); + $this->groupManager = $groupManager; + } + + public function getDisplayName(string $groupId): ?string { + if (isset($this->cache[$groupId])) { + return $this->cache[$groupId]; + } + $displayName = $this->memCache->get($groupId); + if ($displayName) { + $this->cache[$groupId] = $displayName; + return $displayName; + } + + $group = $this->groupManager->get($groupId); + if ($group) { + $displayName = $group->getDisplayName(); + } else { + $displayName = null; + } + $this->cache[$groupId] = $displayName; + $this->memCache->set($groupId, $displayName, 60 * 10); // 10 minutes + + return $displayName; + } + + public function clear(): void { + $this->cache = new CappedMemoryCache(); + $this->memCache->clear(); + } + + public function handle(Event $event): void { + if ($event instanceof GroupChangedEvent && $event->getFeature() === 'displayName') { + $groupId = $event->getGroup()->getGID(); + $newDisplayName = $event->getValue(); + $this->cache[$groupId] = $newDisplayName; + $this->memCache->set($groupId, $newDisplayName, 60 * 10); // 10 minutes + } + } +} diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 2ef4d2ee23f..ae70a611e4e 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -38,6 +38,7 @@ use OCP\Group\Backend\IGetDisplayNameBackend; use OCP\Group\Backend\IHideFromCollaborationBackend; use OCP\Group\Backend\INamedBackend; use OCP\Group\Backend\ISetDisplayNameBackend; +use OCP\Group\Events\GroupChangedEvent; use OCP\GroupInterface; use OCP\IGroup; use OCP\IUser; @@ -112,6 +113,7 @@ class Group implements IGroup { if (($backend instanceof ISetDisplayNameBackend) && $backend->setDisplayName($this->gid, $displayName)) { $this->displayName = $displayName; + $this->dispatcher->dispatch(new GroupChangedEvent($this, 'displayName', $displayName, '')); return true; } } diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php index 28f7a400b41..b718afa5168 100644 --- a/lib/private/Group/Manager.php +++ b/lib/private/Group/Manager.php @@ -42,6 +42,7 @@ namespace OC\Group; use OC\Hooks\PublicEmitter; use OCP\EventDispatcher\IEventDispatcher; use OCP\GroupInterface; +use OCP\ICacheFactory; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; @@ -82,12 +83,16 @@ class Manager extends PublicEmitter implements IGroupManager { /** @var \OC\SubAdmin */ private $subAdmin = null; + private DisplayNameCache $displayNameCache; + public function __construct(\OC\User\Manager $userManager, EventDispatcherInterface $dispatcher, - LoggerInterface $logger) { + LoggerInterface $logger, + ICacheFactory $cacheFactory) { $this->userManager = $userManager; $this->dispatcher = $dispatcher; $this->logger = $logger; + $this->displayNameCache = new DisplayNameCache($cacheFactory, $this); $cachedGroups = &$this->cachedGroups; $cachedUserGroups = &$this->cachedUserGroups; @@ -339,6 +344,14 @@ class Manager extends PublicEmitter implements IGroupManager { } /** + * @param string $groupId + * @return ?string + */ + public function getDisplayName(string $groupId): ?string { + return $this->displayNameCache->getDisplayName($groupId); + } + + /** * get an array of groupid and displayName for a user * * @param IUser $user @@ -346,7 +359,7 @@ class Manager extends PublicEmitter implements IGroupManager { */ public function getUserGroupNames(IUser $user) { return array_map(function ($group) { - return ['displayName' => $group->getDisplayName()]; + return ['displayName' => $this->displayNameCache->getDisplayName($group->getGID())]; }, $this->getUserGroups($user)); } diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php index 4bf7fd02400..2e370395132 100644 --- a/lib/private/Http/Client/Client.php +++ b/lib/private/Http/Client/Client.php @@ -37,8 +37,11 @@ use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\RequestOptions; use OCP\Http\Client\IClient; use OCP\Http\Client\IResponse; +use OCP\Http\Client\LocalServerException; use OCP\ICertificateManager; use OCP\IConfig; +use OCP\Security\IRemoteHostValidator; +use function parse_url; /** * Class Client @@ -52,19 +55,18 @@ class Client implements IClient { private $config; /** @var ICertificateManager */ private $certificateManager; - /** @var LocalAddressChecker */ - private $localAddressChecker; + private IRemoteHostValidator $remoteHostValidator; public function __construct( IConfig $config, ICertificateManager $certificateManager, GuzzleClient $client, - LocalAddressChecker $localAddressChecker + IRemoteHostValidator $remoteHostValidator ) { $this->config = $config; $this->client = $client; $this->certificateManager = $certificateManager; - $this->localAddressChecker = $localAddressChecker; + $this->remoteHostValidator = $remoteHostValidator; } private function buildRequestOptions(array $options): array { @@ -181,7 +183,13 @@ class Client implements IClient { return; } - $this->localAddressChecker->ThrowIfLocalAddress($uri); + $host = parse_url($uri, PHP_URL_HOST); + if ($host === false || $host === null) { + throw new LocalServerException('Could not detect any host'); + } + if (!$this->remoteHostValidator->isValid($host)) { + throw new LocalServerException('Host violates local access rules'); + } } /** diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php index e868d4af7a5..bbc2330176f 100644 --- a/lib/private/Http/Client/ClientService.php +++ b/lib/private/Http/Client/ClientService.php @@ -33,6 +33,7 @@ use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\ICertificateManager; use OCP\IConfig; +use OCP\Security\IRemoteHostValidator; /** * Class ClientService @@ -46,17 +47,16 @@ class ClientService implements IClientService { private $certificateManager; /** @var DnsPinMiddleware */ private $dnsPinMiddleware; - /** @var LocalAddressChecker */ - private $localAddressChecker; + private IRemoteHostValidator $remoteHostValidator; public function __construct(IConfig $config, ICertificateManager $certificateManager, DnsPinMiddleware $dnsPinMiddleware, - LocalAddressChecker $localAddressChecker) { + IRemoteHostValidator $remoteHostValidator) { $this->config = $config; $this->certificateManager = $certificateManager; $this->dnsPinMiddleware = $dnsPinMiddleware; - $this->localAddressChecker = $localAddressChecker; + $this->remoteHostValidator = $remoteHostValidator; } /** @@ -73,7 +73,7 @@ class ClientService implements IClientService { $this->config, $this->certificateManager, $client, - $this->localAddressChecker + $this->remoteHostValidator, ); } } diff --git a/lib/private/Http/Client/DnsPinMiddleware.php b/lib/private/Http/Client/DnsPinMiddleware.php index f5e6214a4ab..c6a58972fdd 100644 --- a/lib/private/Http/Client/DnsPinMiddleware.php +++ b/lib/private/Http/Client/DnsPinMiddleware.php @@ -25,20 +25,21 @@ declare(strict_types=1); */ namespace OC\Http\Client; +use OC\Net\IpAddressClassifier; +use OCP\Http\Client\LocalServerException; use Psr\Http\Message\RequestInterface; class DnsPinMiddleware { /** @var NegativeDnsCache */ private $negativeDnsCache; - /** @var LocalAddressChecker */ - private $localAddressChecker; + private IpAddressClassifier $ipAddressClassifier; public function __construct( NegativeDnsCache $negativeDnsCache, - LocalAddressChecker $localAddressChecker + IpAddressClassifier $ipAddressClassifier ) { $this->negativeDnsCache = $negativeDnsCache; - $this->localAddressChecker = $localAddressChecker; + $this->ipAddressClassifier = $ipAddressClassifier; } /** @@ -133,7 +134,10 @@ class DnsPinMiddleware { $curlResolves["$hostName:$port"] = []; foreach ($targetIps as $ip) { - $this->localAddressChecker->ThrowIfLocalIp($ip); + if ($this->ipAddressClassifier->isLocalAddress($ip)) { + // TODO: continue with all non-local IPs? + throw new LocalServerException('Host violates local access rules'); + } $curlResolves["$hostName:$port"][] = $ip; } } diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php deleted file mode 100644 index 13a7d062de3..00000000000 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OC\Http\Client; - -use IPLib\Address\IPv6; -use IPLib\Factory; -use IPLib\ParseStringFlag; -use OCP\Http\Client\LocalServerException; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\IpUtils; - -class LocalAddressChecker { - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; - } - - public function ThrowIfLocalIp(string $ip) : void { - $parsedIp = Factory::parseAddressString( - $ip, - ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED - ); - if ($parsedIp === null) { - /* Not an IP */ - return; - } - /* Replace by normalized form */ - if ($parsedIp instanceof IPv6) { - $ip = (string)($parsedIp->toIPv4() ?? $parsedIp); - } else { - $ip = (string)$parsedIp; - } - - $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'); - } - } - - public function ThrowIfLocalAddress(string $uri) : void { - $host = parse_url($uri, PHP_URL_HOST); - if ($host === false || $host === null) { - $this->logger->warning("Could not detect any host in $uri"); - throw new LocalServerException('Could not detect any host'); - } - - $host = idn_to_utf8(strtolower(urldecode($host))); - // Remove brackets from IPv6 addresses - if (strpos($host, '[') === 0 && substr($host, -1) === ']') { - $host = substr($host, 1, -1); - } - - // 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'); - } - - // Disallow hostname only - if (substr_count($host, '.') === 0 && !(bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - $this->logger->warning("Host $host was not connected to because it violates local access rules"); - throw new LocalServerException('Host violates local access rules'); - } - - $this->ThrowIfLocalIp($host); - } -} diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 7fcfab37fa6..71910873db7 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -40,6 +40,8 @@ declare(strict_types=1); namespace OC\L10N; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IRequest; use OCP\IUser; @@ -94,7 +96,9 @@ class Factory implements IFactory { protected $request; /** @var IUserSession */ - protected $userSession; + protected IUserSession $userSession; + + private ICache $cache; /** @var string */ protected $serverRoot; @@ -109,11 +113,13 @@ class Factory implements IFactory { IConfig $config, IRequest $request, IUserSession $userSession, + ICacheFactory $cacheFactory, $serverRoot ) { $this->config = $config; $this->request = $request; $this->userSession = $userSession; + $this->cache = $cacheFactory->createLocal('L10NFactory'); $this->serverRoot = $serverRoot; } @@ -338,6 +344,10 @@ class Factory implements IFactory { $key = 'null'; } + if ($availableLanguages = $this->cache->get($key)) { + $this->availableLanguages[$key] = $availableLanguages; + } + // also works with null as key if (!empty($this->availableLanguages[$key])) { return $this->availableLanguages[$key]; @@ -374,6 +384,7 @@ class Factory implements IFactory { } $this->availableLanguages[$key] = $available; + $this->cache->set($key, $available, 60); return $available; } diff --git a/lib/private/Log/ErrorHandler.php b/lib/private/Log/ErrorHandler.php index d56fecb1ecb..c4b9631e75a 100644 --- a/lib/private/Log/ErrorHandler.php +++ b/lib/private/Log/ErrorHandler.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * @@ -27,84 +30,75 @@ */ namespace OC\Log; +use Error; use OCP\ILogger; +use Psr\Log\LoggerInterface; +use Throwable; class ErrorHandler { - /** @var ILogger */ - private static $logger; + private LoggerInterface $logger; + + public function __construct(LoggerInterface $logger) { + $this->logger = $logger; + } /** - * remove password in URLs - * @param string $msg - * @return string + * Remove password in URLs */ - protected static function removePassword($msg) { + private static function removePassword(string $msg): string { return preg_replace('#//(.*):(.*)@#', '//xxx:xxx@', $msg); } - public static function register($debug = false) { - $handler = new ErrorHandler(); - - if ($debug) { - set_error_handler([$handler, 'onAll'], E_ALL); - if (\OC::$CLI) { - set_exception_handler(['OC_Template', 'printExceptionErrorPage']); - } - } else { - set_error_handler([$handler, 'onError']); - } - register_shutdown_function([$handler, 'onShutdown']); - set_exception_handler([$handler, 'onException']); - } - - public static function setLogger(ILogger $logger) { - self::$logger = $logger; - } - - //Fatal errors handler - public static function onShutdown() { + /** + * Fatal errors handler + */ + public function onShutdown(): void { $error = error_get_last(); - if ($error && self::$logger) { - //ob_end_clean(); + if ($error) { $msg = $error['message'] . ' at ' . $error['file'] . '#' . $error['line']; - self::$logger->critical(self::removePassword($msg), ['app' => 'PHP']); + $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']); } } /** - * Uncaught exception handler - * - * @param \Exception $exception + * Uncaught exception handler */ - public static function onException($exception) { + public function onException(Throwable $exception): void { $class = get_class($exception); $msg = $exception->getMessage(); $msg = "$class: $msg at " . $exception->getFile() . '#' . $exception->getLine(); - self::$logger->critical(self::removePassword($msg), ['app' => 'PHP']); + $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']); } - //Recoverable errors handler - public static function onError($number, $message, $file, $line) { + /** + * Recoverable errors handler + */ + public function onError(int $number, string $message, string $file, int $line): bool { if (!(error_reporting() & $number)) { - return; + return true; } $msg = $message . ' at ' . $file . '#' . $line; - $e = new \Error(self::removePassword($msg)); - self::$logger->logException($e, ['app' => 'PHP', 'level' => self::errnoToLogLevel($number)]); + $e = new Error(self::removePassword($msg)); + $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']); + return true; } - //Recoverable handler which catch all errors, warnings and notices - public static function onAll($number, $message, $file, $line) { + /** + * Recoverable handler which catch all errors, warnings and notices + */ + public function onAll(int $number, string $message, string $file, int $line): bool { $msg = $message . ' at ' . $file . '#' . $line; - $e = new \Error(self::removePassword($msg)); - self::$logger->logException($e, ['app' => 'PHP', 'level' => self::errnoToLogLevel($number)]); + $e = new Error(self::removePassword($msg)); + $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']); + return true; } - public static function errnoToLogLevel(int $errno): int { + private static function errnoToLogLevel(int $errno): int { switch ($errno) { case E_USER_WARNING: return ILogger::WARN; + case E_DEPRECATED: case E_USER_DEPRECATED: return ILogger::DEBUG; diff --git a/lib/private/Log/Errorlog.php b/lib/private/Log/Errorlog.php index d27759d7050..7fca04d8b34 100644 --- a/lib/private/Log/Errorlog.php +++ b/lib/private/Log/Errorlog.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * The MIT License (MIT) * @@ -32,7 +35,7 @@ class Errorlog implements IWriter { /** @var string */ protected $tag; - public function __construct(string $tag = 'owncloud') { + public function __construct(string $tag = 'nextcloud') { $this->tag = $tag; } diff --git a/lib/private/Mail/Message.php b/lib/private/Mail/Message.php index c1b08b4e231..8b94f44ddba 100644 --- a/lib/private/Mail/Message.php +++ b/lib/private/Mail/Message.php @@ -78,14 +78,18 @@ class Message implements IMessage { $convertedAddresses = []; foreach ($addresses as $email => $readableName) { - if (!is_numeric($email)) { - [$name, $domain] = explode('@', $email, 2); - $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); - $convertedAddresses[$name.'@'.$domain] = $readableName; + $parsableEmail = is_numeric($email) ? $readableName : $email; + if (strpos($parsableEmail, '@') === false) { + $convertedAddresses[$parsableEmail] = $readableName; + continue; + } + + [$name, $domain] = explode('@', $parsableEmail, 2); + $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); + if (is_numeric($email)) { + $convertedAddresses[] = $name . '@' . $domain; } else { - [$name, $domain] = explode('@', $readableName, 2); - $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); - $convertedAddresses[$email] = $name.'@'.$domain; + $convertedAddresses[$name . '@' . $domain] = $readableName; } } diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index de38033ca32..f4094e0bef6 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -33,20 +33,28 @@ use OCP\IMemcacheTTL; class Redis extends Cache implements IMemcacheTTL { /** - * @var \Redis $cache + * @var \Redis|\RedisCluster $cache */ private static $cache = null; public function __construct($prefix = '', string $logFile = '') { parent::__construct($prefix); + } + + /** + * @return \Redis|\RedisCluster|null + * @throws \Exception + */ + public function getCache() { if (is_null(self::$cache)) { self::$cache = \OC::$server->getGetRedisFactory()->getInstance(); } + return self::$cache; } public function get($key) { - $result = self::$cache->get($this->getPrefix() . $key); - if ($result === false && !self::$cache->exists($this->getPrefix() . $key)) { + $result = $this->getCache()->get($this->getPrefix() . $key); + if ($result === false && !$this->getCache()->exists($this->getPrefix() . $key)) { return null; } else { return json_decode($result, true); @@ -55,18 +63,18 @@ class Redis extends Cache implements IMemcacheTTL { public function set($key, $value, $ttl = 0) { if ($ttl > 0) { - return self::$cache->setex($this->getPrefix() . $key, $ttl, json_encode($value)); + return $this->getCache()->setex($this->getPrefix() . $key, $ttl, json_encode($value)); } else { - return self::$cache->set($this->getPrefix() . $key, json_encode($value)); + return $this->getCache()->set($this->getPrefix() . $key, json_encode($value)); } } public function hasKey($key) { - return (bool)self::$cache->exists($this->getPrefix() . $key); + return (bool)$this->getCache()->exists($this->getPrefix() . $key); } public function remove($key) { - if (self::$cache->del($this->getPrefix() . $key)) { + if ($this->getCache()->del($this->getPrefix() . $key)) { return true; } else { return false; @@ -75,8 +83,8 @@ class Redis extends Cache implements IMemcacheTTL { public function clear($prefix = '') { $prefix = $this->getPrefix() . $prefix . '*'; - $keys = self::$cache->keys($prefix); - $deleted = self::$cache->del($keys); + $keys = $this->getCache()->keys($prefix); + $deleted = $this->getCache()->del($keys); return (is_array($keys) && (count($keys) === $deleted)); } @@ -100,7 +108,7 @@ class Redis extends Cache implements IMemcacheTTL { $args['ex'] = $ttl; } - return self::$cache->set($this->getPrefix() . $key, $value, $args); + return $this->getCache()->set($this->getPrefix() . $key, (string)$value, $args); } /** @@ -111,7 +119,7 @@ class Redis extends Cache implements IMemcacheTTL { * @return int | bool */ public function inc($key, $step = 1) { - return self::$cache->incrBy($this->getPrefix() . $key, $step); + return $this->getCache()->incrBy($this->getPrefix() . $key, $step); } /** @@ -125,7 +133,7 @@ class Redis extends Cache implements IMemcacheTTL { if (!$this->hasKey($key)) { return false; } - return self::$cache->decrBy($this->getPrefix() . $key, $step); + return $this->getCache()->decrBy($this->getPrefix() . $key, $step); } /** @@ -140,14 +148,14 @@ class Redis extends Cache implements IMemcacheTTL { if (!is_int($new)) { $new = json_encode($new); } - self::$cache->watch($this->getPrefix() . $key); + $this->getCache()->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { - $result = self::$cache->multi() + $result = $this->getCache()->multi() ->set($this->getPrefix() . $key, $new) ->exec(); return $result !== false; } - self::$cache->unwatch(); + $this->getCache()->unwatch(); return false; } @@ -159,19 +167,19 @@ class Redis extends Cache implements IMemcacheTTL { * @return bool */ public function cad($key, $old) { - self::$cache->watch($this->getPrefix() . $key); + $this->getCache()->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { - $result = self::$cache->multi() + $result = $this->getCache()->multi() ->del($this->getPrefix() . $key) ->exec(); return $result !== false; } - self::$cache->unwatch(); + $this->getCache()->unwatch(); return false; } public function setTTL($key, $ttl) { - self::$cache->expire($this->getPrefix() . $key, $ttl); + $this->getCache()->expire($this->getPrefix() . $key, $ttl); } public static function isAvailable(): bool { diff --git a/lib/private/Metadata/FileMetadata.php b/lib/private/Metadata/FileMetadata.php index 7d1db21cf39..9ad0f9d35c6 100644 --- a/lib/private/Metadata/FileMetadata.php +++ b/lib/private/Metadata/FileMetadata.php @@ -28,7 +28,7 @@ use OCP\DB\Types; /** * @method string getGroupName() * @method void setGroupName(string $groupName) - * @method string getMetadata() + * @method array getMetadata() * @method void setMetadata(array $metadata) * @see \OC\Core\Migrations\Version240000Date20220404230027 */ diff --git a/lib/private/NavigationManager.php b/lib/private/NavigationManager.php index 7e162e65a5d..b78d9fa1ed8 100644 --- a/lib/private/NavigationManager.php +++ b/lib/private/NavigationManager.php @@ -193,7 +193,7 @@ class NavigationManager implements INavigationManager { $this->add([ 'type' => 'settings', 'id' => 'help', - 'order' => 6, + 'order' => 99998, 'href' => $this->urlGenerator->linkToRoute('settings.Help.help'), 'name' => $l->t('Help'), 'icon' => $this->urlGenerator->imagePath('settings', 'help.svg'), diff --git a/lib/private/Net/HostnameClassifier.php b/lib/private/Net/HostnameClassifier.php new file mode 100644 index 00000000000..626aa47083e --- /dev/null +++ b/lib/private/Net/HostnameClassifier.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Net; + +use function filter_var; +use function in_array; +use function strrchr; +use function substr; +use function substr_count; + +/** + * Classifier for network hostnames + * + * @internal + */ +class HostnameClassifier { + private const LOCAL_TOPLEVEL_DOMAINS = [ + 'local', + 'localhost', + 'intranet', + 'internal', + 'private', + 'corp', + 'home', + 'lan', + ]; + + /** + * Check host identifier for local hostname + * + * IP addresses are not considered local. Use the IpAddressClassifier for those. + * + * @param string $hostname + * + * @return bool + */ + public function isLocalHostname(string $hostname): bool { + // Disallow local network top-level domains from RFC 6762 + $topLevelDomain = substr((strrchr($hostname, '.') ?: ''), 1); + if (in_array($topLevelDomain, self::LOCAL_TOPLEVEL_DOMAINS)) { + return true; + } + + // Disallow hostname only + if (substr_count($hostname, '.') === 0 && !filter_var($hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return true; + } + + return false; + } +} diff --git a/lib/private/Net/IpAddressClassifier.php b/lib/private/Net/IpAddressClassifier.php new file mode 100644 index 00000000000..d4698864ec8 --- /dev/null +++ b/lib/private/Net/IpAddressClassifier.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Net; + +use IPLib\Address\IPv6; +use IPLib\Factory; +use IPLib\ParseStringFlag; +use Symfony\Component\HttpFoundation\IpUtils; +use function filter_var; + +/** + * Classifier for IP addresses + * + * @internal + */ +class IpAddressClassifier { + private const LOCAL_ADDRESS_RANGES = [ + '100.64.0.0/10', // See RFC 6598 + '192.0.0.0/24', // See RFC 6890 + ]; + + /** + * Check host identifier for local IPv4 and IPv6 address ranges + * + * Hostnames are not considered local. Use the HostnameClassifier for those. + * + * @param string $ip + * + * @return bool + */ + public function isLocalAddress(string $ip): bool { + $parsedIp = Factory::parseAddressString( + $ip, + ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED + ); + if ($parsedIp === null) { + /* Not an IP */ + return false; + } + /* Replace by normalized form */ + if ($parsedIp instanceof IPv6) { + $ip = (string)($parsedIp->toIPv4() ?? $parsedIp); + } else { + $ip = (string)$parsedIp; + } + + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + /* Range address */ + return true; + } + if (IpUtils::checkIp($ip, self::LOCAL_ADDRESS_RANGES)) { + /* Within local range */ + return true; + } + + return false; + } +} diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php index d758cae428f..3d77f643d93 100644 --- a/lib/private/Notification/Manager.php +++ b/lib/private/Notification/Manager.php @@ -305,7 +305,7 @@ class Manager implements IManager { * users overload our infrastructure. For this reason we have to rate-limit the * use of push notifications. If you need this feature, consider using Nextcloud Enterprise. */ - $isFairUse = $this->subscription->delegateHasValidSubscription() || $this->userManager->countSeenUsers() < 500; + $isFairUse = $this->subscription->delegateHasValidSubscription() || $this->userManager->countSeenUsers() < 1000; $pushAllowed = $isFairUse ? 'yes' : 'no'; $this->cache->set('push_fair_use', $pushAllowed, 3600); } diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php index ab40aeaaa79..9a493384f11 100644 --- a/lib/private/Preview/BackgroundCleanupJob.php +++ b/lib/private/Preview/BackgroundCleanupJob.php @@ -25,8 +25,9 @@ declare(strict_types=1); */ namespace OC\Preview; -use OC\BackgroundJob\TimedJob; use OC\Preview\Storage\Root; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\IMimeTypeLoader; use OCP\Files\NotFoundException; @@ -34,7 +35,6 @@ use OCP\Files\NotPermittedException; use OCP\IDBConnection; class BackgroundCleanupJob extends TimedJob { - /** @var IDBConnection */ private $connection; @@ -47,10 +47,12 @@ class BackgroundCleanupJob extends TimedJob { /** @var IMimeTypeLoader */ private $mimeTypeLoader; - public function __construct(IDBConnection $connection, + public function __construct(ITimeFactory $timeFactory, + IDBConnection $connection, Root $previewFolder, IMimeTypeLoader $mimeTypeLoader, bool $isCLI) { + parent::__construct($timeFactory); // Run at most once an hour $this->setInterval(3600); diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index ef44188da74..7d2408d683f 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -29,6 +29,7 @@ */ namespace OC\Preview; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IAppData; use OCP\Files\InvalidPathException; @@ -40,12 +41,15 @@ use OCP\IConfig; use OCP\IImage; use OCP\IPreview; use OCP\IStreamImage; +use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Preview\IProviderV2; use OCP\Preview\IVersionedPreviewFile; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; class Generator { + public const SEMAPHORE_ID_ALL = 0x0a11; + public const SEMAPHORE_ID_NEW = 0x07ea; /** @var IPreview */ private $previewManager; @@ -56,26 +60,23 @@ class Generator { /** @var GeneratorHelper */ private $helper; /** @var EventDispatcherInterface */ + private $legacyEventDispatcher; + /** @var IEventDispatcher */ private $eventDispatcher; - /** - * @param IConfig $config - * @param IPreview $previewManager - * @param IAppData $appData - * @param GeneratorHelper $helper - * @param EventDispatcherInterface $eventDispatcher - */ public function __construct( IConfig $config, IPreview $previewManager, IAppData $appData, GeneratorHelper $helper, - EventDispatcherInterface $eventDispatcher + EventDispatcherInterface $legacyEventDispatcher, + IEventDispatcher $eventDispatcher ) { $this->config = $config; $this->previewManager = $previewManager; $this->appData = $appData; $this->helper = $helper; + $this->legacyEventDispatcher = $legacyEventDispatcher; $this->eventDispatcher = $eventDispatcher; } @@ -102,10 +103,14 @@ class Generator { 'crop' => $crop, 'mode' => $mode, ]; - $this->eventDispatcher->dispatch( + + $this->legacyEventDispatcher->dispatch( IPreview::EVENT, new GenericEvent($file, $specification) ); + $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent( + $file + )); // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want return $this->generatePreviews($file, [$specification], $mimeType); @@ -300,6 +305,98 @@ class Generator { } /** + * Acquire a semaphore of the specified id and concurrency, blocking if necessary. + * Return an identifier of the semaphore on success, which can be used to release it via + * {@see Generator::unguardWithSemaphore()}. + * + * @param int $semId + * @param int $concurrency + * @return false|resource the semaphore on success or false on failure + */ + public static function guardWithSemaphore(int $semId, int $concurrency) { + if (!extension_loaded('sysvsem')) { + return false; + } + $sem = sem_get($semId, $concurrency); + if ($sem === false) { + return false; + } + if (!sem_acquire($sem)) { + return false; + } + return $sem; + } + + /** + * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}. + * + * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore + * @return bool + */ + public static function unguardWithSemaphore($semId): bool { + if (!is_resource($semId) || !extension_loaded('sysvsem')) { + return false; + } + return sem_release($semId); + } + + /** + * Get the number of concurrent threads supported by the host. + * + * @return int number of concurrent threads, or 0 if it cannot be determined + */ + public static function getHardwareConcurrency(): int { + static $width; + if (!isset($width)) { + if (is_file("/proc/cpuinfo")) { + $width = substr_count(file_get_contents("/proc/cpuinfo"), "processor"); + } else { + $width = 0; + } + } + return $width; + } + + /** + * Get number of concurrent preview generations from system config + * + * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`, + * are available. If not set, the default values are determined with the hardware concurrency + * of the host. In case the hardware concurrency cannot be determined, or the user sets an + * invalid value, fallback values are: + * For new images whose previews do not exist and need to be generated, 4; + * For all preview generation requests, 8. + * Value of `preview_concurrency_all` should be greater than or equal to that of + * `preview_concurrency_new`, otherwise, the latter is returned. + * + * @param string $type either `preview_concurrency_new` or `preview_concurrency_all` + * @return int number of concurrent preview generations, or -1 if $type is invalid + */ + public function getNumConcurrentPreviews(string $type): int { + static $cached = array(); + if (array_key_exists($type, $cached)) { + return $cached[$type]; + } + + $hardwareConcurrency = self::getHardwareConcurrency(); + switch ($type) { + case "preview_concurrency_all": + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; + $concurrency_all = $this->config->getSystemValueInt($type, $fallback); + $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new"); + $cached[$type] = max($concurrency_all, $concurrency_new); + break; + case "preview_concurrency_new": + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; + $cached[$type] = $this->config->getSystemValueInt($type, $fallback); + break; + default: + return -1; + } + return $cached[$type]; + } + + /** * @param ISimpleFolder $previewFolder * @param File $file * @param string $mimeType @@ -337,7 +434,13 @@ class Generator { $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); - $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight); + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + try { + $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight); + } finally { + self::unguardWithSemaphore($sem); + } if (!($preview instanceof IImage)) { continue; @@ -507,29 +610,34 @@ class Generator { throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); } - if ($crop) { - if ($height !== $preview->height() && $width !== $preview->width()) { - //Resize - $widthR = $preview->width() / $width; - $heightR = $preview->height() / $height; - - if ($widthR > $heightR) { - $scaleH = $height; - $scaleW = $maxWidth / $heightR; - } else { - $scaleH = $maxHeight / $widthR; - $scaleW = $width; + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + try { + if ($crop) { + if ($height !== $preview->height() && $width !== $preview->width()) { + //Resize + $widthR = $preview->width() / $width; + $heightR = $preview->height() / $height; + + if ($widthR > $heightR) { + $scaleH = $height; + $scaleW = $maxWidth / $heightR; + } else { + $scaleH = $maxHeight / $widthR; + $scaleW = $width; + } + $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); } - $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); + $cropX = (int)floor(abs($width - $preview->width()) * 0.5); + $cropY = (int)floor(abs($height - $preview->height()) * 0.5); + $preview = $preview->cropCopy($cropX, $cropY, $width, $height); + } else { + $preview = $maxPreview->resizeCopy(max($width, $height)); } - $cropX = (int)floor(abs($width - $preview->width()) * 0.5); - $cropY = (int)floor(abs($height - $preview->height()) * 0.5); - $preview = $preview->cropCopy($cropX, $cropY, $width, $height); - } else { - $preview = $maxPreview->resizeCopy(max($width, $height)); + } finally { + self::unguardWithSemaphore($sem); } - $path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix); try { $file = $previewFolder->newFile($path); diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index ec200defce8..71df98f9ac6 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -115,6 +115,9 @@ class HEIC extends ProviderV2 { // Layer 0 contains either the bitmap or a flat representation of all vector layers $bp->readImage($tmpPath . '[0]'); + // Fix orientation from EXIF + $bp->autoOrient(); + $bp->setImageFormat('jpg'); $bp = $this->resize($bp, $maxX, $maxY); diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index 4da88f1ab26..e78b9b441f6 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -27,6 +27,7 @@ use OCP\Files\File; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IImage; +use OCP\Image; use OC\StreamImage; use Psr\Log\LoggerInterface; @@ -89,6 +90,8 @@ class Imaginary extends ProviderV2 { $mimeType = 'jpeg'; } + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + $operations = [ [ 'operation' => 'autorotate', @@ -101,6 +104,7 @@ class Imaginary extends ProviderV2 { 'stripmeta' => 'true', 'type' => $mimeType, 'norotation' => 'true', + 'quality' => $quality, ] ] ]; @@ -126,12 +130,21 @@ class Imaginary extends ProviderV2 { return null; } - if ($response->getHeader('X-Image-Width') && $response->getHeader('X-Image-Height')) { - $maxX = (int)$response->getHeader('X-Image-Width'); - $maxY = (int)$response->getHeader('X-Image-Height'); + // This is not optimal but previews are distorted if the wrong width and height values are + // used. Both dimension headers are only sent when passing the option "-return-size" to + // Imaginary. + if ($response->getHeader('Image-Width') && $response->getHeader('Image-Height')) { + $image = new StreamImage( + $response->getBody(), + $response->getHeader('Content-Type'), + (int)$response->getHeader('Image-Width'), + (int)$response->getHeader('Image-Height'), + ); + } else { + $image = new Image(); + $image->loadFromFileHandle($response->getBody()); } - $image = new StreamImage($response->getBody(), $response->getHeader('Content-Type'), $maxX, $maxY); return $image->valid() ? $image : null; } diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index a2d34892c68..87e709e9bcc 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -34,6 +34,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\Preview\Generator; use OC\Preview\GeneratorHelper; use OCP\AppFramework\QueryException; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IAppData; use OCP\Files\IRootFolder; @@ -51,7 +52,8 @@ class PreviewManager implements IPreview { protected IConfig $config; protected IRootFolder $rootFolder; protected IAppData $appData; - protected EventDispatcherInterface $eventDispatcher; + protected IEventDispatcher $eventDispatcher; + protected EventDispatcherInterface $legacyEventDispatcher; private ?Generator $generator = null; private GeneratorHelper $helper; protected bool $providerListDirty = false; @@ -73,20 +75,22 @@ class PreviewManager implements IPreview { private IBinaryFinder $binaryFinder; public function __construct( - IConfig $config, - IRootFolder $rootFolder, - IAppData $appData, - EventDispatcherInterface $eventDispatcher, - GeneratorHelper $helper, - ?string $userId, - Coordinator $bootstrapCoordinator, - IServerContainer $container, - IBinaryFinder $binaryFinder + IConfig $config, + IRootFolder $rootFolder, + IAppData $appData, + IEventDispatcher $eventDispatcher, + EventDispatcherInterface $legacyEventDispatcher, + GeneratorHelper $helper, + ?string $userId, + Coordinator $bootstrapCoordinator, + IServerContainer $container, + IBinaryFinder $binaryFinder ) { $this->config = $config; $this->rootFolder = $rootFolder; $this->appData = $appData; $this->eventDispatcher = $eventDispatcher; + $this->legacyEventDispatcher = $legacyEventDispatcher; $this->helper = $helper; $this->userId = $userId; $this->bootstrapCoordinator = $bootstrapCoordinator; @@ -153,6 +157,7 @@ class PreviewManager implements IPreview { $this->rootFolder, $this->config ), + $this->legacyEventDispatcher, $this->eventDispatcher ); } @@ -177,7 +182,15 @@ class PreviewManager implements IPreview { * @since 11.0.0 - \InvalidArgumentException was added in 12.0.0 */ public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) { - return $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType); + $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all'); + $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency); + try { + $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType); + } finally { + Generator::unguardWithSemaphore($sem); + } + + return $preview; } /** diff --git a/lib/private/Profile/Actions/FediverseAction.php b/lib/private/Profile/Actions/FediverseAction.php new file mode 100644 index 00000000000..ed3fcd80b52 --- /dev/null +++ b/lib/private/Profile/Actions/FediverseAction.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2021 Christopher Ng <chrng8@gmail.com> + * + * @author Christopher Ng <chrng8@gmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profile\Actions; + +use function Safe\substr; +use OCP\Accounts\IAccountManager; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Profile\ILinkAction; + +class FediverseAction implements ILinkAction { + private ?string $value = null; + private IAccountManager $accountManager; + private IFactory $l10nFactory; + private IURLGenerator $urlGenerator; + + public function __construct( + IAccountManager $accountManager, + IFactory $l10nFactory, + IURLGenerator $urlGenerator + ) { + $this->accountManager = $accountManager; + $this->l10nFactory = $l10nFactory; + $this->urlGenerator = $urlGenerator; + } + + public function preload(IUser $targetUser): void { + $account = $this->accountManager->getAccount($targetUser); + $this->value = $account->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(); + } + + public function getAppId(): string { + return 'core'; + } + + public function getId(): string { + return IAccountManager::PROPERTY_FEDIVERSE; + } + + public function getDisplayId(): string { + return $this->l10nFactory->get('lib')->t('Fediverse'); + } + + public function getTitle(): string { + $displayUsername = $this->value[0] === '@' ? $this->value : '@' . $this->value; + return $this->l10nFactory->get('lib')->t('View %s on the fediverse', [$displayUsername]); + } + + public function getPriority(): int { + return 50; + } + + public function getIcon(): string { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'actions/mastodon.svg')); + } + + public function getTarget(): ?string { + if (empty($this->value)) { + return null; + } + $username = $this->value[0] === '@' ? substr($this->value, 1) : $this->value; + [$username, $instance] = explode('@', $username); + return 'https://' . $instance . '/@' . $username; + } +} diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php index f038986cf6d..ab1af1c1c16 100644 --- a/lib/private/Profile/ProfileManager.php +++ b/lib/private/Profile/ProfileManager.php @@ -35,6 +35,7 @@ use OC\KnownUser\KnownUserService; use OC\Profile\Actions\EmailAction; use OC\Profile\Actions\PhoneAction; use OC\Profile\Actions\TwitterAction; +use OC\Profile\Actions\FediverseAction; use OC\Profile\Actions\WebsiteAction; use OCP\Accounts\IAccountManager; use OCP\Accounts\PropertyDoesNotExistException; @@ -95,6 +96,7 @@ class ProfileManager { PhoneAction::class, WebsiteAction::class, TwitterAction::class, + FediverseAction::class, ]; /** diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 5f4747721ca..9ca3ece6dd4 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -71,6 +71,7 @@ use OC\Repair\NC21\AddCheckForUserCertificatesJob; use OC\Repair\NC21\ValidatePhoneNumber; use OC\Repair\NC22\LookupServerSendCheck; use OC\Repair\NC24\AddTokenCleanupJob; +use OC\Repair\NC25\AddMissingSecretJob; use OC\Repair\OldGroupMembershipShares; use OC\Repair\Owncloud\CleanPreviews; use OC\Repair\Owncloud\DropAccountTermsTable; @@ -209,6 +210,7 @@ class Repair implements IOutput { \OCP\Server::get(LookupServerSendCheck::class), \OCP\Server::get(AddTokenCleanupJob::class), \OCP\Server::get(CleanUpAbandonedApps::class), + \OCP\Server::get(AddMissingSecretJob::class), ]; } diff --git a/lib/private/Repair/NC25/AddMissingSecretJob.php b/lib/private/Repair/NC25/AddMissingSecretJob.php new file mode 100644 index 00000000000..1194fe3f6ab --- /dev/null +++ b/lib/private/Repair/NC25/AddMissingSecretJob.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Repair\NC25; + +use OCP\HintException; +use OCP\IConfig; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use OCP\Security\ISecureRandom; + +class AddMissingSecretJob implements IRepairStep { + private IConfig $config; + private ISecureRandom $random; + + public function __construct(IConfig $config, ISecureRandom $random) { + $this->config = $config; + $this->random = $random; + } + + public function getName(): string { + return 'Add possibly missing system config'; + } + + public function run(IOutput $output): void { + $passwordSalt = $this->config->getSystemValue('passwordsalt', null); + if ($passwordSalt === null || $passwordSalt === '') { + try { + $this->config->setSystemValue('passwordsalt', $this->random->generate(30)); + } catch (HintException $e) { + $output->warning("passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually."); + } + } + + $secret = $this->config->getSystemValue('secret', null); + if ($secret === null || $secret === '') { + try { + $this->config->setSystemValue('secret', $this->random->generate(48)); + } catch (HintException $e) { + $output->warning("secret is missing from your config.php and your config.php is read only. Please fix it manually."); + } + } + } +} diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php index 5b216331dc7..5f3531ea79c 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -106,6 +106,15 @@ class RepairMimeTypes implements IRepairStep { return $count; } + private function introduceAsciidocType() { + $updatedMimetypes = [ + 'adoc' => 'text/asciidoc', + 'asciidoc' => 'text/asciidoc', + ]; + + return $this->updateMimetypes($updatedMimetypes); + } + private function introduceImageTypes() { $updatedMimetypes = [ 'jp2' => 'image/jp2', @@ -273,5 +282,9 @@ class RepairMimeTypes implements IRepairStep { if (version_compare($ocVersionFromBeforeUpdate, '25.0.0.2', '<') && $this->introduceOnlyofficeFormType()) { $out->info('Fixed ONLYOFFICE Forms OpenXML mime types'); } + + if (version_compare($ocVersionFromBeforeUpdate, '26.0.0.1', '<') && $this->introduceAsciidocType()) { + $out->info('Fixed AsciiDoc mime types'); + } } } diff --git a/lib/private/Security/RemoteHostValidator.php b/lib/private/Security/RemoteHostValidator.php new file mode 100644 index 00000000000..e48bd862472 --- /dev/null +++ b/lib/private/Security/RemoteHostValidator.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Security; + +use OC\Net\HostnameClassifier; +use OC\Net\IpAddressClassifier; +use OCP\IConfig; +use OCP\Security\IRemoteHostValidator; +use Psr\Log\LoggerInterface; +use function strpos; +use function strtolower; +use function substr; +use function urldecode; + +/** + * @internal + */ +final class RemoteHostValidator implements IRemoteHostValidator { + private IConfig $config; + private HostnameClassifier $hostnameClassifier; + private IpAddressClassifier $ipAddressClassifier; + private LoggerInterface $logger; + + public function __construct(IConfig $config, + HostnameClassifier $hostnameClassifier, + IpAddressClassifier $ipAddressClassifier, + LoggerInterface $logger) { + $this->config = $config; + $this->hostnameClassifier = $hostnameClassifier; + $this->ipAddressClassifier = $ipAddressClassifier; + $this->logger = $logger; + } + + public function isValid(string $host): bool { + if ($this->config->getSystemValueBool('allow_local_remote_servers', false)) { + return true; + } + + $host = idn_to_utf8(strtolower(urldecode($host))); + // Remove brackets from IPv6 addresses + if (strpos($host, '[') === 0 && substr($host, -1) === ']') { + $host = substr($host, 1, -1); + } + + if ($this->hostnameClassifier->isLocalHostname($host) + || $this->ipAddressClassifier->isLocalAddress($host)) { + $this->logger->warning("Host $host was not connected to because it violates local access rules"); + return false; + } + + return true; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index d804cca2086..d420b6fd11d 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -105,8 +105,6 @@ use OC\Files\Type\Loader; use OC\Files\View; use OC\FullTextSearch\FullTextSearchManager; use OC\Http\Client\ClientService; -use OC\Http\Client\DnsPinMiddleware; -use OC\Http\Client\LocalAddressChecker; use OC\Http\Client\NegativeDnsCache; use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Helpers\AppLocator; @@ -335,6 +333,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(IRootFolder::class), $c->get(SystemConfig::class) ), + $c->get(IEventDispatcher::class), $c->get(SymfonyAdapter::class), $c->get(GeneratorHelper::class), $c->get(ISession::class)->get('user_id'), @@ -488,7 +487,8 @@ class Server extends ServerContainer implements IServerContainer { $groupManager = new \OC\Group\Manager( $this->get(IUserManager::class), $c->get(SymfonyAdapter::class), - $this->get(LoggerInterface::class) + $this->get(LoggerInterface::class), + $this->get(ICacheFactory::class) ); $groupManager->listen('\OC\Group', 'preCreate', function ($gid) { /** @var IEventDispatcher $dispatcher */ @@ -688,6 +688,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(\OCP\IConfig::class), $c->getRequest(), $c->get(IUserSession::class), + $c->get(ICacheFactory::class), \OC::$SERVERROOT ); }); @@ -857,7 +858,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('SecureRandom', \OCP\Security\ISecureRandom::class); - + $this->registerAlias(\OCP\Security\IRemoteHostValidator::class, \OC\Security\RemoteHostValidator::class); $this->registerAlias(IVerificationToken::class, VerificationToken::class); $this->registerAlias(ICrypto::class, Crypto::class); @@ -889,22 +890,11 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(ICertificateManager::class, CertificateManager::class); $this->registerAlias(IClientService::class, ClientService::class); - $this->registerService(LocalAddressChecker::class, function (ContainerInterface $c) { - return new LocalAddressChecker( - $c->get(LoggerInterface::class), - ); - }); $this->registerService(NegativeDnsCache::class, function (ContainerInterface $c) { return new NegativeDnsCache( $c->get(ICacheFactory::class), ); }); - $this->registerService(DnsPinMiddleware::class, function (ContainerInterface $c) { - return new DnsPinMiddleware( - $c->get(NegativeDnsCache::class), - $c->get(LocalAddressChecker::class) - ); - }); $this->registerDeprecatedAlias('HttpClientService', IClientService::class); $this->registerService(IEventLogger::class, function (ContainerInterface $c) { return new EventLogger($c->get(SystemConfig::class), $c->get(LoggerInterface::class), $c->get(Log::class)); @@ -1221,21 +1211,22 @@ class Server extends ServerContainer implements IServerContainer { } if ($classExists && $c->get(\OCP\IConfig::class)->getSystemValue('installed', false) && $c->get(IAppManager::class)->isInstalled('theming') && $c->getTrustedDomainHelper()->isTrustedDomain($c->getRequest()->getInsecureServerHost())) { + $imageManager = new ImageManager( + $c->get(\OCP\IConfig::class), + $c->getAppDataDir('theming'), + $c->get(IURLGenerator::class), + $this->get(ICacheFactory::class), + $this->get(ILogger::class), + $this->get(ITempManager::class) + ); return new ThemingDefaults( $c->get(\OCP\IConfig::class), $c->getL10N('theming'), $c->get(IUserSession::class), $c->get(IURLGenerator::class), $c->get(ICacheFactory::class), - new Util($c->get(\OCP\IConfig::class), $this->get(IAppManager::class), $c->getAppDataDir('theming')), - new ImageManager( - $c->get(\OCP\IConfig::class), - $c->getAppDataDir('theming'), - $c->get(IURLGenerator::class), - $this->get(ICacheFactory::class), - $this->get(ILogger::class), - $this->get(ITempManager::class) - ), + new Util($c->get(\OCP\IConfig::class), $this->get(IAppManager::class), $c->getAppDataDir('theming'), $imageManager), + $imageManager, $c->get(IAppManager::class), $c->get(INavigationManager::class) ); diff --git a/lib/private/ServerContainer.php b/lib/private/ServerContainer.php index 0bc99f6c152..e53737990e8 100644 --- a/lib/private/ServerContainer.php +++ b/lib/private/ServerContainer.php @@ -139,10 +139,13 @@ class ServerContainer extends SimpleContainer { public function query(string $name, bool $autoload = true) { $name = $this->sanitizeName($name); - try { - return parent::query($name, false); - } catch (QueryException $e) { - // Continue with general autoloading then + if (str_starts_with($name, 'OCA\\')) { + // Skip server container query for app namespace classes + try { + return parent::query($name, false); + } catch (QueryException $e) { + // Continue with general autoloading then + } } // In case the service starts with OCA\ we try to find the service in diff --git a/lib/private/Session/CryptoSessionData.php b/lib/private/Session/CryptoSessionData.php index b01887e39e2..df810d5b30c 100644 --- a/lib/private/Session/CryptoSessionData.php +++ b/lib/private/Session/CryptoSessionData.php @@ -143,7 +143,6 @@ class CryptoSessionData implements \ArrayAccess, ISession { $reopened = $this->reopen(); $this->isModified = true; unset($this->sessionValues[$key]); - $this->session->remove(self::encryptedSessionName); if ($reopened) { $this->close(); } @@ -163,7 +162,11 @@ class CryptoSessionData implements \ArrayAccess, ISession { } public function reopen(): bool { - return $this->session->reopen(); + $reopened = $this->session->reopen(); + if ($reopened) { + $this->initializeSession(); + } + return $reopened; } /** diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 3b79b31b849..7b08fb2f66f 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -247,14 +247,13 @@ class Setup { ]; } - if ($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) { + if (PHP_INT_SIZE < 8) { $errors[] = [ 'error' => $this->l10n->t( - 'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. ' . - 'This will lead to problems with files over 4 GB and is highly discouraged.', + 'It seems that this %s instance is running on a 32-bit PHP environment. 64-bit is required for 26 and higher.', [$this->defaults->getProductName()] ), - 'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.'), + 'hint' => $this->l10n->t('Please switch to 64-bit PHP.'), ]; } diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php index bc24909dc3d..af816c7ad04 100644 --- a/lib/private/Setup/PostgreSQL.php +++ b/lib/private/Setup/PostgreSQL.php @@ -62,6 +62,7 @@ class PostgreSQL extends AbstractDatabase { } if ($canCreateRoles) { + $connectionMainDatabase = $this->connect(); //use the admin login data for the new database user //add prefix to the postgresql user name to prevent collisions @@ -70,6 +71,16 @@ class PostgreSQL extends AbstractDatabase { $this->dbPassword = \OC::$server->getSecureRandom()->generate(30, ISecureRandom::CHAR_ALPHANUMERIC); $this->createDBUser($connection); + + // Go to the main database and grant create on the public schema + // The code below is implemented to make installing possible with PostgreSQL version 15: + // https://www.postgresql.org/docs/release/15.0/ + // From the release notes: For new databases having no need to defend against insider threats, granting CREATE permission will yield the behavior of prior releases + // Therefore we assume that the database is only used by one user/service which is Nextcloud + // Additional services should get installed in a separate database in order to stay secure + // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS + $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO ' . addslashes($this->dbUser)); + $connectionMainDatabase->close(); } $this->config->setValues([ diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index c78cf62e069..a5a5568788d 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -1087,6 +1087,10 @@ class DefaultShareProvider implements IShareProvider { } } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $share->setSharedWith($data['share_with']); + $group = $this->groupManager->get($data['share_with']); + if ($group !== null) { + $share->setSharedWithDisplayName($group->getDisplayName()); + } } elseif ($share->getShareType() === IShare::TYPE_LINK) { $share->setPassword($data['password']); $share->setSendPasswordByTalk((bool)$data['password_by_talk']); diff --git a/lib/private/StreamImage.php b/lib/private/StreamImage.php index 38fd114df17..33078310d27 100644 --- a/lib/private/StreamImage.php +++ b/lib/private/StreamImage.php @@ -37,7 +37,7 @@ class StreamImage implements IStreamImage { /** @var resource The internal stream */ private $stream; - /** @var string */ + /** @var null|string */ private $mimeType; /** @var int */ @@ -55,38 +55,38 @@ class StreamImage implements IStreamImage { } /** @inheritDoc */ - public function valid() { + public function valid(): bool { return is_resource($this->stream); } /** @inheritDoc */ - public function mimeType() { + public function mimeType(): ?string { return $this->mimeType; } /** @inheritDoc */ - public function width() { + public function width(): int { return $this->width; } /** @inheritDoc */ - public function height() { + public function height(): int { return $this->height; } - public function widthTopLeft() { + public function widthTopLeft(): int { throw new \BadMethodCallException('Not implemented'); } - public function heightTopLeft() { + public function heightTopLeft(): int { throw new \BadMethodCallException('Not implemented'); } - public function show($mimeType = null) { + public function show(?string $mimeType = null): bool { throw new \BadMethodCallException('Not implemented'); } - public function save($filePath = null, $mimeType = null) { + public function save(?string $filePath = null, ?string $mimeType = null): bool { throw new \BadMethodCallException('Not implemented'); } @@ -94,23 +94,23 @@ class StreamImage implements IStreamImage { return $this->stream; } - public function dataMimeType() { + public function dataMimeType(): ?string { return $this->mimeType; } - public function data() { + public function data(): ?string { return ''; } - public function getOrientation() { + public function getOrientation(): int { throw new \BadMethodCallException('Not implemented'); } - public function fixOrientation() { + public function fixOrientation(): bool { throw new \BadMethodCallException('Not implemented'); } - public function resize($maxSize) { + public function resize(int $maxSize): bool { throw new \BadMethodCallException('Not implemented'); } @@ -118,7 +118,7 @@ class StreamImage implements IStreamImage { throw new \BadMethodCallException('Not implemented'); } - public function centerCrop($size = 0) { + public function centerCrop(int $size = 0): bool { throw new \BadMethodCallException('Not implemented'); } @@ -126,11 +126,11 @@ class StreamImage implements IStreamImage { throw new \BadMethodCallException('Not implemented'); } - public function fitIn($maxWidth, $maxHeight) { + public function fitIn(int $maxWidth, int $maxHeight): bool { throw new \BadMethodCallException('Not implemented'); } - public function scaleDownToFit($maxWidth, $maxHeight) { + public function scaleDownToFit(int $maxWidth, int $maxHeight): bool { throw new \BadMethodCallException('Not implemented'); } diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php index 88204be9805..db7d0024b5a 100644 --- a/lib/private/Streamer.php +++ b/lib/private/Streamer.php @@ -86,7 +86,7 @@ class Streamer { } elseif ($request->isUserAgent($this->preferTarFor)) { $this->streamerInstance = new TarStreamer(); } else { - $this->streamerInstance = new ZipStreamer(['zip64' => PHP_INT_SIZE !== 4]); + $this->streamerInstance = new ZipStreamer(['zip64' => true]); } } diff --git a/lib/private/Support/Subscription/Assertion.php b/lib/private/Support/Subscription/Assertion.php index 9b77e875944..451afe83bd3 100644 --- a/lib/private/Support/Subscription/Assertion.php +++ b/lib/private/Support/Subscription/Assertion.php @@ -49,7 +49,7 @@ class Assertion implements IAssertion { public function createUserIsLegit(): void { if ($this->registry->delegateIsHardUserLimitReached($this->notificationManager)) { $l = $this->l10nFactory->get('lib'); - throw new HintException($l->t('The user limit has been reached and the user was not created. Check your notifications to learn more.')); + throw new HintException($l->t('The user was not created because the user limit has been reached. Check your notifications to learn more.')); } } } diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index 0bc6154fbc4..6cd0e4376c5 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -72,6 +72,10 @@ class SystemConfig { 'host' => true, 'password' => true, ], + 'redis.cluster' => [ + 'seeds' => true, + 'password' => true, + ], 'objectstore' => [ 'arguments' => [ // Legacy Swift (https://github.com/nextcloud/server/pull/17696#discussion_r341302207) diff --git a/lib/private/Talk/Broker.php b/lib/private/Talk/Broker.php index a686adeed04..12e6c5fb34b 100644 --- a/lib/private/Talk/Broker.php +++ b/lib/private/Talk/Broker.php @@ -106,4 +106,12 @@ class Broker implements IBroker { $options ?? ConversationOptions::default() ); } + + public function deleteConversation(string $id): void { + if (!$this->hasBackend()) { + throw new NoBackendException("The Talk broker has no registered backend"); + } + + $this->backend->deleteConversation($id); + } } diff --git a/lib/private/Template/CSSResourceLocator.php b/lib/private/Template/CSSResourceLocator.php index 2cbf12ce65d..5047b3e906f 100644 --- a/lib/private/Template/CSSResourceLocator.php +++ b/lib/private/Template/CSSResourceLocator.php @@ -34,14 +34,8 @@ namespace OC\Template; use Psr\Log\LoggerInterface; class CSSResourceLocator extends ResourceLocator { - - /** - * @param string $theme - * @param array $core_map - * @param array $party_map - */ - public function __construct(LoggerInterface $logger, $theme, $core_map, $party_map) { - parent::__construct($logger, $theme, $core_map, $party_map); + public function __construct(LoggerInterface $logger) { + parent::__construct($logger); } /** @@ -50,8 +44,7 @@ class CSSResourceLocator extends ResourceLocator { public function doFind($style) { $app = substr($style, 0, strpos($style, '/')); if (strpos($style, '3rdparty') === 0 - && $this->appendIfExist($this->thirdpartyroot, $style.'.css') - || $this->appendIfExist($this->serverroot, $style.'.css') + && $this->appendIfExist($this->serverroot, $style.'.css') || $this->appendIfExist($this->serverroot, 'core/'.$style.'.css') ) { return; diff --git a/lib/private/Template/JSCombiner.php b/lib/private/Template/JSCombiner.php index a6d9f0ee558..c075e65d76a 100644 --- a/lib/private/Template/JSCombiner.php +++ b/lib/private/Template/JSCombiner.php @@ -198,7 +198,7 @@ class JSCombiner { $gzipFile->putContent(gzencode($res, 9)); $this->logger->debug('JSCombiner: successfully cached: ' . $fileName); return true; - } catch (NotPermittedException $e) { + } catch (NotPermittedException|NotFoundException $e) { $this->logger->error('JSCombiner: unable to cache: ' . $fileName); return false; } diff --git a/lib/private/Template/JSResourceLocator.php b/lib/private/Template/JSResourceLocator.php index 9e76655aba2..88323af75de 100644 --- a/lib/private/Template/JSResourceLocator.php +++ b/lib/private/Template/JSResourceLocator.php @@ -34,8 +34,8 @@ class JSResourceLocator extends ResourceLocator { /** @var JSCombiner */ protected $jsCombiner; - public function __construct(LoggerInterface $logger, $theme, array $core_map, array $party_map, JSCombiner $JSCombiner) { - parent::__construct($logger, $theme, $core_map, $party_map); + public function __construct(LoggerInterface $logger, JSCombiner $JSCombiner) { + parent::__construct($logger); $this->jsCombiner = $JSCombiner; } @@ -45,10 +45,6 @@ class JSResourceLocator extends ResourceLocator { */ public function doFind($script) { $theme_dir = 'themes/'.$this->theme.'/'; - if (strpos($script, '3rdparty') === 0 - && $this->appendIfExist($this->thirdpartyroot, $script.'.js')) { - return; - } // Extracting the appId and the script file name $app = substr($script, 0, strpos($script, '/')); diff --git a/lib/private/Template/ResourceLocator.php b/lib/private/Template/ResourceLocator.php index 5a50cc6fd1b..9e6e2056e6b 100755 --- a/lib/private/Template/ResourceLocator.php +++ b/lib/private/Template/ResourceLocator.php @@ -36,25 +36,20 @@ abstract class ResourceLocator { protected $mapping; protected $serverroot; - protected $thirdpartyroot; protected $webroot; protected $resources = []; protected LoggerInterface $logger; - /** - * @param string $theme - * @param array $core_map - * @param array $party_map - */ - public function __construct(LoggerInterface $logger, $theme, $core_map, $party_map) { + public function __construct(LoggerInterface $logger) { $this->logger = $logger; - $this->theme = $theme; - $this->mapping = $core_map + $party_map; - $this->serverroot = key($core_map); - $this->thirdpartyroot = key($party_map); - $this->webroot = $this->mapping[$this->serverroot]; + $this->mapping = [ + \OC::$SERVERROOT => \OC::$WEBROOT + ]; + $this->serverroot = \OC::$SERVERROOT; + $this->webroot = \OC::$WEBROOT; + $this->theme = \OC_Util::getTheme(); } /** diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index d041db2a7c2..178bec9c8dc 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -44,8 +44,9 @@ namespace OC; use bantu\IniGetWrapper\IniGetWrapper; use OC\Search\SearchQuery; -use OC\Template\JSCombiner; +use OC\Template\CSSResourceLocator; use OC\Template\JSConfigHelper; +use OC\Template\JSResourceLocator; use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; use OCP\IConfig; @@ -54,11 +55,16 @@ use OCP\INavigationManager; use OCP\IUserSession; use OCP\Support\Subscription\IRegistry; use OCP\Util; -use Psr\Log\LoggerInterface; class TemplateLayout extends \OC_Template { private static $versionHash = ''; + /** @var CSSResourceLocator|null */ + public static $cssLocator = null; + + /** @var JSResourceLocator|null */ + public static $jsLocator = null; + /** @var IConfig */ private $config; @@ -102,7 +108,7 @@ class TemplateLayout extends \OC_Template { $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); $this->initialState->provideInitialState('core', 'apps', $this->navigationManager->getAll()); $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); - $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)2)); + $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); Util::addScript('core', 'unified-search', 'core'); @@ -114,10 +120,6 @@ class TemplateLayout extends \OC_Template { $this->assign('enabledThemes', $themesService->getEnabledThemes()); } - // set logo link target - $logoUrl = $this->config->getSystemValueString('logo_url', ''); - $this->assign('logoUrl', $logoUrl); - // Add navigation entry $this->assign('application', ''); $this->assign('appid', $appId); @@ -188,6 +190,11 @@ class TemplateLayout extends \OC_Template { } else { parent::__construct('core', 'layout.base'); } + + // set logo link target + $logoUrl = $this->config->getSystemValueString('logo_url', ''); + $this->assign('logoUrl', $logoUrl); + // Send the language and the locale to our layouts $lang = \OC::$server->getL10NFactory()->findLanguage(); $locale = \OC::$server->getL10NFactory()->findLocale($lang); @@ -332,17 +339,11 @@ class TemplateLayout extends \OC_Template { * @return array */ public static function findStylesheetFiles($styles, $compileScss = true) { - // Read the selected theme from the config file - $theme = \OC_Util::getTheme(); - - $locator = new \OC\Template\CSSResourceLocator( - \OC::$server->get(LoggerInterface::class), - $theme, - [ \OC::$SERVERROOT => \OC::$WEBROOT ], - [ \OC::$SERVERROOT => \OC::$WEBROOT ], - ); - $locator->find($styles); - return $locator->getResources(); + if (!self::$cssLocator) { + self::$cssLocator = \OCP\Server::get(CSSResourceLocator::class); + } + self::$cssLocator->find($styles); + return self::$cssLocator->getResources(); } /** @@ -366,18 +367,11 @@ class TemplateLayout extends \OC_Template { * @return array */ public static function findJavascriptFiles($scripts) { - // Read the selected theme from the config file - $theme = \OC_Util::getTheme(); - - $locator = new \OC\Template\JSResourceLocator( - \OC::$server->get(LoggerInterface::class), - $theme, - [ \OC::$SERVERROOT => \OC::$WEBROOT ], - [ \OC::$SERVERROOT => \OC::$WEBROOT ], - \OC::$server->query(JSCombiner::class) - ); - $locator->find($scripts); - return $locator->getResources(); + if (!self::$jsLocator) { + self::$jsLocator = \OCP\Server::get(JSResourceLocator::class); + } + self::$jsLocator->find($scripts); + return self::$jsLocator->getResources(); } /** diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 78613ddbb0c..53b07e25809 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -138,6 +138,9 @@ class Updater extends BasicEmitter { $success = true; try { + if (PHP_INT_SIZE < 8 && version_compare($currentVersion, '26.0.0.0', '>=')) { + throw new HintException('You are running a 32-bit PHP version. Cannot upgrade to Nextcloud 26 and higher. Please switch to 64-bit PHP.'); + } $this->doUpgrade($currentVersion, $installedVersion); } catch (HintException $exception) { $this->log->error($exception->getMessage(), [ diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index dc31eece414..82fc4d818ad 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -44,6 +44,8 @@ use OCP\IGroup; use OCP\IUser; use OCP\IUserBackend; use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Server; use OCP\Support\Subscription\IAssertion; use OCP\User\Backend\IGetRealUIDBackend; use OCP\User\Backend\ISearchKnownUsersBackend; @@ -427,31 +429,7 @@ class Manager extends PublicEmitter implements IUserManager { public function createUserFromBackend($uid, $password, UserInterface $backend) { $l = \OC::$server->getL10N('lib'); - // Check the name for bad characters - // Allowed are: "a-z", "A-Z", "0-9" and "_.@-'" - if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) { - throw new \InvalidArgumentException($l->t('Only the following characters are allowed in a username:' - . ' "a-z", "A-Z", "0-9", and "_.@-\'"')); - } - - // No empty username - if (trim($uid) === '') { - throw new \InvalidArgumentException($l->t('A valid username must be provided')); - } - - // No whitespace at the beginning or at the end - if (trim($uid) !== $uid) { - throw new \InvalidArgumentException($l->t('Username contains whitespace at the beginning or at the end')); - } - - // Username only consists of 1 or 2 dots (directory traversal) - if ($uid === '.' || $uid === '..') { - throw new \InvalidArgumentException($l->t('Username must not consist of dots only')); - } - - if (!$this->verifyUid($uid)) { - throw new \InvalidArgumentException($l->t('Username is invalid because files already exist for this user')); - } + $this->validateUserId($uid, true); // No empty password if (trim($password) === '') { @@ -726,7 +704,43 @@ class Manager extends PublicEmitter implements IUserManager { })); } - private function verifyUid(string $uid): bool { + /** + * @param string $uid + * @param bool $checkDataDirectory + * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid + * @since 26.0.0 + */ + public function validateUserId(string $uid, bool $checkDataDirectory = false): void { + $l = Server::get(IFactory::class)->get('lib'); + + // Check the name for bad characters + // Allowed are: "a-z", "A-Z", "0-9" and "_.@-'" + if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) { + throw new \InvalidArgumentException($l->t('Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", and "_.@-\'"')); + } + + // No empty username + if (trim($uid) === '') { + throw new \InvalidArgumentException($l->t('A valid username must be provided')); + } + + // No whitespace at the beginning or at the end + if (trim($uid) !== $uid) { + throw new \InvalidArgumentException($l->t('Username contains whitespace at the beginning or at the end')); + } + + // Username only consists of 1 or 2 dots (directory traversal) + if ($uid === '.' || $uid === '..') { + throw new \InvalidArgumentException($l->t('Username must not consist of dots only')); + } + + if (!$this->verifyUid($uid, $checkDataDirectory)) { + throw new \InvalidArgumentException($l->t('Username is invalid because files already exist for this user')); + } + } + + private function verifyUid(string $uid, bool $checkDataDirectory = false): bool { $appdata = 'appdata_' . $this->config->getSystemValueString('instanceid'); if (\in_array($uid, [ @@ -740,6 +754,10 @@ class Manager extends PublicEmitter implements IUserManager { return false; } + if (!$checkDataDirectory) { + return true; + } + $dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data'); return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid); diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 5117812db31..c7b11e22504 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -89,7 +89,6 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\User */ class Session implements IUserSession, Emitter { - /** @var Manager $manager */ private $manager; @@ -372,6 +371,7 @@ class Session implements IUserSession, Emitter { if ($regenerateSessionId) { $this->session->regenerateId(); + $this->session->remove(Auth::DAV_AUTHENTICATED); } $this->setUser($user); @@ -448,7 +448,6 @@ class Session implements IUserSession, Emitter { // Try to login with this username and password if (!$this->login($user, $password)) { - // Failed, maybe the user used their email address if (!filter_var($user, FILTER_VALIDATE_EMAIL)) { return false; @@ -459,7 +458,7 @@ class Session implements IUserSession, Emitter { $throttler->registerAttempt('login', $request->getRemoteAddress(), ['user' => $user]); - $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user)); + $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password)); if ($currentDelay === 0) { $throttler->sleepDelay($request->getRemoteAddress(), 'login'); @@ -672,7 +671,7 @@ class Session implements IUserSession, Emitter { // User does not exist return false; } - $name = isset($request->server['HTTP_USER_AGENT']) ? utf8_encode($request->server['HTTP_USER_AGENT']) : 'unknown browser'; + $name = isset($request->server['HTTP_USER_AGENT']) ? mb_convert_encoding($request->server['HTTP_USER_AGENT'], 'UTF-8', 'ISO-8859-1') : 'unknown browser'; try { $sessionId = $this->session->getId(); $pwd = $this->getPassword($password); @@ -868,7 +867,7 @@ class Session implements IUserSession, Emitter { $tokens = $this->config->getUserKeys($uid, 'login_token'); // test cookies token against stored tokens if (!in_array($currentToken, $tokens, true)) { - $this->logger->error('Tried to log in {uid} but could not verify token', [ + $this->logger->info('Tried to log in {uid} but could not verify token', [ 'app' => 'core', 'uid' => $uid, ]); diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index b793029e1f1..3b409fd1d04 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -457,10 +457,12 @@ class OC_Helper { * * @param string $path * @param \OCP\Files\FileInfo $rootInfo (optional) + * @param bool $includeMountPoints whether to include mount points in the size calculation + * @param bool $useCache whether to use the cached quota values * @return array * @throws \OCP\Files\NotFoundException */ - public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true) { + public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) { /** @var ICacheFactory $cacheFactory */ $cacheFactory = \OC::$server->get(ICacheFactory::class); $memcache = $cacheFactory->createLocal('storage_info'); @@ -470,9 +472,11 @@ class OC_Helper { $fullPath = Filesystem::getView()->getAbsolutePath($path); $cacheKey = $fullPath. '::' . ($includeMountPoints ? 'include' : 'exclude'); - $cached = $memcache->get($cacheKey); - if ($cached) { - return $cached; + if ($useCache) { + $cached = $memcache->get($cacheKey); + if ($cached) { + return $cached; + } } if (!$rootInfo) { diff --git a/lib/private/legacy/OC_Image.php b/lib/private/legacy/OC_Image.php index a212d639084..9ccc6409ba0 100644 --- a/lib/private/legacy/OC_Image.php +++ b/lib/private/legacy/OC_Image.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * @@ -50,14 +53,15 @@ class OC_Image implements \OCP\IImage { // Default memory limit for images to load (128 MBytes). protected const DEFAULT_MEMORY_LIMIT = 128; + // Default quality for jpeg images + protected const DEFAULT_JPEG_QUALITY = 80; + /** @var false|resource|\GdImage */ protected $resource = false; // tmp resource. /** @var int */ protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident. - /** @var string */ + /** @var null|string */ protected $mimeType = 'image/png'; // Default to png - /** @var int */ - protected $bitDepth = 24; /** @var null|string */ protected $filePath = null; /** @var finfo */ @@ -102,7 +106,7 @@ class OC_Image implements \OCP\IImage { * * @return bool */ - public function valid() { + public function valid(): bool { if ((is_resource($this->resource) && get_resource_type($this->resource) === 'gd') || (is_object($this->resource) && get_class($this->resource) === \GdImage::class)) { return true; @@ -112,12 +116,12 @@ class OC_Image implements \OCP\IImage { } /** - * Returns the MIME type of the image or an empty string if no image is loaded. + * Returns the MIME type of the image or null if no image is loaded. * * @return string */ - public function mimeType() { - return $this->valid() ? $this->mimeType : ''; + public function mimeType(): ?string { + return $this->valid() ? $this->mimeType : null; } /** @@ -125,7 +129,7 @@ class OC_Image implements \OCP\IImage { * * @return int */ - public function width() { + public function width(): int { if ($this->valid()) { $width = imagesx($this->resource); if ($width !== false) { @@ -140,7 +144,7 @@ class OC_Image implements \OCP\IImage { * * @return int */ - public function height() { + public function height(): int { if ($this->valid()) { $height = imagesy($this->resource); if ($height !== false) { @@ -155,7 +159,7 @@ class OC_Image implements \OCP\IImage { * * @return int */ - public function widthTopLeft() { + public function widthTopLeft(): int { $o = $this->getOrientation(); $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']); switch ($o) { @@ -179,7 +183,7 @@ class OC_Image implements \OCP\IImage { * * @return int */ - public function heightTopLeft() { + public function heightTopLeft(): int { $o = $this->getOrientation(); $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']); switch ($o) { @@ -204,7 +208,7 @@ class OC_Image implements \OCP\IImage { * @param string $mimeType * @return bool */ - public function show($mimeType = null) { + public function show(string $mimeType = null): bool { if ($mimeType === null) { $mimeType = $this->mimeType(); } @@ -220,7 +224,7 @@ class OC_Image implements \OCP\IImage { * @return bool */ - public function save($filePath = null, $mimeType = null) { + public function save(?string $filePath = null, ?string $mimeType = null): bool { if ($mimeType === null) { $mimeType = $this->mimeType(); } @@ -243,7 +247,7 @@ class OC_Image implements \OCP\IImage { * @return bool * @throws Exception */ - private function _output($filePath = null, $mimeType = null) { + private function _output(?string $filePath = null, ?string $mimeType = null): bool { if ($filePath) { if (!file_exists(dirname($filePath))) { mkdir(dirname($filePath), 0777, true); @@ -252,7 +256,7 @@ class OC_Image implements \OCP\IImage { if (!$isWritable) { $this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']); return false; - } elseif ($isWritable && file_exists($filePath) && !is_writable($filePath)) { + } elseif (file_exists($filePath) && !is_writable($filePath)) { $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']); return false; } @@ -290,6 +294,8 @@ class OC_Image implements \OCP\IImage { $retVal = imagegif($this->resource, $filePath); break; case IMAGETYPE_JPEG: + /** @psalm-suppress InvalidScalarArgument */ + imageinterlace($this->resource, (PHP_VERSION_ID >= 80000 ? true : 1)); $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality()); break; case IMAGETYPE_PNG: @@ -307,7 +313,7 @@ class OC_Image implements \OCP\IImage { $retVal = imagewbmp($this->resource, $filePath); break; case IMAGETYPE_BMP: - $retVal = imagebmp($this->resource, $filePath, $this->bitDepth); + $retVal = imagebmp($this->resource, $filePath); break; default: $retVal = imagepng($this->resource, $filePath); @@ -348,12 +354,11 @@ class OC_Image implements \OCP\IImage { } /** - * @return string Returns the mimetype of the data. Returns the empty string - * if the data is not valid. + * @return string Returns the mimetype of the data. Returns null if the data is not valid. */ - public function dataMimeType() { + public function dataMimeType(): ?string { if (!$this->valid()) { - return ''; + return null; } switch ($this->mimeType) { @@ -369,7 +374,7 @@ class OC_Image implements \OCP\IImage { /** * @return null|string Returns the raw image data. */ - public function data() { + public function data(): ?string { if (!$this->valid()) { return null; } @@ -379,12 +384,10 @@ class OC_Image implements \OCP\IImage { $res = imagepng($this->resource); break; case "image/jpeg": + /** @psalm-suppress InvalidScalarArgument */ + imageinterlace($this->resource, (PHP_VERSION_ID >= 80000 ? true : 1)); $quality = $this->getJpegQuality(); - if ($quality !== null) { - $res = imagejpeg($this->resource, null, $quality); - } else { - $res = imagejpeg($this->resource); - } + $res = imagejpeg($this->resource, null, $quality); break; case "image/gif": $res = imagegif($this->resource); @@ -408,14 +411,15 @@ class OC_Image implements \OCP\IImage { } /** - * @return int|null + * @return int */ - protected function getJpegQuality() { - $quality = $this->config->getAppValue('preview', 'jpeg_quality', 90); - if ($quality !== null) { - $quality = min(100, max(10, (int) $quality)); + protected function getJpegQuality(): int { + $quality = $this->config->getAppValue('preview', 'jpeg_quality', (string) self::DEFAULT_JPEG_QUALITY); + // TODO: remove when getAppValue is type safe + if ($quality === null) { + $quality = self::DEFAULT_JPEG_QUALITY; } - return $quality; + return min(100, max(10, (int) $quality)); } /** @@ -424,7 +428,7 @@ class OC_Image implements \OCP\IImage { * * @return int The orientation or -1 if no EXIF data is available. */ - public function getOrientation() { + public function getOrientation(): int { if ($this->exif !== null) { return $this->exif['Orientation']; } @@ -456,7 +460,7 @@ class OC_Image implements \OCP\IImage { return $exif['Orientation']; } - public function readExif($data) { + public function readExif($data): void { if (!is_callable('exif_read_data')) { $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); return; @@ -482,7 +486,7 @@ class OC_Image implements \OCP\IImage { * * @return bool */ - public function fixOrientation() { + public function fixOrientation(): bool { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -712,7 +716,7 @@ class OC_Image implements \OCP\IImage { } break; case IMAGETYPE_BMP: - $this->resource = $this->imagecreatefrombmp($imagePath); + $this->resource = imagecreatefrombmp($imagePath); break; case IMAGETYPE_WEBP: if (imagetypes() & IMG_WEBP) { @@ -774,10 +778,7 @@ class OC_Image implements \OCP\IImage { * @param string $str A string of image data as read from a file. * @return bool|resource|\GdImage An image resource or false on error */ - public function loadFromData($str) { - if (!is_string($str)) { - return false; - } + public function loadFromData(string $str) { if (!$this->checkImageDataSize($str)) { return false; } @@ -803,10 +804,7 @@ class OC_Image implements \OCP\IImage { * @param string $str A string base64 encoded string of image data. * @return bool|resource|\GdImage An image resource or false on error */ - public function loadFromBase64($str) { - if (!is_string($str)) { - return false; - } + public function loadFromBase64(string $str) { $data = base64_decode($str); if ($data) { // try to load from string data if (!$this->checkImageDataSize($data)) { @@ -827,170 +825,12 @@ class OC_Image implements \OCP\IImage { } /** - * Create a new image from file or URL - * - * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm - * @version 1.00 - * @param string $fileName <p> - * Path to the BMP image. - * </p> - * @return bool|resource|\GdImage an image resource identifier on success, <b>FALSE</b> on errors. - */ - private function imagecreatefrombmp($fileName) { - if (!($fh = fopen($fileName, 'rb'))) { - $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']); - return false; - } - // read file header - $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); - // check for bitmap - if ($meta['type'] != 19778) { - fclose($fh); - $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']); - return false; - } - // read image header - $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); - // read additional 16bit header - if ($meta['bits'] == 16) { - $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); - } - // set bytes and padding - $meta['bytes'] = $meta['bits'] / 8; - $this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call - $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); - if ($meta['decal'] == 4) { - $meta['decal'] = 0; - } - // obtain imagesize - if ($meta['imagesize'] < 1) { - $meta['imagesize'] = $meta['filesize'] - $meta['offset']; - // in rare cases filesize is equal to offset so we need to read physical size - if ($meta['imagesize'] < 1) { - $meta['imagesize'] = @filesize($fileName) - $meta['offset']; - if ($meta['imagesize'] < 1) { - fclose($fh); - $this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']); - return false; - } - } - } - // calculate colors - $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; - // read color palette - $palette = []; - if ($meta['bits'] < 16) { - $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); - // in rare cases the color value is signed - if ($palette[1] < 0) { - foreach ($palette as $i => $color) { - $palette[$i] = $color + 16777216; - } - } - } - if (!$this->checkImageMemory($meta['width'], $meta['height'])) { - fclose($fh); - return false; - } - // create gd image - $im = imagecreatetruecolor($meta['width'], $meta['height']); - if ($im == false) { - fclose($fh); - $this->logger->warning( - 'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'], - ['app' => 'core']); - return false; - } - - $data = fread($fh, $meta['imagesize']); - $p = 0; - $vide = chr(0); - $y = $meta['height'] - 1; - $error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!'; - // loop through the image data beginning with the lower left corner - while ($y >= 0) { - $x = 0; - while ($x < $meta['width']) { - switch ($meta['bits']) { - case 32: - case 24: - if (!($part = substr($data, $p, 3))) { - $this->logger->warning($error, ['app' => 'core']); - return $im; - } - $color = @unpack('V', $part . $vide); - break; - case 16: - if (!($part = substr($data, $p, 2))) { - fclose($fh); - $this->logger->warning($error, ['app' => 'core']); - return $im; - } - $color = @unpack('v', $part); - $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); - break; - case 8: - $color = @unpack('n', $vide . ($data[$p] ?? '')); - $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; - break; - case 4: - $color = @unpack('n', $vide . ($data[floor($p)] ?? '')); - $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; - $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; - break; - case 1: - $color = @unpack('n', $vide . ($data[floor($p)] ?? '')); - switch (($p * 8) % 8) { - case 0: - $color[1] = $color[1] >> 7; - break; - case 1: - $color[1] = ($color[1] & 0x40) >> 6; - break; - case 2: - $color[1] = ($color[1] & 0x20) >> 5; - break; - case 3: - $color[1] = ($color[1] & 0x10) >> 4; - break; - case 4: - $color[1] = ($color[1] & 0x8) >> 3; - break; - case 5: - $color[1] = ($color[1] & 0x4) >> 2; - break; - case 6: - $color[1] = ($color[1] & 0x2) >> 1; - break; - case 7: - $color[1] = ($color[1] & 0x1); - break; - } - $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; - break; - default: - fclose($fh); - $this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']); - return false; - } - imagesetpixel($im, $x, $y, $color[1]); - $x++; - $p += $meta['bytes']; - } - $y--; - $p += $meta['decal']; - } - fclose($fh); - return $im; - } - - /** * Resizes the image preserving ratio. * - * @param integer $maxSize The maximum size of either the width or height. + * @param int $maxSize The maximum size of either the width or height. * @return bool */ - public function resize($maxSize) { + public function resize(int $maxSize): bool { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -1005,7 +845,7 @@ class OC_Image implements \OCP\IImage { * @param $maxSize * @return resource|bool|\GdImage */ - private function resizeNew($maxSize) { + private function resizeNew(int $maxSize) { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -1041,7 +881,6 @@ class OC_Image implements \OCP\IImage { return $this->valid(); } - /** * @param int $width * @param int $height @@ -1086,7 +925,7 @@ class OC_Image implements \OCP\IImage { * @param int $size maximum size for the result (optional) * @return bool for success or failure */ - public function centerCrop($size = 0) { + public function centerCrop(int $size = 0): bool { if (!$this->valid()) { $this->logger->debug('OC_Image->centerCrop, No image loaded', ['app' => 'core']); return false; @@ -1100,10 +939,10 @@ class OC_Image implements \OCP\IImage { $width = $height = min($widthOrig, $heightOrig); if ($ratioOrig > 1) { - $x = ($widthOrig / 2) - ($width / 2); + $x = (int) (($widthOrig / 2) - ($width / 2)); $y = 0; } else { - $y = ($heightOrig / 2) - ($height / 2); + $y = (int) (($heightOrig / 2) - ($height / 2)); $x = 0; } if ($size > 0) { @@ -1196,11 +1035,11 @@ class OC_Image implements \OCP\IImage { * * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up * - * @param integer $maxWidth - * @param integer $maxHeight + * @param int $maxWidth + * @param int $maxHeight * @return bool */ - public function fitIn($maxWidth, $maxHeight) { + public function fitIn(int $maxWidth, int $maxHeight): bool { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -1219,11 +1058,11 @@ class OC_Image implements \OCP\IImage { /** * Shrinks larger images to fit within specified boundaries while preserving ratio. * - * @param integer $maxWidth - * @param integer $maxHeight + * @param int $maxWidth + * @param int $maxHeight * @return bool */ - public function scaleDownToFit($maxWidth, $maxHeight) { + public function scaleDownToFit(int $maxWidth, int $maxHeight): bool { if (!$this->valid()) { $this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']); return false; @@ -1259,7 +1098,6 @@ class OC_Image implements \OCP\IImage { $image = new OC_Image(null, $this->logger, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; - $image->bitDepth = $this->bitDepth; $image->resource = $this->cropNew($x, $y, $w, $h); return $image; @@ -1269,7 +1107,6 @@ class OC_Image implements \OCP\IImage { $image = new OC_Image(null, $this->logger, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; - $image->bitDepth = $this->bitDepth; $image->resource = $this->preciseResizeNew($width, $height); return $image; @@ -1279,7 +1116,6 @@ class OC_Image implements \OCP\IImage { $image = new OC_Image(null, $this->logger, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; - $image->bitDepth = $this->bitDepth; $image->resource = $this->resizeNew($maxSize); return $image; @@ -1288,7 +1124,7 @@ class OC_Image implements \OCP\IImage { /** * Destroys the current image and resets the object */ - public function destroy() { + public function destroy(): void { if ($this->valid()) { imagedestroy($this->resource); } @@ -1300,123 +1136,6 @@ class OC_Image implements \OCP\IImage { } } -if (!function_exists('imagebmp')) { - /** - * Output a BMP image to either the browser or a file - * - * @link http://www.ugia.cn/wp-data/imagebmp.php - * @author legend <legendsky@hotmail.com> - * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm - * @author mgutt <marc@gutt.it> - * @version 1.00 - * @param resource|\GdImage $im - * @param string $fileName [optional] <p>The path to save the file to.</p> - * @param int $bit [optional] <p>Bit depth, (default is 24).</p> - * @param int $compression [optional] - * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure. - */ - function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) { - if (!in_array($bit, [1, 4, 8, 16, 24, 32])) { - $bit = 24; - } elseif ($bit == 32) { - $bit = 24; - } - $bits = (int)pow(2, $bit); - imagetruecolortopalette($im, true, $bits); - $width = imagesx($im); - $height = imagesy($im); - $colorsNum = imagecolorstotal($im); - $rgbQuad = ''; - if ($bit <= 8) { - for ($i = 0; $i < $colorsNum; $i++) { - $colors = imagecolorsforindex($im, $i); - $rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0"; - } - $bmpData = ''; - if ($compression == 0 || $bit < 8) { - $compression = 0; - $extra = ''; - $padding = 4 - ceil($width / (8 / $bit)) % 4; - if ($padding % 4 != 0) { - $extra = str_repeat("\0", $padding); - } - for ($j = $height - 1; $j >= 0; $j--) { - $i = 0; - while ($i < $width) { - $bin = 0; - $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0; - for ($k = 8 - $bit; $k >= $limit; $k -= $bit) { - $index = imagecolorat($im, $i, $j); - $bin |= $index << $k; - $i++; - } - $bmpData .= chr($bin); - } - $bmpData .= $extra; - } - } // RLE8 - elseif ($compression == 1 && $bit == 8) { - for ($j = $height - 1; $j >= 0; $j--) { - $lastIndex = null; - $sameNum = 0; - for ($i = 0; $i <= $width; $i++) { - $index = imagecolorat($im, $i, $j); - if ($index !== $lastIndex || $sameNum > 255) { - if ($sameNum != 0) { - $bmpData .= chr($sameNum) . chr($lastIndex); - } - $lastIndex = $index; - $sameNum = 1; - } else { - $sameNum++; - } - } - $bmpData .= "\0\0"; - } - $bmpData .= "\0\1"; - } - $sizeQuad = strlen($rgbQuad); - $sizeData = strlen($bmpData); - } else { - $extra = ''; - $padding = 4 - ($width * ($bit / 8)) % 4; - if ($padding % 4 != 0) { - $extra = str_repeat("\0", $padding); - } - $bmpData = ''; - for ($j = $height - 1; $j >= 0; $j--) { - for ($i = 0; $i < $width; $i++) { - $index = imagecolorat($im, $i, $j); - $colors = imagecolorsforindex($im, $index); - if ($bit == 16) { - $bin = 0 << $bit; - $bin |= ($colors['red'] >> 3) << 10; - $bin |= ($colors['green'] >> 3) << 5; - $bin |= $colors['blue'] >> 3; - $bmpData .= pack("v", $bin); - } else { - $bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']); - } - } - $bmpData .= $extra; - } - $sizeQuad = 0; - $sizeData = strlen($bmpData); - $colorsNum = 0; - } - $fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad); - $infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0); - if ($fileName != '') { - $fp = fopen($fileName, 'wb'); - fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData); - fclose($fp); - return true; - } - echo $fileHeader . $infoHeader . $rgbQuad . $bmpData; - return true; - } -} - if (!function_exists('exif_imagetype')) { /** * Workaround if exif_imagetype does not exist @@ -1425,7 +1144,7 @@ if (!function_exists('exif_imagetype')) { * @param string $fileName * @return string|boolean */ - function exif_imagetype($fileName) { + function exif_imagetype(string $fileName) { if (($info = getimagesize($fileName)) !== false) { return $info[2]; } diff --git a/lib/private/legacy/OC_Response.php b/lib/private/legacy/OC_Response.php index e4525fe9e10..b849710e90b 100644 --- a/lib/private/legacy/OC_Response.php +++ b/lib/private/legacy/OC_Response.php @@ -52,19 +52,6 @@ class OC_Response { * @param string|int|float $length Length to be sent */ public static function setContentLengthHeader($length) { - if (PHP_INT_SIZE === 4) { - if ($length > PHP_INT_MAX && stripos(PHP_SAPI, 'apache') === 0) { - // Apache PHP SAPI casts Content-Length headers to PHP integers. - // This enforces a limit of PHP_INT_MAX (2147483647 on 32-bit - // platforms). So, if the length is greater than PHP_INT_MAX, - // we just do not send a Content-Length header to prevent - // bodies from being received incompletely. - return; - } - // Convert signed integer or float to unsigned base-10 string. - $lfh = new \OC\LargeFileHelper; - $length = $lfh->formatUnsignedInteger($length); - } header('Content-Length: '.$length); } diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index 4778fe33097..429f7ed5d05 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -255,7 +255,7 @@ class OC_Util { closedir($dir); return; } - stream_copy_to_stream($sourceStream, $child->fopen('w')); + $child->putContent($sourceStream); } } } |