diff options
Diffstat (limited to 'lib/private')
47 files changed, 1063 insertions, 480 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index d14f0a2644e..5f243a1250e 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -39,15 +39,25 @@ namespace OC\App; use OC\AppConfig; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\ServerNotAvailableException; +use OCP\Activity\IManager as IActivityManager; use OCP\App\AppPathNotFoundException; +use OCP\App\Events\AppDisableEvent; +use OCP\App\Events\AppEnableEvent; use OCP\App\IAppManager; use OCP\App\ManagerEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager; +use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch; +use OCP\Diagnostics\IEventLogger; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserSession; +use OCP\Settings\IManager as ISettingsManager; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -64,57 +74,51 @@ class AppManager implements IAppManager { 'prevent_group_restriction', ]; - /** @var IUserSession */ - private $userSession; - - /** @var IConfig */ - private $config; - - /** @var AppConfig */ - private $appConfig; - - /** @var IGroupManager */ - private $groupManager; - - /** @var ICacheFactory */ - private $memCacheFactory; - - /** @var EventDispatcherInterface */ - private $dispatcher; - - /** @var LoggerInterface */ - private $logger; + private IUserSession $userSession; + private IConfig $config; + private AppConfig $appConfig; + private IGroupManager $groupManager; + private ICacheFactory $memCacheFactory; + private EventDispatcherInterface $legacyDispatcher; + private IEventDispatcher $dispatcher; + private LoggerInterface $logger; /** @var string[] $appId => $enabled */ - private $installedAppsCache; + private array $installedAppsCache = []; - /** @var string[] */ - private $shippedApps; + /** @var string[]|null */ + private ?array $shippedApps = null; private array $alwaysEnabled = []; private array $defaultEnabled = []; /** @var array */ - private $appInfos = []; + private array $appInfos = []; /** @var array */ - private $appVersions = []; + private array $appVersions = []; /** @var array */ - private $autoDisabledApps = []; + private array $autoDisabledApps = []; + private array $appTypes = []; + + /** @var array<string, true> */ + private array $loadedApps = []; public function __construct(IUserSession $userSession, IConfig $config, AppConfig $appConfig, IGroupManager $groupManager, ICacheFactory $memCacheFactory, - EventDispatcherInterface $dispatcher, + EventDispatcherInterface $legacyDispatcher, + IEventDispatcher $dispatcher, LoggerInterface $logger) { $this->userSession = $userSession; $this->config = $config; $this->appConfig = $appConfig; $this->groupManager = $groupManager; $this->memCacheFactory = $memCacheFactory; + $this->legacyDispatcher = $legacyDispatcher; $this->dispatcher = $dispatcher; $this->logger = $logger; } @@ -122,7 +126,7 @@ class AppManager implements IAppManager { /** * @return string[] $appId => $enabled */ - private function getInstalledAppsValues() { + private function getInstalledAppsValues(): array { if (!$this->installedAppsCache) { $values = $this->appConfig->getValues(false, 'enabled'); @@ -163,7 +167,7 @@ class AppManager implements IAppManager { } /** - * @param \OCP\IGroup $group + * @param IGroup $group * @return array */ public function getEnabledAppsForGroup(IGroup $group): array { @@ -175,6 +179,91 @@ class AppManager implements IAppManager { } /** + * Loads all apps + * + * @param string[] $types + * @return bool + * + * This function walks through the Nextcloud directory and loads all apps + * it can find. A directory contains an app if the file /appinfo/info.xml + * exists. + * + * if $types is set to non-empty array, only apps of those types will be loaded + */ + public function loadApps(array $types = []): bool { + if ($this->config->getSystemValueBool('maintenance', false)) { + return false; + } + // Load the enabled apps here + $apps = \OC_App::getEnabledApps(); + + // Add each apps' folder as allowed class path + foreach ($apps as $app) { + // If the app is already loaded then autoloading it makes no sense + if (!$this->isAppLoaded($app)) { + $path = \OC_App::getAppPath($app); + if ($path !== false) { + \OC_App::registerAutoloading($app, $path); + } + } + } + + // prevent app.php from printing output + ob_start(); + foreach ($apps as $app) { + if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) { + try { + $this->loadApp($app); + } catch (\Throwable $e) { + $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [ + 'exception' => $e, + 'app' => $app, + ]); + } + } + } + ob_end_clean(); + + return true; + } + + /** + * check if an app is of a specific type + * + * @param string $app + * @param array $types + * @return bool + */ + public function isType(string $app, array $types): bool { + $appTypes = $this->getAppTypes($app); + foreach ($types as $type) { + if (in_array($type, $appTypes, true)) { + return true; + } + } + return false; + } + + /** + * get the types of an app + * + * @param string $app + * @return string[] + */ + private function getAppTypes(string $app): array { + //load the cache + if (count($this->appTypes) === 0) { + $this->appTypes = $this->appConfig->getValues(false, 'types') ?: []; + } + + if (isset($this->appTypes[$app])) { + return explode(',', $this->appTypes[$app]); + } + + return []; + } + + /** * @return array */ public function getAutoDisabledApps(): array { @@ -221,12 +310,7 @@ class AppManager implements IAppManager { } } - /** - * @param string $enabled - * @param IUser $user - * @return bool - */ - private function checkAppForUser($enabled, $user) { + private function checkAppForUser(string $enabled, ?IUser $user): bool { if ($enabled === 'yes') { return true; } elseif ($user === null) { @@ -254,16 +338,9 @@ class AppManager implements IAppManager { } } - /** - * @param string $enabled - * @param IGroup $group - * @return bool - */ private function checkAppForGroups(string $enabled, IGroup $group): bool { if ($enabled === 'yes') { return true; - } elseif ($group === null) { - return false; } else { if (empty($enabled)) { return false; @@ -287,7 +364,7 @@ class AppManager implements IAppManager { * Notice: This actually checks if the app is enabled and not only if it is installed. * * @param string $appId - * @param \OCP\IGroup[]|String[] $groups + * @param IGroup[]|String[] $groups * @return bool */ public function isInstalled($appId) { @@ -303,6 +380,151 @@ class AppManager implements IAppManager { } } + public function loadApp(string $app): void { + if (isset($this->loadedApps[$app])) { + return; + } + $this->loadedApps[$app] = true; + $appPath = \OC_App::getAppPath($app); + if ($appPath === false) { + return; + } + $eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class); + $eventLogger->start("bootstrap:load_app:$app", "Load $app"); + + // in case someone calls loadApp() directly + \OC_App::registerAutoloading($app, $appPath); + + /** @var Coordinator $coordinator */ + $coordinator = \OC::$server->get(Coordinator::class); + $isBootable = $coordinator->isBootable($app); + + $hasAppPhpFile = is_file($appPath . '/appinfo/app.php'); + + $eventLogger = \OC::$server->get(IEventLogger::class); + $eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app); + if ($isBootable && $hasAppPhpFile) { + $this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [ + 'app' => $app, + ]); + } elseif ($hasAppPhpFile) { + $eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app"); + $this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [ + 'app' => $app, + ]); + try { + self::requireAppFile($appPath); + } catch (\Throwable $ex) { + if ($ex instanceof ServerNotAvailableException) { + throw $ex; + } + if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) { + $this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [ + 'exception' => $ex, + ]); + + // Only disable apps which are not shipped and that are not authentication apps + $this->disableApp($app, true); + } else { + $this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [ + 'exception' => $ex, + ]); + } + } + $eventLogger->end("bootstrap:load_app:$app:app.php"); + } + + $coordinator->bootApp($app); + + $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it"); + $info = $this->getAppInfo($app); + if (!empty($info['activity'])) { + $activityManager = \OC::$server->get(IActivityManager::class); + if (!empty($info['activity']['filters'])) { + foreach ($info['activity']['filters'] as $filter) { + $activityManager->registerFilter($filter); + } + } + if (!empty($info['activity']['settings'])) { + foreach ($info['activity']['settings'] as $setting) { + $activityManager->registerSetting($setting); + } + } + if (!empty($info['activity']['providers'])) { + foreach ($info['activity']['providers'] as $provider) { + $activityManager->registerProvider($provider); + } + } + } + + if (!empty($info['settings'])) { + $settingsManager = \OC::$server->get(ISettingsManager::class); + if (!empty($info['settings']['admin'])) { + foreach ($info['settings']['admin'] as $setting) { + $settingsManager->registerSetting('admin', $setting); + } + } + if (!empty($info['settings']['admin-section'])) { + foreach ($info['settings']['admin-section'] as $section) { + $settingsManager->registerSection('admin', $section); + } + } + if (!empty($info['settings']['personal'])) { + foreach ($info['settings']['personal'] as $setting) { + $settingsManager->registerSetting('personal', $setting); + } + } + if (!empty($info['settings']['personal-section'])) { + foreach ($info['settings']['personal-section'] as $section) { + $settingsManager->registerSection('personal', $section); + } + } + } + + if (!empty($info['collaboration']['plugins'])) { + // deal with one or many plugin entries + $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ? + [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin']; + $collaboratorSearch = null; + $autoCompleteManager = null; + foreach ($plugins as $plugin) { + if ($plugin['@attributes']['type'] === 'collaborator-search') { + $pluginInfo = [ + 'shareType' => $plugin['@attributes']['share-type'], + 'class' => $plugin['@value'], + ]; + $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class); + $collaboratorSearch->registerPlugin($pluginInfo); + } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') { + $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class); + $autoCompleteManager->registerSorter($plugin['@value']); + } + } + } + $eventLogger->end("bootstrap:load_app:$app:info"); + + $eventLogger->end("bootstrap:load_app:$app"); + } + /** + * Check if an app is loaded + * @param string $app app id + * @since 26.0.0 + */ + public function isAppLoaded(string $app): bool { + return isset($this->loadedApps[$app]); + } + + /** + * Load app.php from the given app + * + * @param string $app app name + * @throws \Error + */ + private static function requireAppFile(string $app): void { + // encapsulated here to avoid variable scope conflicts + require_once $app . '/appinfo/app.php'; + } + /** * Enable an app for every user * @@ -320,7 +542,8 @@ class AppManager implements IAppManager { $this->installedAppsCache[$appId] = 'yes'; $this->appConfig->setValue($appId, 'enabled', 'yes'); - $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( + $this->dispatcher->dispatchTyped(new AppEnableEvent($appId)); + $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( ManagerEvent::EVENT_APP_ENABLE, $appId )); $this->clearAppsCache(); @@ -345,7 +568,7 @@ class AppManager implements IAppManager { * Enable an app only for specific groups * * @param string $appId - * @param \OCP\IGroup[] $groups + * @param IGroup[] $groups * @param bool $forceEnable * @throws \InvalidArgumentException if app can't be enabled for groups * @throws AppPathNotFoundException @@ -363,8 +586,9 @@ class AppManager implements IAppManager { $this->ignoreNextcloudRequirementForApp($appId); } + /** @var string[] $groupIds */ $groupIds = array_map(function ($group) { - /** @var \OCP\IGroup $group */ + /** @var IGroup $group */ return ($group instanceof IGroup) ? $group->getGID() : $group; @@ -372,7 +596,8 @@ class AppManager implements IAppManager { $this->installedAppsCache[$appId] = json_encode($groupIds); $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds)); - $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( + $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds)); + $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups )); $this->clearAppsCache(); @@ -407,7 +632,8 @@ class AppManager implements IAppManager { \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']); } - $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent( + $this->dispatcher->dispatchTyped(new AppDisableEvent($appId)); + $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent( ManagerEvent::EVENT_APP_DISABLE, $appId )); $this->clearAppsCache(); @@ -556,7 +782,7 @@ class AppManager implements IAppManager { return in_array($appId, $this->shippedApps, true); } - private function isAlwaysEnabled($appId) { + private function isAlwaysEnabled(string $appId): bool { $alwaysEnabled = $this->getAlwaysEnabledApps(); return in_array($appId, $alwaysEnabled, true); } @@ -565,7 +791,7 @@ class AppManager implements IAppManager { * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson() * @throws \Exception */ - private function loadShippedJson() { + private function loadShippedJson(): void { if ($this->shippedApps === null) { $shippedJson = \OC::$SERVERROOT . '/core/shipped.json'; if (!file_exists($shippedJson)) { diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 04e76373466..96fa6bf7467 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -34,6 +34,7 @@ namespace OC; use OC\DB\Connection; use OC\DB\OracleConnection; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IAppConfig; use OCP\IConfig; @@ -111,6 +112,7 @@ class AppConfig implements IAppConfig { 'spreed' => [ '/^bridge_bot_password$/', '/^hosted-signaling-server-(.*)$/', + '/^recording_servers$/', '/^signaling_servers$/', '/^signaling_ticket_secret$/', '/^signaling_token_privkey_(.*)$/', @@ -298,7 +300,7 @@ class AppConfig implements IAppConfig { $sql->andWhere( $sql->expr()->orX( $sql->expr()->isNull('configvalue'), - $sql->expr()->neq('configvalue', $sql->createNamedParameter($value)) + $sql->expr()->neq('configvalue', $sql->createNamedParameter($value), IQueryBuilder::PARAM_STR) ) ); } diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index a78a895d029..9a6c298419a 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -34,6 +34,7 @@ use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Room\IBackend as IRoomBackend; use OCP\Collaboration\Reference\IReferenceProvider; use OCP\Talk\ITalkBackend; +use OCP\Translation\ITranslationProvider; use RuntimeException; use function array_shift; use OC\Support\CrashReport\Registry; @@ -113,6 +114,9 @@ class RegistrationContext { /** @var ServiceRegistration<ICustomTemplateProvider>[] */ private $templateProviders = []; + /** @var ServiceRegistration<ITranslationProvider>[] */ + private $translationProviders = []; + /** @var ServiceRegistration<INotifier>[] */ private $notifierServices = []; @@ -125,6 +129,9 @@ class RegistrationContext { /** @var ServiceRegistration<IReferenceProvider>[] */ private array $referenceProviders = []; + + + /** @var ParameterRegistration[] */ private $sensitiveMethods = []; @@ -252,6 +259,13 @@ class RegistrationContext { ); } + public function registerTranslationProvider(string $providerClass): void { + $this->context->registerTranslationProvider( + $this->appId, + $providerClass + ); + } + public function registerNotifierService(string $notifierClass): void { $this->context->registerNotifierService( $this->appId, @@ -404,6 +418,10 @@ class RegistrationContext { $this->templateProviders[] = new ServiceRegistration($appId, $class); } + public function registerTranslationProvider(string $appId, string $class): void { + $this->translationProviders[] = new ServiceRegistration($appId, $class); + } + public function registerNotifierService(string $appId, string $class): void { $this->notifierServices[] = new ServiceRegistration($appId, $class); } @@ -675,6 +693,13 @@ class RegistrationContext { } /** + * @return ServiceRegistration<ITranslationProvider>[] + */ + public function getTranslationProviders(): array { + return $this->translationProviders; + } + + /** * @return ServiceRegistration<INotifier>[] */ public function getNotifierServices(): array { diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 9b202f07fbf..9a9740b7bcc 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -292,7 +292,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { new OC\AppFramework\Middleware\Security\BruteForceMiddleware( $c->get(IControllerMethodReflector::class), $c->get(OC\Security\Bruteforce\Throttler::class), - $c->get(IRequest::class) + $c->get(IRequest::class), + $c->get(LoggerInterface::class) ) ); $dispatcher->registerMiddleware( diff --git a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php index 069d04a9e75..ba8c7f45b49 100644 --- a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php @@ -3,6 +3,7 @@ declare(strict_types=1); /** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> * * @author Christoph Wurst <christoph@winzerhof-wurst.at> @@ -31,6 +32,7 @@ use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TooManyRequestsResponse; use OCP\AppFramework\Middleware; @@ -38,6 +40,8 @@ use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\Security\Bruteforce\MaxDelayReached; +use Psr\Log\LoggerInterface; +use ReflectionMethod; /** * Class BruteForceMiddleware performs the bruteforce protection for controllers @@ -47,16 +51,12 @@ use OCP\Security\Bruteforce\MaxDelayReached; * @package OC\AppFramework\Middleware\Security */ class BruteForceMiddleware extends Middleware { - private ControllerMethodReflector $reflector; - private Throttler $throttler; - private IRequest $request; - - public function __construct(ControllerMethodReflector $controllerMethodReflector, - Throttler $throttler, - IRequest $request) { - $this->reflector = $controllerMethodReflector; - $this->throttler = $throttler; - $this->request = $request; + public function __construct( + protected ControllerMethodReflector $reflector, + protected Throttler $throttler, + protected IRequest $request, + protected LoggerInterface $logger, + ) { } /** @@ -68,6 +68,20 @@ class BruteForceMiddleware extends Middleware { if ($this->reflector->hasAnnotation('BruteForceProtection')) { $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action'); $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $action); + } else { + $reflectionMethod = new ReflectionMethod($controller, $methodName); + $attributes = $reflectionMethod->getAttributes(BruteForceProtection::class); + + if (!empty($attributes)) { + $remoteAddress = $this->request->getRemoteAddress(); + + foreach ($attributes as $attribute) { + /** @var BruteForceProtection $protection */ + $protection = $attribute->newInstance(); + $action = $protection->getAction(); + $this->throttler->sleepDelayOrThrowOnMax($remoteAddress, $action); + } + } } } @@ -75,11 +89,34 @@ class BruteForceMiddleware extends Middleware { * {@inheritDoc} */ public function afterController($controller, $methodName, Response $response) { - if ($this->reflector->hasAnnotation('BruteForceProtection') && $response->isThrottled()) { - $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action'); - $ip = $this->request->getRemoteAddress(); - $this->throttler->sleepDelay($ip, $action); - $this->throttler->registerAttempt($action, $ip, $response->getThrottleMetadata()); + if ($response->isThrottled()) { + if ($this->reflector->hasAnnotation('BruteForceProtection')) { + $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action'); + $ip = $this->request->getRemoteAddress(); + $this->throttler->sleepDelay($ip, $action); + $this->throttler->registerAttempt($action, $ip, $response->getThrottleMetadata()); + } else { + $reflectionMethod = new ReflectionMethod($controller, $methodName); + $attributes = $reflectionMethod->getAttributes(BruteForceProtection::class); + + if (!empty($attributes)) { + $ip = $this->request->getRemoteAddress(); + $metaData = $response->getThrottleMetadata(); + + foreach ($attributes as $attribute) { + /** @var BruteForceProtection $protection */ + $protection = $attribute->newInstance(); + $action = $protection->getAction(); + + if (!isset($metaData['action']) || $metaData['action'] === $action) { + $this->throttler->sleepDelay($ip, $action); + $this->throttler->registerAttempt($action, $ip, $metaData); + } + } + } else { + $this->logger->debug('Response for ' . get_class($controller) . '::' . $methodName . ' got bruteforce throttled but has no annotation nor attribute defined.'); + } + } } return parent::afterController($controller, $methodName, $response); diff --git a/lib/private/AppFramework/Utility/TimeFactory.php b/lib/private/AppFramework/Utility/TimeFactory.php index 27117ed3cfc..1e4655dd1cd 100644 --- a/lib/private/AppFramework/Utility/TimeFactory.php +++ b/lib/private/AppFramework/Utility/TimeFactory.php @@ -3,6 +3,7 @@ declare(strict_types=1); /** + * @copyright Copyright (c) 2022, Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Bernhard Posselt <dev@bernhard-posselt.com> @@ -30,11 +31,23 @@ namespace OC\AppFramework\Utility; use OCP\AppFramework\Utility\ITimeFactory; /** - * Needed to mock calls to time() + * Use this to get a timestamp or DateTime object in code to remain testable + * + * @since 8.0.0 + * @since 26.0.0 Extends the \Psr\Clock\ClockInterface interface + * @ref https://www.php-fig.org/psr/psr-20/#21-clockinterface */ class TimeFactory implements ITimeFactory { + protected \DateTimeZone $timezone; + + public function __construct() { + $this->timezone = new \DateTimeZone('UTC'); + } + /** * @return int the result of a call to time() + * @since 8.0.0 + * @deprecated 26.0.0 {@see ITimeFactory::now()} */ public function getTime(): int { return time(); @@ -45,8 +58,19 @@ class TimeFactory implements ITimeFactory { * @param \DateTimeZone $timezone * @return \DateTime * @since 15.0.0 + * @deprecated 26.0.0 {@see ITimeFactory::now()} */ public function getDateTime(string $time = 'now', \DateTimeZone $timezone = null): \DateTime { return new \DateTime($time, $timezone); } + + public function now(): \DateTimeImmutable { + return new \DateTimeImmutable('now', $this->timezone); + } + public function withTimeZone(\DateTimeZone $timezone): static { + $clone = clone $this; + $clone->timezone = $timezone; + + return $clone; + } } diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 59c7ca714c6..761e799d298 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -32,8 +32,9 @@ use OC\Authentication\Exceptions\ExpiredTokenException; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IProvider as OCPIProvider; -class Manager implements IProvider { +class Manager implements IProvider, OCPIProvider { /** @var PublicKeyTokenProvider */ private $publicKeyTokenProvider; @@ -239,4 +240,13 @@ class Manager implements IProvider { public function updatePasswords(string $uid, string $password) { $this->publicKeyTokenProvider->updatePasswords($uid, $password); } + + public function invalidateTokensOfUser(string $uid, ?string $clientName) { + $tokens = $this->getTokenByUser($uid); + foreach ($tokens as $token) { + if ($clientName === null || ($token->getName() === $clientName)) { + $this->invalidateTokenById($uid, $token->getId()); + } + } + } } diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 38bbef8fb61..824e2e056c8 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -113,7 +113,7 @@ class PublicKeyTokenProvider implements IProvider { // We need to check against one old token to see if there is a password // hash that we can reuse for detecting outdated passwords $randomOldToken = $this->mapper->getFirstTokenForUser($uid); - $oldTokenMatches = $randomOldToken && $randomOldToken->getPasswordHash() && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash()); + $oldTokenMatches = $randomOldToken && $randomOldToken->getPasswordHash() && $password !== null && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash()); $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); diff --git a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php index 95e49cdf860..d423a830495 100644 --- a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php +++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php @@ -25,8 +25,8 @@ declare(strict_types=1); namespace OC\Collaboration\Reference\File; use OC\User\NoUserException; +use OCP\Collaboration\Reference\ADiscoverableReferenceProvider; use OCP\Collaboration\Reference\IReference; -use OCP\Collaboration\Reference\IReferenceProvider; use OCP\Collaboration\Reference\Reference; use OCP\Files\IMimeTypeDetector; use OCP\Files\InvalidPathException; @@ -34,27 +34,34 @@ use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\IL10N; use OCP\IPreview; use OCP\IURLGenerator; use OCP\IUserSession; +use OCP\L10N\IFactory; -class FileReferenceProvider implements IReferenceProvider { +class FileReferenceProvider extends ADiscoverableReferenceProvider { private IURLGenerator $urlGenerator; private IRootFolder $rootFolder; private ?string $userId; private IPreview $previewManager; private IMimeTypeDetector $mimeTypeDetector; - - public function __construct(IURLGenerator $urlGenerator, - IRootFolder $rootFolder, - IUserSession $userSession, - IMimeTypeDetector $mimeTypeDetector, - IPreview $previewManager) { + private IL10N $l10n; + + public function __construct( + IURLGenerator $urlGenerator, + IRootFolder $rootFolder, + IUserSession $userSession, + IMimeTypeDetector $mimeTypeDetector, + IPreview $previewManager, + IFactory $l10n + ) { $this->urlGenerator = $urlGenerator; $this->rootFolder = $rootFolder; $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; $this->previewManager = $previewManager; $this->mimeTypeDetector = $mimeTypeDetector; + $this->l10n = $l10n->get('files'); } public function matchReference(string $referenceText): bool { @@ -145,9 +152,10 @@ class FileReferenceProvider implements IReferenceProvider { 'id' => $file->getId(), 'name' => $file->getName(), 'size' => $file->getSize(), - 'path' => $file->getPath(), + 'path' => $userFolder->getRelativePath($file->getPath()), 'link' => $reference->getUrl(), 'mimetype' => $file->getMimetype(), + 'mtime' => $file->getMTime(), 'preview-available' => $this->previewManager->isAvailable($file) ]); } catch (InvalidPathException|NotFoundException|NotPermittedException|NoUserException $e) { @@ -162,4 +170,20 @@ class FileReferenceProvider implements IReferenceProvider { public function getCacheKey(string $referenceId): ?string { return $this->userId ?? ''; } + + public function getId(): string { + return 'files'; + } + + public function getTitle(): string { + return $this->l10n->t('Files'); + } + + public function getOrder(): int { + return 0; + } + + public function getIconUrl(): string { + return $this->urlGenerator->imagePath('files', 'folder.svg'); + } } diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index 00cf323bfbf..c5fb4ebfe34 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -1031,6 +1031,7 @@ class Manager implements ICommentsManager { ->select('message_id') ->from('reactions') ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId))) + ->orderBy('message_id', 'DESC') ->executeQuery(); $commentIds = []; @@ -1106,22 +1107,29 @@ class Manager implements ICommentsManager { if (!$commentIds) { return []; } - $query = $this->dbConn->getQueryBuilder(); + $chunks = array_chunk($commentIds, 500); + + $query = $this->dbConn->getQueryBuilder(); $query->select('*') ->from('comments') - ->where($query->expr()->in('id', $query->createNamedParameter($commentIds, IQueryBuilder::PARAM_STR_ARRAY))) + ->where($query->expr()->in('id', $query->createParameter('ids'))) ->orderBy('creation_timestamp', 'DESC') ->addOrderBy('id', 'DESC'); $comments = []; - $result = $query->executeQuery(); - while ($data = $result->fetch()) { - $comment = $this->getCommentFromData($data); - $this->cache($comment); - $comments[] = $comment; + foreach ($chunks as $ids) { + $query->setParameter('ids', $ids, IQueryBuilder::PARAM_STR_ARRAY); + + $result = $query->executeQuery(); + while ($data = $result->fetch()) { + $comment = $this->getCommentFromData($data); + $this->cache($comment); + $comments[] = $comment; + } + $result->closeCursor(); } - $result->closeCursor(); + return $comments; } diff --git a/lib/private/Config.php b/lib/private/Config.php index a9ecaf2c825..3ea822101df 100644 --- a/lib/private/Config.php +++ b/lib/private/Config.php @@ -286,10 +286,12 @@ class Config { } // Never write file back if disk space should be too low - $df = disk_free_space($this->configDir); - $size = strlen($content) + 10240; - if ($df !== false && $df < (float)$size) { - throw new \Exception($this->configDir . " does not have enough space for writing the config file! Not writing it back!"); + if (function_exists('disk_free_space')) { + $df = disk_free_space($this->configDir); + $size = strlen($content) + 10240; + if ($df !== false && $df < (float)$size) { + throw new \Exception($this->configDir . " does not have enough space for writing the config file! Not writing it back!"); + } } // Try to acquire a file lock diff --git a/lib/private/DB/SQLiteMigrator.php b/lib/private/DB/SQLiteMigrator.php index 2be3591afdc..cbb39070a48 100644 --- a/lib/private/DB/SQLiteMigrator.php +++ b/lib/private/DB/SQLiteMigrator.php @@ -39,9 +39,13 @@ class SQLiteMigrator extends Migrator { $platform->registerDoctrineTypeMapping('smallint unsigned', 'integer'); $platform->registerDoctrineTypeMapping('varchar ', 'string'); - // with sqlite autoincrement columns is of type integer foreach ($targetSchema->getTables() as $table) { foreach ($table->getColumns() as $column) { + // column comments are not supported on SQLite + if ($column->getComment() !== null) { + $column->setComment(null); + } + // with sqlite autoincrement columns is of type integer if ($column->getType() instanceof BigIntType && $column->getAutoincrement()) { $column->setType(Type::getType('integer')); } diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 6440bf05a1d..9afeea7b573 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -575,7 +575,7 @@ class Cache implements ICache { } /** - * Recursively remove all children of a folder + * Remove all children of a folder * * @param ICacheEntry $entry the cache entry of the folder to remove the children of * @throws \OC\DatabaseException @@ -583,6 +583,8 @@ class Cache implements ICache { private function removeChildren(ICacheEntry $entry) { $parentIds = [$entry->getId()]; $queue = [$entry->getId()]; + $deletedIds = []; + $deletedPaths = []; // we walk depth first through the file tree, removing all filecache_extended attributes while we walk // and collecting all folder ids to later use to delete the filecache entries @@ -591,6 +593,12 @@ class Cache implements ICache { $childIds = array_map(function (ICacheEntry $cacheEntry) { return $cacheEntry->getId(); }, $children); + $childPaths = array_map(function (ICacheEntry $cacheEntry) { + return $cacheEntry->getPath(); + }, $children); + + $deletedIds = array_merge($deletedIds, $childIds); + $deletedPaths = array_merge($deletedPaths, $childPaths); $query = $this->getQueryBuilder(); $query->delete('filecache_extended') @@ -619,6 +627,16 @@ class Cache implements ICache { $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY); $query->execute(); } + + foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) { + $cacheEntryRemovedEvent = new CacheEntryRemovedEvent( + $this->storage, + $filePath, + $fileId, + $this->getNumericStorageId() + ); + $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent); + } } /** diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index fe677c5ea52..a60b39823c5 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -130,18 +130,25 @@ class UserMountCache implements IUserMountCache { $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts); - foreach ($addedMounts as $mount) { - $this->addToCache($mount); - /** @psalm-suppress InvalidArgument */ - $this->mountsForUsers[$user->getUID()][] = $mount; - } - foreach ($removedMounts as $mount) { - $this->removeFromCache($mount); - $index = array_search($mount, $this->mountsForUsers[$user->getUID()]); - unset($this->mountsForUsers[$user->getUID()][$index]); - } - foreach ($changedMounts as $mount) { - $this->updateCachedMount($mount); + $this->connection->beginTransaction(); + try { + foreach ($addedMounts as $mount) { + $this->addToCache($mount); + /** @psalm-suppress InvalidArgument */ + $this->mountsForUsers[$user->getUID()][] = $mount; + } + foreach ($removedMounts as $mount) { + $this->removeFromCache($mount); + $index = array_search($mount, $this->mountsForUsers[$user->getUID()]); + unset($this->mountsForUsers[$user->getUID()][$index]); + } + foreach ($changedMounts as $mount) { + $this->updateCachedMount($mount); + } + $this->connection->commit(); + } catch (\Throwable $e) { + $this->connection->rollBack(); + throw $e; } $this->eventLogger->end('fs:setup:user:register'); } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 90aed642a2d..2c376fe5885 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Christoph Wurst <christoph@winzerhof-wurst.at> @@ -68,7 +69,7 @@ class Folder extends Node implements \OCP\Files\Folder { public function getFullPath($path) { $path = $this->normalizePath($path); if (!$this->isValidPath($path)) { - throw new NotPermittedException('Invalid path'); + throw new NotPermittedException('Invalid path "' . $path . '"'); } return $this->path . $path; } @@ -163,14 +164,14 @@ class Folder extends Node implements \OCP\Files\Folder { $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath); $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]); if (!$this->view->mkdir($fullPath)) { - throw new NotPermittedException('Could not create folder'); + throw new NotPermittedException('Could not create folder "' . $fullPath . '"'); } $parent = dirname($fullPath) === $this->getPath() ? $this : null; $node = new Folder($this->root, $this->view, $fullPath, null, $parent); $this->sendHooks(['postWrite', 'postCreate'], [$node]); return $node; } else { - throw new NotPermittedException('No create permission for folder'); + throw new NotPermittedException('No create permission for folder "' . $path . '"'); } } @@ -194,13 +195,13 @@ class Folder extends Node implements \OCP\Files\Folder { $result = $this->view->touch($fullPath); } if ($result === false) { - throw new NotPermittedException('Could not create path'); + throw new NotPermittedException('Could not create path "' . $fullPath . '"'); } $node = new File($this->root, $this->view, $fullPath, null, $this); $this->sendHooks(['postWrite', 'postCreate'], [$node]); return $node; } - throw new NotPermittedException('No create permission for path'); + throw new NotPermittedException('No create permission for path "' . $path . '"'); } private function queryFromOperator(ISearchOperator $operator, string $uid = null): ISearchQuery { @@ -230,7 +231,7 @@ class Folder extends Node implements \OCP\Files\Folder { $limitToHome = $query->limitToHome(); if ($limitToHome && count(explode('/', $this->path)) !== 3) { - throw new \InvalidArgumentException('searching by owner is only allows on the users home folder'); + throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder'); } $rootLength = strlen($this->path); @@ -392,7 +393,7 @@ class Folder extends Node implements \OCP\Files\Folder { $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo); $this->sendHooks(['postDelete'], [$nonExisting]); } else { - throw new NotPermittedException('No delete permission for path'); + throw new NotPermittedException('No delete permission for path "' . $this->path . '"'); } } diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php index c85a356ddd3..81009532dbf 100644 --- a/lib/private/Files/Node/LazyUserFolder.php +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -26,6 +26,7 @@ namespace OC\Files\Node; use OCP\Files\FileInfo; use OCP\Constants; use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountManager; use OCP\Files\NotFoundException; use OCP\IUser; @@ -33,10 +34,12 @@ class LazyUserFolder extends LazyFolder { private IRootFolder $root; private IUser $user; private string $path; + private IMountManager $mountManager; - public function __construct(IRootFolder $rootFolder, IUser $user) { + public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager $mountManager) { $this->root = $rootFolder; $this->user = $user; + $this->mountManager = $mountManager; $this->path = '/' . $user->getUID() . '/files'; parent::__construct(function () use ($user) { try { @@ -61,9 +64,20 @@ class LazyUserFolder extends LazyFolder { /** * @param int $id - * @return \OC\Files\Node\Node[] + * @return \OCP\Files\Node[] */ public function getById($id) { return $this->root->getByIdInPath((int)$id, $this->getPath()); } + + public function getMountPoint() { + if ($this->folder !== null) { + return $this->folder->getMountPoint(); + } + $mountPoint = $this->mountManager->find('/' . $this->user->getUID()); + if (is_null($mountPoint)) { + throw new \Exception("No mountpoint for user folder"); + } + return $mountPoint; + } } diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 29cdbb987c3..e9fb14e5364 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -395,7 +395,7 @@ class Root extends Folder implements IRootFolder { $folder = $this->newFolder('/' . $userId . '/files'); } } else { - $folder = new LazyUserFolder($this, $userObject); + $folder = new LazyUserFolder($this, $userObject, $this->mountManager); } $this->userFolderCache->set($userId, $folder); diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index d0c5bd14b38..4ca00cf6a16 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -29,6 +29,8 @@ */ namespace OC\Files\ObjectStore; +use Aws\S3\Exception\S3Exception; +use Aws\S3\Exception\S3MultipartUploadException; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; @@ -37,11 +39,14 @@ use OC\Files\Cache\CacheEntry; use OC\Files\Storage\PolyFill\CopyDirectory; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; +use OCP\Files\GenericFileException; use OCP\Files\NotFoundException; use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; +use OCP\Files\Storage\IChunkedFileWrite; use OCP\Files\Storage\IStorage; -class ObjectStoreStorage extends \OC\Files\Storage\Common { +class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite { use CopyDirectory; /** @@ -91,7 +96,6 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { public function mkdir($path) { $path = $this->normalizePath($path); - if ($this->file_exists($path)) { return false; } @@ -627,4 +631,72 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { throw $e; } } + + public function startChunkedWrite(string $targetPath): string { + if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) { + throw new GenericFileException('Object store does not support multipart upload'); + } + $cacheEntry = $this->getCache()->get($targetPath); + $urn = $this->getURN($cacheEntry->getId()); + return $this->objectStore->initiateMultipartUpload($urn); + } + + /** + * + * @throws GenericFileException + */ + public function putChunkedWritePart(string $targetPath, string $writeToken, string $chunkId, $data, $size = null): ?array { + if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) { + throw new GenericFileException('Object store does not support multipart upload'); + } + $cacheEntry = $this->getCache()->get($targetPath); + $urn = $this->getURN($cacheEntry->getId()); + + $result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size); + + $parts[$chunkId] = [ + 'PartNumber' => $chunkId, + 'ETag' => trim($result->get('ETag'), '"') + ]; + return $parts[$chunkId]; + } + + public function completeChunkedWrite(string $targetPath, string $writeToken): int { + if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) { + throw new GenericFileException('Object store does not support multipart upload'); + } + $cacheEntry = $this->getCache()->get($targetPath); + $urn = $this->getURN($cacheEntry->getId()); + $parts = $this->objectStore->getMultipartUploads($urn, $writeToken); + $sortedParts = array_values($parts); + sort($sortedParts); + try { + $size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts); + $stat = $this->stat($targetPath); + $mtime = time(); + if (is_array($stat)) { + $stat['size'] = $size; + $stat['mtime'] = $mtime; + $stat['mimetype'] = $this->getMimeType($targetPath); + $this->getCache()->update($stat['fileid'], $stat); + } + } catch (S3MultipartUploadException | S3Exception $e) { + $this->objectStore->abortMultipartUpload($urn, $writeToken); + $this->logger->logException($e, [ + 'app' => 'objectstore', + 'message' => 'Could not compete multipart upload ' . $urn. ' with uploadId ' . $writeToken + ]); + throw new GenericFileException('Could not write chunked file'); + } + return $size; + } + + public function cancelChunkedWrite(string $targetPath, string $writeToken): void { + if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) { + throw new GenericFileException('Object store does not support multipart upload'); + } + $cacheEntry = $this->getCache()->get($targetPath); + $urn = $this->getURN($cacheEntry->getId()); + $this->objectStore->abortMultipartUpload($urn, $writeToken); + } } diff --git a/lib/private/Files/ObjectStore/S3.php b/lib/private/Files/ObjectStore/S3.php index 6492145fb63..ebc8886f12d 100644 --- a/lib/private/Files/ObjectStore/S3.php +++ b/lib/private/Files/ObjectStore/S3.php @@ -23,9 +23,12 @@ */ namespace OC\Files\ObjectStore; +use Aws\Result; +use Exception; use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; -class S3 implements IObjectStore { +class S3 implements IObjectStore, IObjectStoreMultiPartUpload { use S3ConnectionTrait; use S3ObjectTrait; @@ -41,4 +44,59 @@ class S3 implements IObjectStore { public function getStorageId() { return $this->id; } + + public function initiateMultipartUpload(string $urn): string { + $upload = $this->getConnection()->createMultipartUpload([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + ]); + $uploadId = $upload->get('UploadId'); + if ($uploadId === null) { + throw new Exception('No upload id returned'); + } + return (string)$uploadId; + } + + public function uploadMultipartPart(string $urn, string $uploadId, int $partId, $stream, $size): Result { + return $this->getConnection()->uploadPart([ + 'Body' => $stream, + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'ContentLength' => $size, + 'PartNumber' => $partId, + 'UploadId' => $uploadId, + ]); + } + + public function getMultipartUploads(string $urn, string $uploadId): array { + $parts = $this->getConnection()->listParts([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'UploadId' => $uploadId, + 'MaxParts' => 10000 + ]); + return $parts->get('Parts') ?? []; + } + + public function completeMultipartUpload(string $urn, string $uploadId, array $result): int { + $this->getConnection()->completeMultipartUpload([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'UploadId' => $uploadId, + 'MultipartUpload' => ['Parts' => $result], + ]); + $stat = $this->getConnection()->headObject([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + ]); + return (int)$stat->get('ContentLength'); + } + + public function abortMultipartUpload($urn, $uploadId): void { + $this->getConnection()->abortMultipartUpload([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'UploadId' => $uploadId + ]); + } } diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 456f804ee56..1bd131303e3 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -164,7 +164,7 @@ class View { * get path relative to the root of the view * * @param string $path - * @return string + * @return ?string */ public function getRelativePath($path) { $this->assertPathLength($path); @@ -1241,7 +1241,7 @@ class View { * get the path relative to the default root for hook usage * * @param string $path - * @return string + * @return ?string */ private function getHookPath($path) { if (!Filesystem::getView()) { diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index ae70a611e4e..cca179bfe19 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\BeforeGroupChangedEvent; use OCP\Group\Events\GroupChangedEvent; use OCP\GroupInterface; use OCP\IGroup; @@ -109,6 +110,7 @@ class Group implements IGroup { public function setDisplayName(string $displayName): bool { $displayName = trim($displayName); if ($displayName !== '') { + $this->dispatcher->dispatch(new BeforeGroupChangedEvent($this, 'displayName', $displayName, $this->displayName)); foreach ($this->backends as $backend) { if (($backend instanceof ISetDisplayNameBackend) && $backend->setDisplayName($this->gid, $displayName)) { diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php index 5f806be0ae5..78843de7206 100644 --- a/lib/private/Log/ExceptionSerializer.php +++ b/lib/private/Log/ExceptionSerializer.php @@ -100,6 +100,16 @@ class ExceptionSerializer { // Preview providers, don't log big data strings 'imagecreatefromstring', + + // text: PublicSessionController, SessionController and ApiService + 'create', + 'close', + 'push', + 'sync', + 'updateSession', + 'mention', + 'loginSessionUser', + ]; /** @var SystemConfig */ diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index f4094e0bef6..b6f96fffba4 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -74,7 +74,7 @@ class Redis extends Cache implements IMemcacheTTL { } public function remove($key) { - if ($this->getCache()->del($this->getPrefix() . $key)) { + if ($this->getCache()->unlink($this->getPrefix() . $key)) { return true; } else { return false; @@ -170,7 +170,7 @@ class Redis extends Cache implements IMemcacheTTL { $this->getCache()->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { $result = $this->getCache()->multi() - ->del($this->getPrefix() . $key) + ->unlink($this->getPrefix() . $key) ->exec(); return $result !== false; } diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index 5d559b65f00..ca46383e58b 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -82,8 +82,14 @@ class Imaginary extends ProviderV2 { $httpClient = $this->service->newClient(); $convert = false; + $autorotate = true; switch ($file->getMimeType()) { + case 'image/heic': + // Autorotate seems to be broken for Heic so disable for that + $autorotate = false; + $mimeType = 'jpeg'; + break; case 'image/gif': case 'image/png': $mimeType = 'png'; @@ -92,50 +98,43 @@ class Imaginary extends ProviderV2 { case 'application/pdf': case 'application/illustrator': $convert = true; + // Converted files do not need to be autorotated + $autorotate = false; + $mimeType = 'png'; break; default: $mimeType = 'jpeg'; } + + $operations = []; if ($convert) { - $operations = [ - [ - 'operation' => 'convert', - 'params' => [ - 'type' => 'png', - ] - ], - [ - 'operation' => ($crop ? 'smartcrop' : 'fit'), - 'params' => [ - 'width' => $maxX, - 'height' => $maxY, - 'type' => 'png', - 'norotation' => 'true', - ] + $operations[] = [ + 'operation' => 'convert', + 'params' => [ + 'type' => $mimeType, ] ]; - } else { - $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); - - $operations = [ - [ - 'operation' => 'autorotate', - ], - [ - 'operation' => ($crop ? 'smartcrop' : 'fit'), - 'params' => [ - 'width' => $maxX, - 'height' => $maxY, - 'stripmeta' => 'true', - 'type' => $mimeType, - 'norotation' => 'true', - 'quality' => $quality, - ] - ] + } elseif ($autorotate) { + $operations[] = [ + 'operation' => 'autorotate', ]; } + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + + $operations[] = [ + 'operation' => ($crop ? 'smartcrop' : 'fit'), + 'params' => [ + 'width' => $maxX, + 'height' => $maxY, + 'stripmeta' => 'true', + 'type' => $mimeType, + 'norotation' => 'true', + 'quality' => $quality, + ] + ]; + try { $response = $httpClient->post( $imaginaryUrl . '/pipeline', [ diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php index 486c301d987..13d868cd583 100644 --- a/lib/private/Preview/Movie.php +++ b/lib/private/Preview/Movie.php @@ -125,23 +125,30 @@ class Movie extends ProviderV2 { $binaryType = substr(strrchr($this->binary, '/'), 1); if ($binaryType === 'avconv') { - $cmd = $this->binary . ' -y -ss ' . escapeshellarg((string)$second) . - ' -i ' . escapeshellarg($absPath) . - ' -an -f mjpeg -vframes 1 -vsync 1 ' . escapeshellarg($tmpPath) . - ' 2>&1'; + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-an', '-f', 'mjpeg', '-vframes', '1', '-vsync', '1', + $tmpPath]; } elseif ($binaryType === 'ffmpeg') { - $cmd = $this->binary . ' -y -ss ' . escapeshellarg((string)$second) . - ' -i ' . escapeshellarg($absPath) . - ' -f mjpeg -vframes 1' . - ' ' . escapeshellarg($tmpPath) . - ' 2>&1'; + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-f', 'mjpeg', '-vframes', '1', + $tmpPath]; } else { // Not supported unlink($tmpPath); return null; } - exec($cmd, $output, $returnCode); + $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + $returnCode = -1; + $output = ""; + if (is_resource($proc)) { + $stdout = trim(stream_get_contents($pipes[1])); + $stderr = trim(stream_get_contents($pipes[2])); + $returnCode = proc_close($proc); + $output = $stdout . $stderr; + } if ($returnCode === 0) { $image = new \OCP\Image(); diff --git a/lib/private/Repair/NC21/ValidatePhoneNumber.php b/lib/private/Repair/NC21/ValidatePhoneNumber.php index f9c3c5952bf..b3534dbeae8 100644 --- a/lib/private/Repair/NC21/ValidatePhoneNumber.php +++ b/lib/private/Repair/NC21/ValidatePhoneNumber.php @@ -55,7 +55,8 @@ class ValidatePhoneNumber implements IRepairStep { public function run(IOutput $output): void { if ($this->config->getSystemValueString('default_phone_region', '') === '') { - throw new \Exception('Can not validate phone numbers without `default_phone_region` being set in the config file'); + $output->warning('Can not validate phone numbers without `default_phone_region` being set in the config file'); + return; } $numUpdated = 0; diff --git a/lib/private/Repair/RemoveLinkShares.php b/lib/private/Repair/RemoveLinkShares.php index e1ce78cdbf3..71eead1053b 100644 --- a/lib/private/Repair/RemoveLinkShares.php +++ b/lib/private/Repair/RemoveLinkShares.php @@ -126,7 +126,7 @@ class RemoveLinkShares implements IRepairStep { $query = $this->connection->getQueryBuilder(); $query->select($query->func()->count('*', 'total')) ->from('share') - ->where($query->expr()->in('id', $query->createFunction('(' . $subQuery->getSQL() . ')'))); + ->where($query->expr()->in('id', $query->createFunction($subQuery->getSQL()))); $result = $query->execute(); $data = $result->fetch(); diff --git a/lib/private/Server.php b/lib/private/Server.php index 35f63686457..f1e96170886 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -152,6 +152,7 @@ use OC\SystemTag\ManagerFactory as SystemTagManagerFactory; use OC\Tagging\TagMapper; use OC\Talk\Broker; use OC\Template\JSCombiner; +use OC\Translation\TranslationManager; use OC\User\DisplayNameCache; use OC\User\Listeners\BeforeUserDeletedListener; use OC\User\Listeners\UserChangedListener; @@ -162,6 +163,7 @@ use OCA\Theming\Util; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\Authentication\LoginCredentials\IStore; +use OCP\Authentication\Token\IProvider as OCPIProvider; use OCP\BackgroundJob\IJobList; use OCP\Collaboration\AutoComplete\IManager; use OCP\Collaboration\Reference\IReferenceManager; @@ -247,6 +249,7 @@ use OCP\Share\IShareHelper; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\Talk\IBroker; +use OCP\Translation\ITranslationManager; use OCP\User\Events\BeforePasswordUpdatedEvent; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\BeforeUserLoggedInEvent; @@ -548,6 +551,7 @@ class Server extends ServerContainer implements IServerContainer { }); $this->registerAlias(IStore::class, Store::class); $this->registerAlias(IProvider::class, Authentication\Token\Manager::class); + $this->registerAlias(OCPIProvider::class, Authentication\Token\Manager::class); $this->registerService(\OC\User\Session::class, function (Server $c) { $manager = $c->get(IUserManager::class); @@ -926,6 +930,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(IGroupManager::class), $c->get(ICacheFactory::class), $c->get(SymfonyAdapter::class), + $c->get(IEventDispatcher::class), $c->get(LoggerInterface::class) ); }); @@ -1385,6 +1390,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('ControllerMethodReflector', \OCP\AppFramework\Utility\IControllerMethodReflector::class); $this->registerAlias(\OCP\AppFramework\Utility\ITimeFactory::class, \OC\AppFramework\Utility\TimeFactory::class); + $this->registerAlias(\Psr\Clock\ClockInterface::class, \OCP\AppFramework\Utility\ITimeFactory::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('TimeFactory', \OCP\AppFramework\Utility\ITimeFactory::class); @@ -1453,6 +1459,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\Share\IPublicShareTemplateFactory::class, \OC\Share20\PublicShareTemplateFactory::class); + $this->registerAlias(ITranslationManager::class, TranslationManager::class); + $this->connectDispatcher(); } diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index 94719a742e2..9ec4137cdef 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -57,6 +57,8 @@ abstract class AbstractDatabase { protected $logger; /** @var ISecureRandom */ protected $random; + /** @var bool */ + protected $tryCreateDbUser; public function __construct(IL10N $trans, SystemConfig $config, LoggerInterface $logger, ISecureRandom $random) { $this->trans = $trans; @@ -88,6 +90,10 @@ abstract class AbstractDatabase { $dbPort = !empty($config['dbport']) ? $config['dbport'] : ''; $dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_'; + $createUserConfig = $this->config->getValue("setup_create_db_user", true); + // accept `false` both as bool and string, since setting config values from env will result in a string + $this->tryCreateDbUser = $createUserConfig !== false && $createUserConfig !== "false"; + $this->config->setValues([ 'dbname' => $dbName, 'dbhost' => $dbHost, diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index caa73edccec..50f566728a9 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -49,7 +49,14 @@ class MySQL extends AbstractDatabase { $connection = $this->connect(['dbname' => null]); } - $this->createSpecificUser($username, new ConnectionAdapter($connection)); + if ($this->tryCreateDbUser) { + $this->createSpecificUser($username, new ConnectionAdapter($connection)); + } + + $this->config->setValues([ + 'dbuser' => $this->dbUser, + 'dbpassword' => $this->dbPassword, + ]); //create the database $this->createDatabase($connection); @@ -147,8 +154,7 @@ class MySQL extends AbstractDatabase { . $this->random->generate(2, ISecureRandom::CHAR_UPPER) . $this->random->generate(2, ISecureRandom::CHAR_LOWER) . $this->random->generate(2, ISecureRandom::CHAR_DIGITS) - . $this->random->generate(2, $saveSymbols) - ; + . $this->random->generate(2, $saveSymbols); $this->dbPassword = str_shuffle($password); try { @@ -196,10 +202,5 @@ class MySQL extends AbstractDatabase { $this->dbUser = $rootUser; $this->dbPassword = $rootPassword; } - - $this->config->setValues([ - 'dbuser' => $this->dbUser, - 'dbpassword' => $this->dbPassword, - ]); } } diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php index af816c7ad04..490cbba69a9 100644 --- a/lib/private/Setup/PostgreSQL.php +++ b/lib/private/Setup/PostgreSQL.php @@ -45,42 +45,44 @@ class PostgreSQL extends AbstractDatabase { $connection = $this->connect([ 'dbname' => 'postgres' ]); - //check for roles creation rights in postgresql - $builder = $connection->getQueryBuilder(); - $builder->automaticTablePrefix(false); - $query = $builder - ->select('rolname') - ->from('pg_roles') - ->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE'))) - ->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser))); - - try { - $result = $query->execute(); - $canCreateRoles = $result->rowCount() > 0; - } catch (DatabaseException $e) { - $canCreateRoles = false; - } - - 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 - $this->dbUser = 'oc_' . strtolower($username); - //create a new password so we don't need to store the admin config in the config file - $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(); + if ($this->tryCreateDbUser) { + //check for roles creation rights in postgresql + $builder = $connection->getQueryBuilder(); + $builder->automaticTablePrefix(false); + $query = $builder + ->select('rolname') + ->from('pg_roles') + ->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE'))) + ->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser))); + + try { + $result = $query->execute(); + $canCreateRoles = $result->rowCount() > 0; + } catch (DatabaseException $e) { + $canCreateRoles = false; + } + + 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 + $this->dbUser = 'oc_' . strtolower($username); + //create a new password so we don't need to store the admin config in the config file + $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([ @@ -120,7 +122,7 @@ class PostgreSQL extends AbstractDatabase { private function createDatabase(Connection $connection) { if (!$this->databaseExists($connection)) { //The database does not exists... let's create it - $query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER " . addslashes($this->dbUser)); + $query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER \"" . addslashes($this->dbUser) . '"'); try { $query->execute(); } catch (DatabaseException $e) { @@ -170,10 +172,10 @@ class PostgreSQL extends AbstractDatabase { } // create the user - $query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); + $query = $connection->prepare("CREATE USER \"" . addslashes($this->dbUser) . "\" CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); $query->execute(); if ($this->databaseExists($connection)) { - $query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO '.addslashes($this->dbUser)); + $query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO "' . addslashes($this->dbUser) . '"'); $query->execute(); } } catch (DatabaseException $e) { diff --git a/lib/private/Share/Constants.php b/lib/private/Share/Constants.php index 03c4c2ba828..0c8fad17e07 100644 --- a/lib/private/Share/Constants.php +++ b/lib/private/Share/Constants.php @@ -74,6 +74,8 @@ class Constants { public const SHARE_TYPE_DECK = 12; // const SHARE_TYPE_DECK_USER = 13; // Internal type used by DeckShareProvider + // Note to developers: Do not add new share types here + public const FORMAT_NONE = -1; public const FORMAT_STATUSES = -2; public const FORMAT_SOURCES = -3; // ToDo Check if it is still in use otherwise remove it diff --git a/lib/private/Share/Share.php b/lib/private/Share/Share.php index 487625affc1..dec71f792fd 100644 --- a/lib/private/Share/Share.php +++ b/lib/private/Share/Share.php @@ -63,7 +63,7 @@ class Share extends Constants { * Apps are required to handle permissions on their own, this class only * stores and manages the permissions of shares * - * @see lib/public/constants.php + * @see lib/public/Constants.php */ /** diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 7fd99545668..f84ed1671ba 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -244,6 +244,7 @@ class Manager implements IManager { } } elseif ($share->getShareType() === IShare::TYPE_ROOM) { } elseif ($share->getShareType() === IShare::TYPE_DECK) { + } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { } else { // We cannot handle other types yet throw new \InvalidArgumentException('unknown share type'); diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index 16f9a17ee42..6abfb372a4d 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -340,6 +340,8 @@ class ProviderFactory implements IProviderFactory { $provider = $this->getRoomShareProvider(); } elseif ($shareType === IShare::TYPE_DECK) { $provider = $this->getProvider('deck'); + } elseif ($shareType === IShare::TYPE_SCIENCEMESH) { + $provider = $this->getProvider('sciencemesh'); } diff --git a/lib/private/SystemTag/SystemTagManager.php b/lib/private/SystemTag/SystemTagManager.php index 4524aeaf7bc..79c5adcf450 100644 --- a/lib/private/SystemTag/SystemTagManager.php +++ b/lib/private/SystemTag/SystemTagManager.php @@ -193,10 +193,12 @@ class SystemTagManager implements ISystemTagManager { * {@inheritdoc} */ public function createTag(string $tagName, bool $userVisible, bool $userAssignable): ISystemTag { + // Length of name column is 64 + $truncatedTagName = substr($tagName, 0, 64); $query = $this->connection->getQueryBuilder(); $query->insert(self::TAG_TABLE) ->values([ - 'name' => $query->createNamedParameter($tagName), + 'name' => $query->createNamedParameter($truncatedTagName), 'visibility' => $query->createNamedParameter($userVisible ? 1 : 0), 'editable' => $query->createNamedParameter($userAssignable ? 1 : 0), ]); @@ -205,7 +207,7 @@ class SystemTagManager implements ISystemTagManager { $query->execute(); } catch (UniqueConstraintViolationException $e) { throw new TagAlreadyExistsException( - 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', + 'Tag ("' . $truncatedTagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', 0, $e ); @@ -215,7 +217,7 @@ class SystemTagManager implements ISystemTagManager { $tag = new SystemTag( (string)$tagId, - $tagName, + $truncatedTagName, $userVisible, $userAssignable ); diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php index 5a09a1754f2..b61a81a1fa7 100644 --- a/lib/private/SystemTag/SystemTagObjectMapper.php +++ b/lib/private/SystemTag/SystemTagObjectMapper.php @@ -77,23 +77,25 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ->from(self::RELATION_TABLE) ->where($query->expr()->in('objectid', $query->createParameter('objectids'))) ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) - ->setParameter('objectids', $objIds, IQueryBuilder::PARAM_STR_ARRAY) ->setParameter('objecttype', $objectType) ->addOrderBy('objectid', 'ASC') ->addOrderBy('systemtagid', 'ASC'); - + $chunks = array_chunk($objIds, 900, false); $mapping = []; foreach ($objIds as $objId) { $mapping[$objId] = []; } + foreach ($chunks as $chunk) { + $query->setParameter('objectids', $chunk, IQueryBuilder::PARAM_STR_ARRAY); + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + $objectId = $row['objectid']; + $mapping[$objectId][] = $row['systemtagid']; + } - $result = $query->execute(); - while ($row = $result->fetch()) { - $objectId = $row['objectid']; - $mapping[$objectId][] = $row['systemtagid']; + $result->closeCursor(); } - $result->closeCursor(); return $mapping; } diff --git a/lib/private/Template/JSResourceLocator.php b/lib/private/Template/JSResourceLocator.php index 7648c7953f3..120234146e1 100644 --- a/lib/private/Template/JSResourceLocator.php +++ b/lib/private/Template/JSResourceLocator.php @@ -27,16 +27,19 @@ */ namespace OC\Template; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use Psr\Log\LoggerInterface; class JSResourceLocator extends ResourceLocator { - /** @var JSCombiner */ - protected $jsCombiner; + protected JSCombiner $jsCombiner; + protected IAppManager $appManager; - public function __construct(LoggerInterface $logger, JSCombiner $JSCombiner) { + public function __construct(LoggerInterface $logger, JSCombiner $JSCombiner, IAppManager $appManager) { parent::__construct($logger); $this->jsCombiner = $JSCombiner; + $this->appManager = $appManager; } /** @@ -53,59 +56,63 @@ class JSResourceLocator extends ResourceLocator { // For language files we try to load them all, so themes can overwrite // single l10n strings without having to translate all of them. $found = 0; - $found += $this->appendIfExist($this->serverroot, 'core/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, 'apps/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.'apps/'.$script.'.js'); + $found += $this->appendScriptIfExist($this->serverroot, 'core/'.$script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.'core/'.$script); + $found += $this->appendScriptIfExist($this->serverroot, $script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.$script); + $found += $this->appendScriptIfExist($this->serverroot, 'apps/'.$script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir.'apps/'.$script); if ($found) { return; } - } elseif ($this->appendIfExist($this->serverroot, $theme_dir.'apps/'.$script.'.js') - || $this->appendIfExist($this->serverroot, $theme_dir.$script.'.js') - || $this->appendIfExist($this->serverroot, $script.'.js') - || $this->appendIfExist($this->serverroot, $theme_dir . "dist/$app-$scriptName.js") - || $this->appendIfExist($this->serverroot, "dist/$app-$scriptName.js") - || $this->appendIfExist($this->serverroot, 'apps/'.$script.'.js') + } elseif ($this->appendScriptIfExist($this->serverroot, $theme_dir.'apps/'.$script) + || $this->appendScriptIfExist($this->serverroot, $theme_dir.$script) + || $this->appendScriptIfExist($this->serverroot, $script) + || $this->appendScriptIfExist($this->serverroot, $theme_dir."dist/$app-$scriptName") + || $this->appendScriptIfExist($this->serverroot, "dist/$app-$scriptName") + || $this->appendScriptIfExist($this->serverroot, 'apps/'.$script) || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, $script.'.json') - || $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$script.'.js') - || $this->appendIfExist($this->serverroot, 'core/'.$script.'.js') - || (strpos($scriptName, '/') === -1 && ($this->appendIfExist($this->serverroot, $theme_dir . "dist/core-$scriptName.js") - || $this->appendIfExist($this->serverroot, "dist/core-$scriptName.js"))) + || $this->appendScriptIfExist($this->serverroot, $theme_dir.'core/'.$script) + || $this->appendScriptIfExist($this->serverroot, 'core/'.$script) + || (strpos($scriptName, '/') === -1 && ($this->appendScriptIfExist($this->serverroot, $theme_dir."dist/core-$scriptName") + || $this->appendScriptIfExist($this->serverroot, "dist/core-$scriptName"))) || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, 'core/'.$script.'.json') ) { return; } $script = substr($script, strpos($script, '/') + 1); - $app_path = \OC_App::getAppPath($app); - $app_url = \OC_App::getAppWebPath($app); + $app_url = null; + + try { + $app_url = $this->appManager->getAppWebPath($app); + } catch (AppPathNotFoundException) { + // pass + } + + try { + $app_path = $this->appManager->getAppPath($app); - if ($app_path !== false) { // Account for the possibility of having symlinks in app path. Only // do this if $app_path is set, because an empty argument to realpath // gets turned into cwd. $app_path = realpath($app_path); - } - // missing translations files fill be ignored - if (strpos($script, 'l10n/') === 0) { - $this->appendIfExist($app_path, $script . '.js', $app_url); - return; - } + // missing translations files will be ignored + if (strpos($script, 'l10n/') === 0) { + $this->appendScriptIfExist($app_path, $script, $app_url); + return; + } - if ($app_path === false && $app_url === false) { + if (!$this->cacheAndAppendCombineJsonIfExist($app_path, $script.'.json', $app)) { + $this->appendScriptIfExist($app_path, $script, $app_url); + } + } catch (AppPathNotFoundException) { $this->logger->error('Could not find resource {resource} to load', [ 'resource' => $app . '/' . $script . '.js', 'app' => 'jsresourceloader', ]); - return; - } - - if (!$this->cacheAndAppendCombineJsonIfExist($app_path, $script.'.json', $app)) { - $this->append($app_path, $script . '.js', $app_url); } } @@ -115,6 +122,17 @@ class JSResourceLocator extends ResourceLocator { public function doFindTheme($script) { } + /** + * Try to find ES6 script file (`.mjs`) with fallback to plain javascript (`.js`) + * @see appendIfExist() + */ + protected function appendScriptIfExist(string $root, string $file, string $webRoot = null) { + if (!$this->appendIfExist($root, $file . '.mjs', $webRoot)) { + return $this->appendIfExist($root, $file . '.js', $webRoot); + } + return true; + } + protected function cacheAndAppendCombineJsonIfExist($root, $file, $app = 'core') { if (is_file($root.'/'.$file)) { if ($this->jsCombiner->process($root, $file, $app)) { @@ -122,7 +140,12 @@ class JSResourceLocator extends ResourceLocator { } else { // Add all the files from the json $files = $this->jsCombiner->getContent($root, $file); - $app_url = \OC_App::getAppWebPath($app); + $app_url = null; + try { + $app_url = $this->appManager->getAppWebPath($app); + } catch (AppPathNotFoundException) { + // pass + } foreach ($files as $jsFile) { $this->append($root, $jsFile, $app_url); diff --git a/lib/private/Translation/TranslationManager.php b/lib/private/Translation/TranslationManager.php new file mode 100644 index 00000000000..ec829e83255 --- /dev/null +++ b/lib/private/Translation/TranslationManager.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +namespace OC\Translation; + +use InvalidArgumentException; +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\IServerContainer; +use OCP\PreConditionNotMetException; +use OCP\Translation\IDetectLanguageProvider; +use OCP\Translation\ITranslationManager; +use OCP\Translation\ITranslationProvider; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class TranslationManager implements ITranslationManager { + /** @var ?ITranslationProvider[] */ + private ?array $providers = null; + + public function __construct( + private IServerContainer $serverContainer, + private Coordinator $coordinator, + private LoggerInterface $logger, + ) { + } + + public function getLanguages(): array { + $languages = []; + foreach ($this->getProviders() as $provider) { + $languages = array_merge($languages, $provider->getAvailableLanguages()); + } + return $languages; + } + + public function translate(string $text, ?string $fromLanguage, string $toLanguage): string { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No translation providers available'); + } + + foreach ($this->getProviders() as $provider) { + if ($fromLanguage === null && $provider instanceof IDetectLanguageProvider) { + $fromLanguage = $provider->detectLanguage($text); + } + + if ($fromLanguage === null) { + throw new InvalidArgumentException('Could not detect language'); + } + + try { + return $provider->translate($fromLanguage, $toLanguage, $text); + } catch (RuntimeException $e) { + $this->logger->warning("Failed to translate from {$fromLanguage} to {$toLanguage}", ['exception' => $e]); + } + } + + throw new RuntimeException('Could not translate text'); + } + + public function getProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + + if ($this->providers !== null) { + return $this->providers; + } + + $this->providers = []; + foreach ($context->getTranslationProviders() as $providerRegistration) { + $class = $providerRegistration->getService(); + try { + $this->providers[$class] = $this->serverContainer->get($class); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable $e) { + $this->logger->error('Failed to load translation provider ' . $class, [ + 'exception' => $e + ]); + } + } + + return $this->providers; + } + + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + return !empty($context->getTranslationProviders()); + } + + public function canDetectLanguage(): bool { + foreach ($this->getProviders() as $provider) { + if ($provider instanceof IDetectLanguageProvider) { + return true; + } + } + return false; + } +} diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php index 8bbbccd4540..944202f244e 100644 --- a/lib/private/User/Database.php +++ b/lib/private/User/Database.php @@ -45,6 +45,7 @@ declare(strict_types=1); */ namespace OC\User; +use OCP\AppFramework\Db\TTransactional; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; @@ -85,6 +86,8 @@ class Database extends ABackend implements /** @var string */ private $table; + use TTransactional; + /** * \OC\User\Database constructor. * @@ -122,20 +125,24 @@ class Database extends ABackend implements if (!$this->userExists($uid)) { $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password)); - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert($this->table) - ->values([ - 'uid' => $qb->createNamedParameter($uid), - 'password' => $qb->createNamedParameter(\OC::$server->getHasher()->hash($password)), - 'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)), - ]); + return $this->atomic(function () use ($uid, $password) { + $qb = $this->dbConn->getQueryBuilder(); + $qb->insert($this->table) + ->values([ + 'uid' => $qb->createNamedParameter($uid), + 'password' => $qb->createNamedParameter(\OC::$server->getHasher()->hash($password)), + 'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)), + ]); - $result = $qb->execute(); + $result = $qb->executeStatement(); - // Clear cache - unset($this->cache[$uid]); + // Clear cache + unset($this->cache[$uid]); + // Repopulate the cache + $this->loadUser($uid); - return $result ? true : false; + return (bool) $result; + }, $this->dbConn); } return false; diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index c7b11e22504..3e45ebeab2b 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -59,6 +59,7 @@ use OCP\ISession; use OCP\IUser; use OCP\IUserSession; use OCP\Lockdown\ILockdownManager; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ISecureRandom; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\User\Events\PostLoginEvent; @@ -426,7 +427,8 @@ class Session implements IUserSession, Emitter { $password, IRequest $request, OC\Security\Bruteforce\Throttler $throttler) { - $currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login'); + $remoteAddress = $request->getRemoteAddress(); + $currentDelay = $throttler->sleepDelay($remoteAddress, 'login'); if ($this->manager instanceof PublicEmitter) { $this->manager->emit('\OC\User', 'preLogin', [$user, $password]); @@ -450,19 +452,12 @@ class Session implements IUserSession, Emitter { if (!$this->login($user, $password)) { // Failed, maybe the user used their email address if (!filter_var($user, FILTER_VALIDATE_EMAIL)) { + $this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password); return false; } $users = $this->manager->getByEmail($user); if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) { - $this->logger->warning('Login failed: \'' . $user . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']); - - $throttler->registerAttempt('login', $request->getRemoteAddress(), ['user' => $user]); - - $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password)); - - if ($currentDelay === 0) { - $throttler->sleepDelay($request->getRemoteAddress(), 'login'); - } + $this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password); return false; } } @@ -477,6 +472,17 @@ class Session implements IUserSession, Emitter { return true; } + private function handleLoginFailed(IThrottler $throttler, int $currentDelay, string $remoteAddress, string $user, ?string $password) { + $this->logger->warning("Login failed: '" . $user . "' (Remote IP: '" . $remoteAddress . "')", ['app' => 'core']); + + $throttler->registerAttempt('login', $remoteAddress, ['user' => $user]); + $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password)); + + if ($currentDelay === 0) { + $throttler->sleepDelay($remoteAddress, 'login'); + } + } + protected function supportsCookies(IRequest $request) { if (!is_null($request->getCookie('cookie_test'))) { return true; diff --git a/lib/private/User/User.php b/lib/private/User/User.php index 2b975c290ba..2d80dbc7adf 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -529,6 +529,7 @@ class User implements IUser { $this->config->setUserValue($this->uid, 'files', 'quota', $quota); $this->triggerChange('quota', $quota, $oldQuota); } + \OC_Helper::clearStorageInfo('/' . $this->uid . '/files'); } /** diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 7f51d81d21b..5051d3e7ab5 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -50,11 +50,14 @@ declare(strict_types=1); * along with this program. If not, see <http://www.gnu.org/licenses/> * */ + +use OCP\App\Events\AppUpdateEvent; use OCP\AppFramework\QueryException; +use OCP\App\IAppManager; use OCP\App\ManagerEvent; use OCP\Authentication\IAlternativeLogin; +use OCP\EventDispatcher\IEventDispatcher; use OCP\ILogger; -use OCP\Settings\IManager as ISettingsManager; use OC\AppFramework\Bootstrap\Coordinator; use OC\App\DependencyAnalyzer; use OC\App\Platform; @@ -62,7 +65,6 @@ use OC\DB\MigrationService; use OC\Installer; use OC\Repair; use OC\Repair\Events\RepairErrorEvent; -use OC\ServerNotAvailableException; use Psr\Log\LoggerInterface; /** @@ -73,8 +75,6 @@ use Psr\Log\LoggerInterface; class OC_App { private static $adminForms = []; private static $personalForms = []; - private static $appTypes = []; - private static $loadedApps = []; private static $altLogin = []; private static $alreadyRegistered = []; public const supportedApp = 300; @@ -98,9 +98,10 @@ class OC_App { * * @param string $app * @return bool + * @deprecated 26.0.0 use IAppManager::isAppLoaded */ public static function isAppLoaded(string $app): bool { - return isset(self::$loadedApps[$app]); + return \OC::$server->get(IAppManager::class)->isAppLoaded($app); } /** @@ -116,40 +117,11 @@ class OC_App { * if $types is set to non-empty array, only apps of those types will be loaded */ public static function loadApps(array $types = []): bool { - if ((bool) \OC::$server->getSystemConfig()->getValue('maintenance', false)) { + if (!\OC::$server->getSystemConfig()->getValue('installed', false)) { + // This should be done before calling this method so that appmanager can be used return false; } - // Load the enabled apps here - $apps = self::getEnabledApps(); - - // Add each apps' folder as allowed class path - foreach ($apps as $app) { - // If the app is already loaded then autoloading it makes no sense - if (!isset(self::$loadedApps[$app])) { - $path = self::getAppPath($app); - if ($path !== false) { - self::registerAutoloading($app, $path); - } - } - } - - // prevent app.php from printing output - ob_start(); - foreach ($apps as $app) { - if (!isset(self::$loadedApps[$app]) && ($types === [] || self::isType($app, $types))) { - try { - self::loadApp($app); - } catch (\Throwable $e) { - \OC::$server->get(LoggerInterface::class)->emergency('Error during app loading: ' . $e->getMessage(), [ - 'exception' => $e, - 'app' => $app, - ]); - } - } - } - ob_end_clean(); - - return true; + return \OC::$server->get(IAppManager::class)->loadApps($types); } /** @@ -157,120 +129,10 @@ class OC_App { * * @param string $app * @throws Exception + * @deprecated 26.0.0 use IAppManager::loadApp */ public static function loadApp(string $app): void { - if (isset(self::$loadedApps[$app])) { - return; - } - self::$loadedApps[$app] = true; - $appPath = self::getAppPath($app); - if ($appPath === false) { - return; - } - $eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class); - $eventLogger->start("bootstrap:load_app:$app", "Load $app"); - - // in case someone calls loadApp() directly - self::registerAutoloading($app, $appPath); - - /** @var Coordinator $coordinator */ - $coordinator = \OC::$server->query(Coordinator::class); - $isBootable = $coordinator->isBootable($app); - - $hasAppPhpFile = is_file($appPath . '/appinfo/app.php'); - - if ($isBootable && $hasAppPhpFile) { - \OC::$server->getLogger()->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [ - 'app' => $app, - ]); - } elseif ($hasAppPhpFile) { - $eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app"); - \OC::$server->getLogger()->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [ - 'app' => $app, - ]); - try { - self::requireAppFile($appPath); - } catch (Throwable $ex) { - if ($ex instanceof ServerNotAvailableException) { - throw $ex; - } - if (!\OC::$server->getAppManager()->isShipped($app) && !self::isType($app, ['authentication'])) { - \OC::$server->getLogger()->logException($ex, [ - 'message' => "App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), - ]); - - // Only disable apps which are not shipped and that are not authentication apps - \OC::$server->getAppManager()->disableApp($app, true); - } else { - \OC::$server->getLogger()->logException($ex, [ - 'message' => "App $app threw an error during app.php load: " . $ex->getMessage(), - ]); - } - } - $eventLogger->end("bootstrap:load_app:$app:app.php"); - } - - $coordinator->bootApp($app); - - $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it"); - $info = self::getAppInfo($app); - if (!empty($info['activity']['filters'])) { - foreach ($info['activity']['filters'] as $filter) { - \OC::$server->getActivityManager()->registerFilter($filter); - } - } - if (!empty($info['activity']['settings'])) { - foreach ($info['activity']['settings'] as $setting) { - \OC::$server->getActivityManager()->registerSetting($setting); - } - } - if (!empty($info['activity']['providers'])) { - foreach ($info['activity']['providers'] as $provider) { - \OC::$server->getActivityManager()->registerProvider($provider); - } - } - - if (!empty($info['settings']['admin'])) { - foreach ($info['settings']['admin'] as $setting) { - \OC::$server->get(ISettingsManager::class)->registerSetting('admin', $setting); - } - } - if (!empty($info['settings']['admin-section'])) { - foreach ($info['settings']['admin-section'] as $section) { - \OC::$server->get(ISettingsManager::class)->registerSection('admin', $section); - } - } - if (!empty($info['settings']['personal'])) { - foreach ($info['settings']['personal'] as $setting) { - \OC::$server->get(ISettingsManager::class)->registerSetting('personal', $setting); - } - } - if (!empty($info['settings']['personal-section'])) { - foreach ($info['settings']['personal-section'] as $section) { - \OC::$server->get(ISettingsManager::class)->registerSection('personal', $section); - } - } - - if (!empty($info['collaboration']['plugins'])) { - // deal with one or many plugin entries - $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ? - [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin']; - foreach ($plugins as $plugin) { - if ($plugin['@attributes']['type'] === 'collaborator-search') { - $pluginInfo = [ - 'shareType' => $plugin['@attributes']['share-type'], - 'class' => $plugin['@value'], - ]; - \OC::$server->getCollaboratorSearch()->registerPlugin($pluginInfo); - } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') { - \OC::$server->getAutoCompleteManager()->registerSorter($plugin['@value']); - } - } - } - - $eventLogger->end("bootstrap:load_app:$app:info"); - - $eventLogger->end("bootstrap:load_app:$app"); + \OC::$server->get(IAppManager::class)->loadApp($app); } /** @@ -295,8 +157,6 @@ class OC_App { require_once $path . '/composer/autoload.php'; } else { \OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true); - // Register on legacy autoloader - \OC::$loader->addValidRoot($path); } // Register Test namespace only when testing @@ -306,50 +166,15 @@ class OC_App { } /** - * Load app.php from the given app - * - * @param string $app app name - * @throws Error - */ - private static function requireAppFile(string $app) { - // encapsulated here to avoid variable scope conflicts - require_once $app . '/appinfo/app.php'; - } - - /** * check if an app is of a specific type * * @param string $app * @param array $types * @return bool + * @deprecated 26.0.0 use IAppManager::isType */ public static function isType(string $app, array $types): bool { - $appTypes = self::getAppTypes($app); - foreach ($types as $type) { - if (array_search($type, $appTypes) !== false) { - return true; - } - } - return false; - } - - /** - * get the types of an app - * - * @param string $app - * @return array - */ - private static function getAppTypes(string $app): array { - //load the cache - if (count(self::$appTypes) == 0) { - self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types'); - } - - if (isset(self::$appTypes[$app])) { - return explode(',', self::$appTypes[$app]); - } - - return []; + return \OC::$server->get(IAppManager::class)->isType($app, $types); } /** @@ -1042,6 +867,7 @@ class OC_App { $version = \OC_App::getAppVersion($appId); \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version); + \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId)); \OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( ManagerEvent::EVENT_APP_UPDATE, $appId )); @@ -1061,7 +887,7 @@ class OC_App { // load the app self::loadApp($appId); - $dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class); + $dispatcher = \OC::$server->get(IEventDispatcher::class); // load the steps $r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class)); diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index c2036c7b863..8d708118b96 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -473,7 +473,7 @@ class OC_Helper { if (!$view) { throw new \OCP\Files\NotFoundException(); } - $fullPath = $view->getAbsolutePath($path); + $fullPath = Filesystem::normalizePath($view->getAbsolutePath($path)); $cacheKey = $fullPath. '::' . ($includeMountPoints ? 'include' : 'exclude'); if ($useCache) { @@ -620,6 +620,15 @@ class OC_Helper { ]; } + public static function clearStorageInfo(string $absolutePath): void { + /** @var ICacheFactory $cacheFactory */ + $cacheFactory = \OC::$server->get(ICacheFactory::class); + $memcache = $cacheFactory->createLocal('storage_info'); + $cacheKeyPrefix = Filesystem::normalizePath($absolutePath) . '::'; + $memcache->remove($cacheKeyPrefix . 'include'); + $memcache->remove($cacheKeyPrefix . 'exclude'); + } + /** * Returns whether the config file is set manually to read-only * @return bool diff --git a/lib/private/legacy/OC_Image.php b/lib/private/legacy/OC_Image.php index f2fa2058faa..74e82e62b16 100644 --- a/lib/private/legacy/OC_Image.php +++ b/lib/private/legacy/OC_Image.php @@ -598,7 +598,7 @@ class OC_Image implements \OCP\IImage { * @return bool true if allocating is allowed, false otherwise */ private function checkImageSize($path) { - $size = getimagesize($path); + $size = @getimagesize($path); if (!$size) { return true; } @@ -619,7 +619,7 @@ class OC_Image implements \OCP\IImage { * @return bool true if allocating is allowed, false otherwise */ private function checkImageDataSize($data) { - $size = getimagesizefromstring($data); + $size = @getimagesizefromstring($data); if (!$size) { return true; } diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index 8aaa9072ba4..caa4f5dca65 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -40,6 +40,7 @@ use OC\User\LoginException; use OCP\EventDispatcher\IEventDispatcher; use OCP\ILogger; use OCP\IUserManager; +use OCP\User\Events\BeforeUserLoggedInEvent; use OCP\User\Events\UserLoggedInEvent; /** @@ -172,6 +173,10 @@ class OC_User { if (self::getUser() !== $uid) { self::setUserId($uid); $userSession = \OC::$server->getUserSession(); + + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->get(IEventDispatcher::class); + if ($userSession->getUser() && !$userSession->getUser()->isEnabled()) { $message = \OC::$server->getL10N('lib')->t('User disabled'); throw new LoginException($message); @@ -182,6 +187,10 @@ class OC_User { if ($backend instanceof \OCP\Authentication\IProvideUserSecretBackend) { $password = $backend->getCurrentUserSecret(); } + + /** @var IEventDispatcher $dispatcher */ + $dispatcher->dispatchTyped(new BeforeUserLoggedInEvent($uid, $password, $backend)); + $userSession->createSessionToken($request, $uid, $uid, $password); $userSession->createRememberMeToken($userSession->getUser()); // setup the filesystem @@ -199,8 +208,6 @@ class OC_User { 'isTokenLogin' => false, ] ); - /** @var IEventDispatcher $dispatcher */ - $dispatcher = \OC::$server->get(IEventDispatcher::class); $dispatcher->dispatchTyped(new UserLoggedInEvent( \OC::$server->get(IUserManager::class)->get($uid), $uid, diff --git a/lib/private/legacy/template/functions.php b/lib/private/legacy/template/functions.php index 56c488d5abe..7081bd4f743 100644 --- a/lib/private/legacy/template/functions.php +++ b/lib/private/legacy/template/functions.php @@ -72,14 +72,19 @@ function emit_css_loading_tags($obj) { * Prints a <script> tag with nonce and defer depending on config * @param string $src the source URL, ignored when empty * @param string $script_content the inline script content, ignored when empty + * @param string $content_type the type of the source (e.g. 'module') */ -function emit_script_tag($src, $script_content = '') { +function emit_script_tag(string $src, string $script_content = '', string $content_type = '') { + $nonceManager = \OC::$server->get(\OC\Security\CSP\ContentSecurityPolicyNonceManager::class); + $defer_str = ' defer'; - $s = '<script nonce="' . \OC::$server->getContentSecurityPolicyNonceManager()->getNonce() . '"'; + $type = $content_type !== '' ? ' type="' . $content_type . '"' : ''; + + $s = '<script nonce="' . $nonceManager->getNonce() . '"'; if (!empty($src)) { // emit script tag for deferred loading from $src - $s .= $defer_str.' src="' . $src .'">'; - } elseif (!empty($script_content)) { + $s .= $defer_str.' src="' . $src .'"' . $type . '>'; + } elseif ($script_content !== '') { // emit script tag for inline script from $script_content without defer (see MDN) $s .= ">\n".$script_content."\n"; } else { @@ -96,7 +101,8 @@ function emit_script_tag($src, $script_content = '') { */ function emit_script_loading_tags($obj) { foreach ($obj['jsfiles'] as $jsfile) { - emit_script_tag($jsfile, ''); + $type = str_ends_with($jsfile, '.mjs') ? 'module' : ''; + emit_script_tag($jsfile, '', $type); } if (!empty($obj['inline_ocjs'])) { emit_script_tag('', $obj['inline_ocjs']); |