diff options
Diffstat (limited to 'lib/private')
42 files changed, 1084 insertions, 181 deletions
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 9fc5accfa08..a3f971df6a1 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -32,6 +32,7 @@ */ namespace OC\Accounts; +use Exception; use InvalidArgumentException; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumber; @@ -45,9 +46,17 @@ use OCP\Accounts\IAccountPropertyCollection; use OCP\Accounts\PropertyDoesNotExistException; use OCP\BackgroundJob\IJobList; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Defaults; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Security\ICrypto; +use OCP\Security\VerificationToken\IVerificationToken; +use OCP\Util; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -88,17 +97,46 @@ class AccountManager implements IAccountManager { /** @var LoggerInterface */ private $logger; - - public function __construct(IDBConnection $connection, - IConfig $config, - EventDispatcherInterface $eventDispatcher, - IJobList $jobList, - LoggerInterface $logger) { + /** @var IVerificationToken */ + private $verificationToken; + /** @var IMailer */ + private $mailer; + /** @var Defaults */ + private $defaults; + /** @var IL10N */ + private $l10n; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var ICrypto */ + private $crypto; + /** @var IFactory */ + private $l10nfactory; + + public function __construct( + IDBConnection $connection, + IConfig $config, + EventDispatcherInterface $eventDispatcher, + IJobList $jobList, + LoggerInterface $logger, + IVerificationToken $verificationToken, + IMailer $mailer, + Defaults $defaults, + IFactory $factory, + IURLGenerator $urlGenerator, + ICrypto $crypto + ) { $this->connection = $connection; $this->config = $config; $this->eventDispatcher = $eventDispatcher; $this->jobList = $jobList; $this->logger = $logger; + $this->verificationToken = $verificationToken; + $this->mailer = $mailer; + $this->defaults = $defaults; + $this->urlGenerator = $urlGenerator; + $this->crypto = $crypto; + // DIing IL10N results in a dependency loop + $this->l10nfactory = $factory; } /** @@ -337,7 +375,6 @@ class AccountManager implements IAccountManager { /** * check if we need to ask the server for email verification, if yes we create a cronjob - * */ protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void { try { @@ -358,11 +395,73 @@ class AccountManager implements IAccountManager { ] ); + $property->setVerified(self::VERIFICATION_IN_PROGRESS); + } + } + + protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void { + $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL); + foreach ($mailCollection->getProperties() as $property) { + if ($property->getLocallyVerified() !== self::NOT_VERIFIED) { + continue; + } + if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) { + $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS); + } + } + } + + protected function sendEmailVerificationEmail(IUser $user, string $email): bool { + $ref = \substr(hash('sha256', $email), 0, 8); + $key = $this->crypto->encrypt($email); + $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email); + $link = $this->urlGenerator->linkToRouteAbsolute('provisioning_api.Verification.verifyMail', + [ + 'userId' => $user->getUID(), + 'token' => $token, + 'key' => $key + ]); + $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [ + 'link' => $link, + ]); - $property->setVerified(self::VERIFICATION_IN_PROGRESS); + if (!$this->l10n) { + $this->l10n = $this->l10nfactory->get('core'); } + + $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($this->l10n->t('Email verification')); + + $emailTemplate->addBodyText( + htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')), + $this->l10n->t('Click the following link to confirm your email.') + ); + + $emailTemplate->addBodyButton( + htmlspecialchars($this->l10n->t('Confirm your email')), + $link, + false + ); + $emailTemplate->addFooter(); + + try { + $message = $this->mailer->createMessage(); + $message->setTo([$email => $user->getDisplayName()]); + $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]); + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + } catch (Exception $e) { + // Log the exception and continue + $this->logger->info('Failed to send verification mail', [ + 'app' => 'core', + 'exception' => $e + ]); + return false; + } + return true; } /** @@ -406,7 +505,6 @@ class AccountManager implements IAccountManager { } } - /** * add new user to accounts table * @@ -435,6 +533,12 @@ class AccountManager implements IAccountManager { foreach ($data as $dataRow) { $propertyName = $dataRow['name']; unset($dataRow['name']); + + if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) { + // do not write default value, save DB space + unset($dataRow['locallyVerified']); + } + if (!$this->isCollection($propertyName)) { $preparedData[$propertyName] = $dataRow; continue; @@ -511,7 +615,6 @@ class AccountManager implements IAccountManager { continue; } - $query->setParameter('name', $property['name']) ->setParameter('value', $property['value'] ?? ''); $query->executeStatement(); @@ -587,6 +690,7 @@ class AccountManager implements IAccountManager { $data['verified'] ?? self::NOT_VERIFIED, '' ); + $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED); $collection->addProperty($p); return $collection; @@ -599,6 +703,10 @@ class AccountManager implements IAccountManager { $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData)); } else { $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED); + if (isset($accountData['locallyVerified'])) { + $property = $account->getProperty($accountData['name']); + $property->setLocallyVerified($accountData['locallyVerified']); + } } } return $account; @@ -640,14 +748,17 @@ class AccountManager implements IAccountManager { $oldData = $this->getUser($account->getUser(), false); $this->updateVerificationStatus($account, $oldData); $this->checkEmailVerification($account, $oldData); + $this->checkLocalEmailVerification($account, $oldData); $data = []; foreach ($account->getAllProperties() as $property) { + /** @var IAccountProperty $property */ $data[] = [ 'name' => $property->getName(), 'value' => $property->getValue(), 'scope' => $property->getScope(), 'verified' => $property->getVerified(), + 'locallyVerified' => $property->getLocallyVerified(), ]; } diff --git a/lib/private/Accounts/AccountProperty.php b/lib/private/Accounts/AccountProperty.php index 1a21baf9698..0e6356e9e92 100644 --- a/lib/private/Accounts/AccountProperty.php +++ b/lib/private/Accounts/AccountProperty.php @@ -27,6 +27,7 @@ declare(strict_types=1); */ namespace OC\Accounts; +use InvalidArgumentException; use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; @@ -42,6 +43,8 @@ class AccountProperty implements IAccountProperty { private $verified; /** @var string */ private $verificationData; + /** @var string */ + private $locallyVerified = IAccountManager::NOT_VERIFIED; public function __construct(string $name, string $value, string $scope, string $verified, string $verificationData) { $this->name = $name; @@ -90,7 +93,7 @@ class AccountProperty implements IAccountProperty { IAccountManager::SCOPE_PRIVATE, IAccountManager::SCOPE_PUBLISHED ])) { - throw new \InvalidArgumentException('Invalid scope'); + throw new InvalidArgumentException('Invalid scope'); } $this->scope = $newScope; return $this; @@ -178,4 +181,20 @@ class AccountProperty implements IAccountProperty { public function getVerificationData(): string { return $this->verificationData; } + + public function setLocallyVerified(string $verified): IAccountProperty { + if (!in_array($verified, [ + IAccountManager::NOT_VERIFIED, + IAccountManager::VERIFICATION_IN_PROGRESS, + IAccountManager::VERIFIED, + ])) { + throw new InvalidArgumentException('Provided verification value is invalid'); + } + $this->locallyVerified = $verified; + return $this; + } + + public function getLocallyVerified(): string { + return $this->locallyVerified; + } } diff --git a/lib/private/Accounts/AccountPropertyCollection.php b/lib/private/Accounts/AccountPropertyCollection.php index eb92536a6a0..3aed76d8746 100644 --- a/lib/private/Accounts/AccountPropertyCollection.php +++ b/lib/private/Accounts/AccountPropertyCollection.php @@ -84,6 +84,15 @@ class AccountPropertyCollection implements IAccountPropertyCollection { return $this; } + public function getPropertyByValue(string $value): ?IAccountProperty { + foreach ($this->properties as $i => $property) { + if ($property->getValue() === $value) { + return $property; + } + } + return null; + } + public function removePropertyByValue(string $value): IAccountPropertyCollection { foreach ($this->properties as $i => $property) { if ($property->getValue() === $value) { diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php index b9ca2d22c1c..9d57ef95688 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -253,7 +253,7 @@ class InfoParser { if (!count($node->children())) { $value = (string)$node; if (!empty($value)) { - $data['@value'] = (string)$node; + $data['@value'] = $value; } } else { $data = array_merge($data, $this->xmlToArray($node)); diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 89d59a471a8..293b9e47b25 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -48,6 +48,7 @@ use OC\AppFramework\Utility\SimpleContainer; use OC\Core\Middleware\TwoFactorMiddleware; use OC\Log\PsrLoggerAdapter; use OC\ServerContainer; +use OC\Settings\AuthorizedGroupMapper; use OCA\WorkflowEngine\Manager; use OCP\AppFramework\Http\IOutput; use OCP\AppFramework\IAppContainer; @@ -246,7 +247,9 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->getUserId() !== null && $server->getGroupManager()->isAdmin($this->getUserId()), $server->getUserSession()->getUser() !== null && $server->query(ISubAdmin::class)->isSubAdmin($server->getUserSession()->getUser()), $server->getAppManager(), - $server->getL10N('lib') + $server->getL10N('lib'), + $c->get(AuthorizedGroupMapper::class), + $server->get(IUserSession::class) ); $dispatcher->registerMiddleware($securityMiddleware); $dispatcher->registerMiddleware( diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index 6ef7bba4fd6..6bbab42cb79 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -155,7 +155,7 @@ class Dispatcher { $response = $this->middlewareDispatcher->afterException( $controller, $methodName, $exception); } catch (\Throwable $throwable) { - $exception = new \Exception($throwable->getMessage(), $throwable->getCode(), $throwable); + $exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable); $response = $this->middlewareDispatcher->afterException( $controller, $methodName, $exception); } diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index bd751183604..d162bb54108 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -34,6 +34,7 @@ declare(strict_types=1); * along with this program. If not, see <http://www.gnu.org/licenses/> * */ + namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException; @@ -43,6 +44,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Settings\AuthorizedGroupMapper; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; @@ -56,6 +58,7 @@ use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUserSession; use OCP\Util; use Psr\Log\LoggerInterface; @@ -88,6 +91,10 @@ class SecurityMiddleware extends Middleware { private $appManager; /** @var IL10N */ private $l10n; + /** @var AuthorizedGroupMapper */ + private $groupAuthorizationMapper; + /** @var IUserSession */ + private $userSession; public function __construct(IRequest $request, ControllerMethodReflector $reflector, @@ -99,7 +106,9 @@ class SecurityMiddleware extends Middleware { bool $isAdminUser, bool $isSubAdmin, IAppManager $appManager, - IL10N $l10n + IL10N $l10n, + AuthorizedGroupMapper $mapper, + IUserSession $userSession ) { $this->navigationManager = $navigationManager; $this->request = $request; @@ -112,12 +121,15 @@ class SecurityMiddleware extends Middleware { $this->isSubAdmin = $isSubAdmin; $this->appManager = $appManager; $this->l10n = $l10n; + $this->groupAuthorizationMapper = $mapper; + $this->userSession = $userSession; } /** * This runs all the security checks before a method call. The * security checks are determined by inspecting the controller method * annotations + * * @param Controller $controller the controller * @param string $methodName the name of the method * @throws SecurityException when a security check fails @@ -140,15 +152,39 @@ class SecurityMiddleware extends Middleware { if (!$this->isLoggedIn) { throw new NotLoggedInException(); } + $authorized = false; + if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) { + $authorized = $this->isAdminUser; + + if (!$authorized && $this->reflector->hasAnnotation('SubAdminRequired')) { + $authorized = $this->isSubAdmin; + } + + if (!$authorized) { + $settingClasses = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings')); + $authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser()); + foreach ($settingClasses as $settingClass) { + $authorized = in_array($settingClass, $authorizedClasses, true); + if ($authorized) { + break; + } + } + } + if (!$authorized) { + throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting')); + } + } if ($this->reflector->hasAnnotation('SubAdminRequired') && !$this->isSubAdmin - && !$this->isAdminUser) { + && !$this->isAdminUser + && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin')); } if (!$this->reflector->hasAnnotation('SubAdminRequired') && !$this->reflector->hasAnnotation('NoAdminRequired') - && !$this->isAdminUser) { + && !$this->isAdminUser + && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in user must be an admin')); } } @@ -200,19 +236,20 @@ class SecurityMiddleware extends Middleware { /** * If an SecurityException is being caught, ajax requests return a JSON error * response and non ajax requests redirect to the index + * * @param Controller $controller the controller that is being called * @param string $methodName the name of the method that will be called on * the controller * @param \Exception $exception the thrown exception - * @throws \Exception the passed in exception if it can't handle it * @return Response a Response object or null in case that the exception could not be handled + * @throws \Exception the passed in exception if it can't handle it */ public function afterException($controller, $methodName, \Exception $exception): Response { if ($exception instanceof SecurityException) { if ($exception instanceof StrictCookieMissingException) { return new RedirectResponse(\OC::$WEBROOT . '/'); } - if (stripos($this->request->getHeader('Accept'),'html') === false) { + if (stripos($this->request->getHeader('Accept'), 'html') === false) { $response = new JSONResponse( ['message' => $exception->getMessage()], $exception->getCode() diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index e3e4b37f383..9ed94082f0d 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -157,7 +157,7 @@ class UserPlugin implements ISearchPlugin { $userStatuses = $this->userStatusManager->getUserStatuses(array_keys($users)); foreach ($users as $uid => $user) { $userDisplayName = $user->getDisplayName(); - $userEmail = $user->getEMailAddress(); + $userEmail = $user->getSystemEMailAddress(); $uid = (string) $uid; $status = []; @@ -244,7 +244,7 @@ class UserPlugin implements ISearchPlugin { if ($addUser) { $status = []; $uid = $user->getUID(); - $userEmail = $user->getEMailAddress(); + $userEmail = $user->getSystemEMailAddress(); if (array_key_exists($user->getUID(), $userStatuses)) { $userStatus = $userStatuses[$user->getUID()]; $status = [ diff --git a/lib/private/Contacts/ContactsMenu/Entry.php b/lib/private/Contacts/ContactsMenu/Entry.php index 2c0b7de6ef6..aea71df2968 100644 --- a/lib/private/Contacts/ContactsMenu/Entry.php +++ b/lib/private/Contacts/ContactsMenu/Entry.php @@ -165,6 +165,7 @@ class Entry implements IEntry { 'topAction' => $topAction, 'actions' => $otherActions, 'lastMessage' => '', + 'emailAddresses' => $this->getEMailAddresses(), ]; } } diff --git a/lib/private/DB/AdapterMySQL.php b/lib/private/DB/AdapterMySQL.php index 43da88b4b74..b4be5c2e96a 100644 --- a/lib/private/DB/AdapterMySQL.php +++ b/lib/private/DB/AdapterMySQL.php @@ -25,7 +25,7 @@ namespace OC\DB; class AdapterMySQL extends Adapter { /** @var string */ - protected $charset; + protected $collation; /** * @param string $tableName @@ -39,16 +39,16 @@ class AdapterMySQL extends Adapter { } public function fixupStatement($statement) { - $statement = str_replace(' ILIKE ', ' COLLATE ' . $this->getCharset() . '_general_ci LIKE ', $statement); + $statement = str_replace(' ILIKE ', ' COLLATE ' . $this->getCollation() . ' LIKE ', $statement); return $statement; } - protected function getCharset() { - if (!$this->charset) { + protected function getCollation(): string { + if (!$this->collation) { $params = $this->conn->getParams(); - $this->charset = isset($params['charset']) ? $params['charset'] : 'utf8'; + $this->collation = $params['collation'] ?? (($params['charset'] ?? 'utf8') . '_general_ci'); } - return $this->charset; + return $this->collation; } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index 53e488b5f09..b4c7597f6d4 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -89,6 +89,10 @@ class ConnectionFactory { if ($this->config->getValue('mysql.utf8mb4', false)) { $this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4'; } + $collationOverride = $this->config->getValue('mysql.collation', null); + if ($collationOverride) { + $this->defaultConnectionParams['mysql']['collation'] = $collationOverride; + } } /** diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php index 3a0f45bcde7..e917ad3ad3a 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php @@ -31,7 +31,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; class MySqlExpressionBuilder extends ExpressionBuilder { /** @var string */ - protected $charset; + protected $collation; /** * @param ConnectionAdapter $connection @@ -41,7 +41,7 @@ class MySqlExpressionBuilder extends ExpressionBuilder { parent::__construct($connection, $queryBuilder); $params = $connection->getInner()->getParams(); - $this->charset = isset($params['charset']) ? $params['charset'] : 'utf8'; + $this->collation = $params['collation'] ?? (($params['charset'] ?? 'utf8') . '_general_ci'); } /** @@ -50,6 +50,6 @@ class MySqlExpressionBuilder extends ExpressionBuilder { public function iLike($x, $y, $type = null): string { $x = $this->helper->quoteColumnName($x); $y = $this->helper->quoteColumnName($y); - return $this->expressionBuilder->comparison($x, ' COLLATE ' . $this->charset . '_general_ci LIKE', $y); + return $this->expressionBuilder->comparison($x, ' COLLATE ' . $this->collation . ' LIKE', $y); } } diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index c6d34e67cc9..af797c7720d 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -76,6 +76,8 @@ class SeekableHttpStream implements File { private $current; /** @var int */ private $offset = 0; + /** @var int */ + private $length = 0; private function reconnect(int $start) { $range = $start . '-'; @@ -101,12 +103,14 @@ class SeekableHttpStream implements File { $content = trim(explode(':', $contentRange)[1]); $range = trim(explode(' ', $content)[1]); $begin = intval(explode('-', $range)[0]); + $length = intval(explode('/', $range)[1]); if ($begin !== $start) { return false; } $this->offset = $begin; + $this->length = $length; return true; } @@ -140,7 +144,12 @@ class SeekableHttpStream implements File { } return $this->reconnect($this->offset + $offset); case SEEK_END: - return false; + if ($this->length === 0) { + return false; + } elseif ($this->length + $offset === $this->offset) { + return true; + } + return $this->reconnect($this->length + $offset); } return false; } diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 37599c21d8f..caa26d81cc4 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -619,18 +619,18 @@ class Factory implements IFactory { $potentialName = $l->t('__language_name__'); return [ - 'commonlanguages' => [[ + 'commonLanguages' => [[ 'code' => $forceLanguage, 'name' => $potentialName, ]], - 'languages' => [], + 'otherLanguages' => [], ]; } $languageCodes = $this->findAvailableLanguages(); $commonLanguages = []; - $languages = []; + $otherLanguages = []; foreach ($languageCodes as $lang) { $l = $this->get('lib', $lang); @@ -658,14 +658,14 @@ class Factory implements IFactory { if (in_array($lang, self::COMMON_LANGUAGE_CODES)) { $commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln; } else { - $languages[] = $ln; + $otherLanguages[] = $ln; } } ksort($commonLanguages); // sort now by displayed language not the iso-code - usort($languages, function ($a, $b) { + usort($otherLanguages, function ($a, $b) { if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) { // If a doesn't have a name, but b does, list b before a return 1; @@ -680,8 +680,8 @@ class Factory implements IFactory { return [ // reset indexes - 'commonlanguages' => array_values($commonLanguages), - 'languages' => $languages + 'commonLanguages' => array_values($commonLanguages), + 'otherLanguages' => $otherLanguages ]; } } diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index efe1a6eef1d..a83f7787829 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -568,7 +568,7 @@ EOF; * * @param string $text Text of button; Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email * @param string $url URL of button - * @param string $plainText Text of button in plain text version + * @param string|false $plainText Text of button in plain text version * if empty the $text is used, if false none will be used * * @since 12.0.0 diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php index e7fc7745996..b139758596c 100644 --- a/lib/private/Preview/Movie.php +++ b/lib/private/Preview/Movie.php @@ -50,17 +50,34 @@ class Movie extends ProviderV2 { public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { // TODO: use proc_open() and stream the source file ? - $absPath = $this->getLocalFile($file, 5242880); // only use the first 5MB + $result = null; + if ($this->useTempFile($file)) { + // try downloading 5 MB first as it's likely that the first frames are present there + // in some cases this doesn't work for example when the moov atom is at the + // end of the file, so if it fails we fall back to getting the full file + $sizeAttempts = [5242880, null]; + } else { + // size is irrelevant, only attempt once + $sizeAttempts = [null]; + } - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); - if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); + foreach ($sizeAttempts as $size) { + $absPath = $this->getLocalFile($file, $size); + + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); + if ($result === null) { + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); + } } - } - $this->cleanTmpFiles(); + $this->cleanTmpFiles(); + + if ($result !== null) { + break; + } + } return $result; } diff --git a/lib/private/Preview/ProviderV2.php b/lib/private/Preview/ProviderV2.php index e7cabd10337..1b5bee0e5cc 100644 --- a/lib/private/Preview/ProviderV2.php +++ b/lib/private/Preview/ProviderV2.php @@ -70,6 +70,10 @@ abstract class ProviderV2 implements IProviderV2 { */ abstract public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage; + protected function useTempFile(File $file) { + return $file->isEncrypted() || !$file->getStorage()->isLocal(); + } + /** * Get a path to either the local file or temporary file * @@ -78,8 +82,7 @@ abstract class ProviderV2 implements IProviderV2 { * @return string */ protected function getLocalFile(File $file, int $maxSize = null): string { - $useTempFile = $file->isEncrypted() || !$file->getStorage()->isLocal(); - if ($useTempFile) { + if ($this->useTempFile($file)) { $absPath = \OC::$server->getTempManager()->getTemporaryFile(); $content = $file->fopen('r'); diff --git a/lib/private/RedisFactory.php b/lib/private/RedisFactory.php index 8160f2569e0..d8c0d12c5cc 100644 --- a/lib/private/RedisFactory.php +++ b/lib/private/RedisFactory.php @@ -27,7 +27,7 @@ namespace OC; class RedisFactory { - public const REDIS_MINIMAL_VERSION = '2.2.5'; + public const REDIS_MINIMAL_VERSION = '3.1.3'; public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0'; /** @var \Redis|\RedisCluster */ @@ -139,8 +139,8 @@ class RedisFactory { /** * Get the ssl context config * - * @param Array $config the current config - * @return Array|null + * @param array $config the current config + * @return array|null * @throws \UnexpectedValueException */ private function getSslContext($config) { @@ -167,9 +167,9 @@ class RedisFactory { return $this->instance; } - public function isAvailable() { - return extension_loaded('redis') - && version_compare(phpversion('redis'), '2.2.5', '>='); + public function isAvailable(): bool { + return \extension_loaded('redis') && + \version_compare(\phpversion('redis'), self::REDIS_MINIMAL_VERSION, '>='); } /** diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index 0bccb8190cd..fcde8f08897 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -130,8 +130,9 @@ class Router implements IRouter { if (isset($this->loadedApps[$app])) { return; } - $file = \OC_App::getAppPath($app) . '/appinfo/routes.php'; - if ($file !== false && file_exists($file)) { + $appPath = \OC_App::getAppPath($app); + $file = $appPath . '/appinfo/routes.php'; + if ($appPath !== false && file_exists($file)) { $routingFiles = [$app => $file]; } else { $routingFiles = []; diff --git a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php new file mode 100644 index 00000000000..1415b5c4131 --- /dev/null +++ b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php @@ -0,0 +1,124 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Lukas Reschke <lukas@statuscode.ch> + * + * @author Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Security\RateLimiting\Backend; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class DatabaseBackend implements IBackend { + private const TABLE_NAME = 'ratelimit_entries'; + + /** @var IDBConnection */ + private $dbConnection; + /** @var ITimeFactory */ + private $timeFactory; + + /** + * @param IDBConnection $dbConnection + * @param ITimeFactory $timeFactory + */ + public function __construct( + IDBConnection $dbConnection, + ITimeFactory $timeFactory + ) { + $this->dbConnection = $dbConnection; + $this->timeFactory = $timeFactory; + } + + /** + * @param string $methodIdentifier + * @param string $userIdentifier + * @return string + */ + private function hash(string $methodIdentifier, + string $userIdentifier): string { + return hash('sha512', $methodIdentifier . $userIdentifier); + } + + /** + * @param string $identifier + * @param int $seconds + * @return int + * @throws \OCP\DB\Exception + */ + private function getExistingAttemptCount( + string $identifier + ): int { + $currentTime = $this->timeFactory->getDateTime(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete(self::TABLE_NAME) + ->where( + $qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE)) + ) + ->executeStatement(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select($qb->func()->count()) + ->from(self::TABLE_NAME) + ->where( + $qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->gte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE)) + ); + + $cursor = $qb->executeQuery(); + $row = $cursor->fetchOne(); + $cursor->closeCursor(); + + return (int)$row; + } + + /** + * {@inheritDoc} + */ + public function getAttempts(string $methodIdentifier, + string $userIdentifier): int { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + return $this->getExistingAttemptCount($identifier); + } + + /** + * {@inheritDoc} + */ + public function registerAttempt(string $methodIdentifier, + string $userIdentifier, + int $period) { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + $deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S")); + + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->insert(self::TABLE_NAME) + ->values([ + 'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR), + 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE), + ]) + ->executeStatement(); + } +} diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php index d87f53311b2..960bfd2d159 100644 --- a/lib/private/Security/RateLimiting/Backend/IBackend.php +++ b/lib/private/Security/RateLimiting/Backend/IBackend.php @@ -35,16 +35,14 @@ namespace OC\Security\RateLimiting\Backend; */ interface IBackend { /** - * Gets the amount of attempts within the last specified seconds + * Gets the number of attempts for the specified method * * @param string $methodIdentifier Identifier for the method * @param string $userIdentifier Identifier for the user - * @param int $seconds Seconds to look back at * @return int */ public function getAttempts(string $methodIdentifier, - string $userIdentifier, - int $seconds): int; + string $userIdentifier): int; /** * Registers an attempt diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCache.php b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php index 0dab25e4048..f4880fb239c 100644 --- a/lib/private/Security/RateLimiting/Backend/MemoryCache.php +++ b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php @@ -33,12 +33,12 @@ use OCP\ICache; use OCP\ICacheFactory; /** - * Class MemoryCache uses the configured distributed memory cache for storing + * Class MemoryCacheBackend uses the configured distributed memory cache for storing * rate limiting data. * * @package OC\Security\RateLimiting\Backend */ -class MemoryCache implements IBackend { +class MemoryCacheBackend implements IBackend { /** @var ICache */ private $cache; /** @var ITimeFactory */ @@ -86,16 +86,14 @@ class MemoryCache implements IBackend { * {@inheritDoc} */ public function getAttempts(string $methodIdentifier, - string $userIdentifier, - int $seconds): int { + string $userIdentifier): int { $identifier = $this->hash($methodIdentifier, $userIdentifier); $existingAttempts = $this->getExistingAttempts($identifier); $count = 0; $currentTime = $this->timeFactory->getTime(); - /** @var array $existingAttempts */ - foreach ($existingAttempts as $attempt) { - if (($attempt + $seconds) > $currentTime) { + foreach ($existingAttempts as $expirationTime) { + if ($expirationTime > $currentTime) { $count++; } } @@ -113,16 +111,16 @@ class MemoryCache implements IBackend { $existingAttempts = $this->getExistingAttempts($identifier); $currentTime = $this->timeFactory->getTime(); - // Unset all attempts older than $period - foreach ($existingAttempts as $key => $attempt) { - if (($attempt + $period) < $currentTime) { + // Unset all attempts that are already expired + foreach ($existingAttempts as $key => $expirationTime) { + if ($expirationTime < $currentTime) { unset($existingAttempts[$key]); } } $existingAttempts = array_values($existingAttempts); // Store the new attempt - $existingAttempts[] = (string)$currentTime; + $existingAttempts[] = (string)($currentTime + $period); $this->cache->set($identifier, json_encode($existingAttempts)); } } diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php index ede72e887fc..91657452d99 100644 --- a/lib/private/Security/RateLimiting/Limiter.php +++ b/lib/private/Security/RateLimiting/Limiter.php @@ -29,23 +29,17 @@ namespace OC\Security\RateLimiting; use OC\Security\Normalizer\IpAddress; use OC\Security\RateLimiting\Backend\IBackend; use OC\Security\RateLimiting\Exception\RateLimitExceededException; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\IUser; class Limiter { /** @var IBackend */ private $backend; - /** @var ITimeFactory */ - private $timeFactory; /** - * @param ITimeFactory $timeFactory * @param IBackend $backend */ - public function __construct(ITimeFactory $timeFactory, - IBackend $backend) { + public function __construct(IBackend $backend) { $this->backend = $backend; - $this->timeFactory = $timeFactory; } /** @@ -59,12 +53,12 @@ class Limiter { string $userIdentifier, int $period, int $limit): void { - $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, $period); + $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier); if ($existingAttempts >= $limit) { throw new RateLimitExceededException(); } - $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime()); + $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period); } /** diff --git a/lib/private/Security/VerificationToken/CleanUpJob.php b/lib/private/Security/VerificationToken/CleanUpJob.php new file mode 100644 index 00000000000..331172898ec --- /dev/null +++ b/lib/private/Security/VerificationToken/CleanUpJob.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OC\Security\VerificationToken; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\Security\VerificationToken\InvalidTokenException; +use OCP\Security\VerificationToken\IVerificationToken; + +class CleanUpJob extends \OCP\BackgroundJob\Job { + + /** @var int */ + protected $runNotBefore; + /** @var string */ + protected $userId; + /** @var string */ + protected $subject; + /** @var string */ + protected $pwdPrefix; + /** @var IConfig */ + private $config; + /** @var IVerificationToken */ + private $verificationToken; + /** @var IUserManager */ + private $userManager; + + public function __construct(ITimeFactory $time, IConfig $config, IVerificationToken $verificationToken, IUserManager $userManager) { + parent::__construct($time); + $this->config = $config; + $this->verificationToken = $verificationToken; + $this->userManager = $userManager; + } + + public function setArgument($argument) { + parent::setArgument($argument); + $args = \json_decode($argument); + $this->userId = (string)$args['userId']; + $this->subject = (string)$args['subject']; + $this->pwdPrefix = (string)$args['pp']; + $this->runNotBefore = (int)$args['notBefore']; + } + + protected function run($argument) { + try { + $user = $this->userManager->get($this->userId); + if ($user === null) { + return; + } + $this->verificationToken->check('irrelevant', $user, $this->subject, $this->pwdPrefix); + } catch (InvalidTokenException $e) { + if ($e->getCode() === InvalidTokenException::TOKEN_EXPIRED) { + // make sure to only remove expired tokens + $this->config->deleteUserValue($this->userId, 'core', $this->subject); + } + } + } + + public function execute($jobList, ILogger $logger = null) { + if ($this->time->getTime() >= $this->runNotBefore) { + $jobList->remove($this, $this->argument); + parent::execute($jobList, $logger); + } + } +} diff --git a/lib/private/Security/VerificationToken/VerificationToken.php b/lib/private/Security/VerificationToken/VerificationToken.php new file mode 100644 index 00000000000..c85e0e7b5a1 --- /dev/null +++ b/lib/private/Security/VerificationToken/VerificationToken.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OC\Security\VerificationToken; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IUser; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; +use OCP\Security\VerificationToken\InvalidTokenException; +use OCP\Security\VerificationToken\IVerificationToken; +use function json_encode; + +class VerificationToken implements IVerificationToken { + protected const TOKEN_LIFETIME = 60 * 60 * 24 * 7; + + /** @var IConfig */ + private $config; + /** @var ICrypto */ + private $crypto; + /** @var ITimeFactory */ + private $timeFactory; + /** @var ISecureRandom */ + private $secureRandom; + /** @var IJobList */ + private $jobList; + + public function __construct( + IConfig $config, + ICrypto $crypto, + ITimeFactory $timeFactory, + ISecureRandom $secureRandom, + IJobList $jobList + ) { + $this->config = $config; + $this->crypto = $crypto; + $this->timeFactory = $timeFactory; + $this->secureRandom = $secureRandom; + $this->jobList = $jobList; + } + + /** + * @throws InvalidTokenException + */ + protected function throwInvalidTokenException(int $code): void { + throw new InvalidTokenException($code); + } + + public function check(string $token, ?IUser $user, string $subject, string $passwordPrefix = '', bool $expiresWithLogin = false): void { + if ($user === null || !$user->isEnabled()) { + $this->throwInvalidTokenException(InvalidTokenException::USER_UNKNOWN); + } + + $encryptedToken = $this->config->getUserValue($user->getUID(), 'core', $subject, null); + if ($encryptedToken === null) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_NOT_FOUND); + } + + try { + $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix.$this->config->getSystemValue('secret')); + } catch (\Exception $e) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR); + } + + $splitToken = explode(':', $decryptedToken ?? ''); + if (count($splitToken) !== 2) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT); + } + + if ($splitToken[0] < ($this->timeFactory->getTime() - self::TOKEN_LIFETIME) + || ($expiresWithLogin && $user->getLastLogin() > $splitToken[0])) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_EXPIRED); + } + + if (!hash_equals($splitToken[1], $token)) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_MISMATCH); + } + } + + public function create(IUser $user, string $subject, string $passwordPrefix = ''): string { + $token = $this->secureRandom->generate( + 21, + ISecureRandom::CHAR_DIGITS. + ISecureRandom::CHAR_LOWER. + ISecureRandom::CHAR_UPPER + ); + $tokenValue = $this->timeFactory->getTime() .':'. $token; + $encryptedValue = $this->crypto->encrypt($tokenValue, $passwordPrefix . $this->config->getSystemValue('secret')); + $this->config->setUserValue($user->getUID(), 'core', $subject, $encryptedValue); + $jobArgs = json_encode([ + 'userId' => $user->getUID(), + 'subject' => $subject, + 'pp' => $passwordPrefix, + 'notBefore' => $this->timeFactory->getTime() + self::TOKEN_LIFETIME * 2, // multiply to provide a grace period + ]); + $this->jobList->add(CleanUpJob::class, $jobArgs); + + return $token; + } + + public function delete(string $token, IUser $user, string $subject): void { + $this->config->deleteUserValue($user->getUID(), 'core', $subject); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 0320eda2b91..a9ea07c27d3 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -135,6 +135,7 @@ use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; use OC\Security\SecureRandom; use OC\Security\TrustedDomainHelper; +use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; use OC\Share20\ProviderFactory; use OC\Share20\ShareHelper; @@ -224,6 +225,7 @@ use OCP\Security\ICredentialsManager; use OCP\Security\ICrypto; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; +use OCP\Security\VerificationToken\IVerificationToken; use OCP\Share\IShareHelper; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; @@ -785,16 +787,28 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('Search', ISearch::class); $this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) { - return new \OC\Security\RateLimiting\Backend\MemoryCache( - $this->get(ICacheFactory::class), - new \OC\AppFramework\Utility\TimeFactory() - ); + $cacheFactory = $c->get(ICacheFactory::class); + if ($cacheFactory->isAvailable()) { + $backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend( + $this->get(ICacheFactory::class), + new \OC\AppFramework\Utility\TimeFactory() + ); + } else { + $backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend( + $c->get(IDBConnection::class), + new \OC\AppFramework\Utility\TimeFactory() + ); + } + + return $backend; }); $this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('SecureRandom', \OCP\Security\ISecureRandom::class); + $this->registerAlias(IVerificationToken::class, VerificationToken::class); + $this->registerAlias(ICrypto::class, Crypto::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('Crypto', ICrypto::class); diff --git a/lib/private/Settings/AuthorizedGroup.php b/lib/private/Settings/AuthorizedGroup.php new file mode 100644 index 00000000000..d549e3d48f3 --- /dev/null +++ b/lib/private/Settings/AuthorizedGroup.php @@ -0,0 +1,50 @@ +<?php + +/** + * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OC\Settings; + +use OCP\AppFramework\Db\Entity; + +/** + * @method setGroupId(string $groupId) + * @method setClass(string $class) + * @method getGroupId(): string + * @method getClass(): string + */ +class AuthorizedGroup extends Entity implements \JsonSerializable { + + /** @var string $group_id */ + protected $groupId; + + /** @var string $class */ + protected $class; + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'group_id' => $this->groupId, + 'class' => $this->class + ]; + } +} diff --git a/lib/private/Settings/AuthorizedGroupMapper.php b/lib/private/Settings/AuthorizedGroupMapper.php new file mode 100644 index 00000000000..4313ce60580 --- /dev/null +++ b/lib/private/Settings/AuthorizedGroupMapper.php @@ -0,0 +1,125 @@ +<?php +/** + * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OC\Settings; + +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; + +class AuthorizedGroupMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authorized_groups', AuthorizedGroup::class); + } + + /** + * @throws Exception + */ + public function findAllClassesForUser(IUser $user): array { + $qb = $this->db->getQueryBuilder(); + + /** @var IGroupManager $groupManager */ + $groupManager = \OC::$server->get(IGroupManager::class); + $groups = $groupManager->getUserGroups($user); + if (count($groups) === 0) { + return []; + } + + $result = $qb->select('class') + ->from($this->getTableName(), 'auth') + ->where($qb->expr()->in('group_id', array_map(function (IGroup $group) use ($qb) { + return $qb->createNamedParameter($group->getGID()); + }, $groups), IQueryBuilder::PARAM_STR)) + ->executeQuery(); + + $classes = []; + while ($row = $result->fetch()) { + $classes[] = $row['class']; + } + $result->closeCursor(); + return $classes; + } + + /** + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws \OCP\DB\Exception + */ + public function find(int $id): AuthorizedGroup { + $queryBuilder = $this->db->getQueryBuilder(); + $queryBuilder->select('*') + ->from($this->getTableName()) + ->where($queryBuilder->expr()->eq('id', $queryBuilder->createNamedParameter($id))); + /** @var AuthorizedGroup $authorizedGroup */ + $authorizedGroup = $this->findEntity($queryBuilder); + return $authorizedGroup; + } + + /** + * Get all the authorizations stored in the database. + * + * @return AuthorizedGroup[] + * @throws \OCP\DB\Exception + */ + public function findAll(): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*')->from($this->getTableName()); + return $this->findEntities($qb); + } + + public function findByGroupIdAndClass(string $groupId, string $class) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($groupId))) + ->andWhere($qb->expr()->eq('class', $qb->createNamedParameter($class))); + return $this->findEntity($qb); + } + + /** + * @return Entity[] + * @throws \OCP\DB\Exception + */ + public function findExistingGroupsForClass(string $class): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('class', $qb->createNamedParameter($class))); + return $this->findEntities($qb); + } + + /** + * @throws Exception + */ + public function removeGroup(string $gid) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) + ->executeStatement(); + } +} diff --git a/lib/private/Settings/Manager.php b/lib/private/Settings/Manager.php index d6b4ce7c080..ebda3fe021d 100644 --- a/lib/private/Settings/Manager.php +++ b/lib/private/Settings/Manager.php @@ -11,6 +11,7 @@ * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author sualko <klaus@jsxc.org> + * @author Carl Schwan <carl@carlschwan.eu> * * @license GNU AGPL version 3 or any later version * @@ -28,23 +29,27 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OC\Settings; use Closure; use OCP\AppFramework\QueryException; +use OCP\Group\ISubAdmin; +use OCP\IGroupManager; use OCP\IL10N; -use OCP\ILogger; use OCP\IServerContainer; use OCP\IURLGenerator; +use OCP\IUser; use OCP\L10N\IFactory; use OCP\Settings\IIconSection; use OCP\Settings\IManager; use OCP\Settings\ISettings; use OCP\Settings\ISubAdminSettings; +use Psr\Log\LoggerInterface; class Manager implements IManager { - /** @var ILogger */ + /** @var LoggerInterface */ private $log; /** @var IL10N */ @@ -59,16 +64,31 @@ class Manager implements IManager { /** @var IServerContainer */ private $container; + /** @var AuthorizedGroupMapper $mapper */ + private $mapper; + + /** @var IGroupManager $groupManager */ + private $groupManager; + + /** @var ISubAdmin $subAdmin */ + private $subAdmin; + public function __construct( - ILogger $log, + LoggerInterface $log, IFactory $l10nFactory, IURLGenerator $url, - IServerContainer $container + IServerContainer $container, + AuthorizedGroupMapper $mapper, + IGroupManager $groupManager, + ISubAdmin $subAdmin ) { $this->log = $log; $this->l10nFactory = $l10nFactory; $this->url = $url; $this->container = $container; + $this->mapper = $mapper; + $this->groupManager = $groupManager; + $this->subAdmin = $subAdmin; } /** @var array */ @@ -106,18 +126,14 @@ class Manager implements IManager { } foreach (array_unique($this->sectionClasses[$type]) as $index => $class) { - try { - /** @var IIconSection $section */ - $section = \OC::$server->query($class); - } catch (QueryException $e) { - $this->log->logException($e, ['level' => ILogger::INFO]); - continue; - } + /** @var IIconSection $section */ + $section = \OC::$server->get($class); $sectionID = $section->getID(); - if ($sectionID !== 'connected-accounts' && isset($this->sections[$type][$sectionID])) { - $this->log->logException(new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class), ['level' => ILogger::INFO]); + if (!$this->isKnownDuplicateSectionId($sectionID) && isset($this->sections[$type][$sectionID])) { + $e = new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class); + $this->log->info($e->getMessage(), ['exception' => $e]); continue; } @@ -129,6 +145,13 @@ class Manager implements IManager { return $this->sections[$type]; } + protected function isKnownDuplicateSectionId(string $sectionID): bool { + return in_array($sectionID, [ + 'connected-accounts', + 'notifications', + ], true); + } + /** @var array */ protected $settingClasses = []; @@ -136,8 +159,9 @@ class Manager implements IManager { protected $settings = []; /** - * @param string $type 'admin' or 'personal' - * @param string $setting Class must implement OCP\Settings\ISetting + * @psam-param 'admin'|'personal' $type The type of the setting. + * @param string $setting Class must implement OCP\Settings\ISettings + * @param bool $allowedDelegation * * @return void */ @@ -167,14 +191,15 @@ class Manager implements IManager { try { /** @var ISettings $setting */ - $setting = $this->container->query($class); + $setting = $this->container->get($class); } catch (QueryException $e) { - $this->log->logException($e, ['level' => ILogger::INFO]); + $this->log->info($e->getMessage(), ['exception' => $e]); continue; } if (!$setting instanceof ISettings) { - $this->log->logException(new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')'), ['level' => ILogger::INFO]); + $e = new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')'); + $this->log->info($e->getMessage(), ['exception' => $e]); continue; } @@ -307,4 +332,52 @@ class Manager implements IManager { ksort($settings); return $settings; } + + public function getAllowedAdminSettings(string $section, IUser $user): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + $isSubAdmin = $this->subAdmin->isSubAdmin($user); + $subAdminOnly = !$isAdmin && $isSubAdmin; + + if ($subAdminOnly) { + // not an admin => look if the user is still authorized to access some + // settings + $subAdminSettingsFilter = function (ISettings $settings) { + return $settings instanceof ISubAdminSettings; + }; + $appSettings = $this->getSettings('admin', $section, $subAdminSettingsFilter); + } elseif ($isAdmin) { + $appSettings = $this->getSettings('admin', $section); + } else { + $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user); + $authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) { + return in_array(get_class($settings), $authorizedSettingsClasses) === true; + }; + $appSettings = $this->getSettings('admin', $section, $authorizedGroupFilter); + } + + $settings = []; + foreach ($appSettings as $setting) { + if (!isset($settings[$setting->getPriority()])) { + $settings[$setting->getPriority()] = []; + } + $settings[$setting->getPriority()][] = $setting; + } + + ksort($settings); + return $settings; + } + + public function getAllAllowedAdminSettings(IUser $user): array { + $this->getSettings('admin', ''); // Make sure all the settings are loaded + $settings = []; + $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user); + foreach ($this->settings['admin'] as $section) { + foreach ($section as $setting) { + if (in_array(get_class($setting), $authorizedSettingsClasses) === true) { + $settings[] = $setting; + } + } + } + return $settings; + } } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index a4873e63aa9..c24d417f8cf 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -439,7 +439,7 @@ class Setup { // Set email for admin if (!empty($options['adminemail'])) { - $config->setUserValue($user->getUID(), 'settings', 'email', $options['adminemail']); + $user->setSystemEMailAddress($options['adminemail']); } } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 7047c32e339..da8de81208e 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -44,6 +44,7 @@ namespace OC\Share20; use OC\Cache\CappedMemoryCache; use OC\Files\Mount\MoveableMount; use OC\Share20\Exception\ProviderException; +use OCA\Files_Sharing\AppInfo\Application; use OCA\Files_Sharing\ISharedStorage; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; @@ -788,7 +789,15 @@ class Manager implements IManager { } // Generate the target - $target = $this->config->getSystemValue('share_folder', '/') . '/' . $share->getNode()->getName(); + $defaultShareFolder = $this->config->getSystemValue('share_folder', '/'); + $allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true); + if ($allowCustomShareFolder) { + $shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $defaultShareFolder); + } else { + $shareFolder = $defaultShareFolder; + } + + $target = $shareFolder . '/' . $share->getNode()->getName(); $target = \OC\Files\Filesystem::normalizePath($target); $share->setTarget($target); diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index be231e1a7e9..c435b9180b9 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -83,6 +83,27 @@ class SystemConfig { ], ], ], + 'objectstore_multibucket' => [ + 'arguments' => [ + 'options' => [ + 'credentials' => [ + 'key' => true, + 'secret' => true, + ] + ], + // S3 + 'key' => true, + 'secret' => true, + // Swift v2 + 'username' => true, + 'password' => true, + // Swift v3 + 'user' => [ + 'name' => true, + 'password' => true, + ], + ], + ], ]; /** @var Config */ diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php index 2087f5f8ed9..2de8c7ad5b1 100644 --- a/lib/private/Template/Base.php +++ b/lib/private/Template/Base.php @@ -65,7 +65,7 @@ class Base { */ protected function getAppTemplateDirs($theme, $app, $serverRoot, $app_dir) { // Check if the app is in the app folder or in the root - if (file_exists($app_dir.'/templates/')) { + if ($app_dir !== false && file_exists($app_dir.'/templates/')) { return [ $serverRoot.'/themes/'.$theme.'/apps/'.$app.'/templates/', $app_dir.'/templates/', diff --git a/lib/private/Template/IconsCacher.php b/lib/private/Template/IconsCacher.php index e379a8ed92d..01500aa2e9c 100644 --- a/lib/private/Template/IconsCacher.php +++ b/lib/private/Template/IconsCacher.php @@ -170,7 +170,10 @@ class IconsCacher { } elseif (\strpos($url, $base) === 0) { if (\preg_match('/([A-z0-9\_\-]+)\/([a-zA-Z0-9-_\~\/\.\=\:\;\+\,]+)\?color=([0-9a-fA-F]{3,6})/', $cleanUrl, $matches)) { [,$app,$cleanUrl, $color] = $matches; - $location = \OC_App::getAppPath($app) . '/img/' . $cleanUrl . '.svg'; + $appPath = \OC_App::getAppPath($app); + if ($appPath !== false) { + $location = $appPath . '/img/' . $cleanUrl . '.svg'; + } if ($app === 'settings') { $location = \OC::$SERVERROOT . '/settings/img/' . $cleanUrl . '.svg'; } diff --git a/lib/private/Template/ResourceLocator.php b/lib/private/Template/ResourceLocator.php index 3f3299e2e84..3ca34259907 100755 --- a/lib/private/Template/ResourceLocator.php +++ b/lib/private/Template/ResourceLocator.php @@ -102,7 +102,7 @@ abstract class ResourceLocator { * @return bool True if the resource was found, false otherwise */ protected function appendIfExist($root, $file, $webRoot = null) { - if (is_file($root.'/'.$file)) { + if ($root !== false && is_file($root.'/'.$file)) { $this->append($root, $file, $webRoot, false); return true; } diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index f7fa6fa5632..382179b23e0 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -8,6 +8,7 @@ declare(strict_types=1); * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Bart Visscher <bartv@thisnet.nl> * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Daniel Rudolf <github.com@daniel-rudolf.de> * @author Felix Epp <work@felixepp.de> * @author Joas Schilling <coding@schilljs.com> * @author Jörn Friedrich Dreyer <jfd@butonic.de> @@ -45,6 +46,7 @@ use OCP\ICacheFactory; use OCP\IConfig; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUserSession; use RuntimeException; /** @@ -53,6 +55,8 @@ use RuntimeException; class URLGenerator implements IURLGenerator { /** @var IConfig */ private $config; + /** @var IUserSession */ + public $userSession; /** @var ICacheFactory */ private $cacheFactory; /** @var IRequest */ @@ -63,10 +67,12 @@ class URLGenerator implements IURLGenerator { private $baseUrl = null; public function __construct(IConfig $config, + IUserSession $userSession, ICacheFactory $cacheFactory, IRequest $request, Router $router) { $this->config = $config; + $this->userSession = $userSession; $this->cacheFactory = $cacheFactory; $this->request = $request; $this->router = $router; @@ -268,10 +274,54 @@ class URLGenerator implements IURLGenerator { } /** + * Returns the URL of the default page based on the system configuration + * and the apps visible for the current user + * @return string + */ + public function linkToDefaultPageUrl(): string { + // Deny the redirect if the URL contains a @ + // This prevents unvalidated redirects like ?redirect_url=:user@domain.com + if (isset($_REQUEST['redirect_url']) && strpos($_REQUEST['redirect_url'], '@') === false) { + return $this->getAbsoluteURL(urldecode($_REQUEST['redirect_url'])); + } + + $defaultPage = $this->config->getAppValue('core', 'defaultpage'); + if ($defaultPage) { + return $this->getAbsoluteURL($defaultPage); + } + + $appId = 'files'; + $defaultApps = explode(',', $this->config->getSystemValue('defaultapp', 'dashboard,files')); + + $userId = $this->userSession->isLoggedIn() ? $this->userSession->getUser()->getUID() : null; + if ($userId !== null) { + $userDefaultApps = explode(',', $this->config->getUserValue($userId, 'core', 'defaultapp')); + $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps)); + } + + // find the first app that is enabled for the current user + foreach ($defaultApps as $defaultApp) { + $defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp)); + if (\OC::$server->getAppManager()->isEnabledForUser($defaultApp)) { + $appId = $defaultApp; + break; + } + } + + if ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true + || getenv('front_controller_active') === 'true') { + return $this->getAbsoluteURL('/apps/' . $appId . '/'); + } + + return $this->getAbsoluteURL('/index.php/apps/' . $appId . '/'); + } + + /** * @return string base url of the current request */ public function getBaseUrl(): string { - if ($this->baseUrl === null) { + // BaseUrl can be equal to 'http(s)://' during the first steps of the intial setup. + if ($this->baseUrl === null || $this->baseUrl === "http://" || $this->baseUrl === "https://") { $this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT; } return $this->baseUrl; diff --git a/lib/private/Updater.php b/lib/private/Updater.php index cffdac310cb..4ddb5e2b7cb 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -362,14 +362,12 @@ class Updater extends BasicEmitter { * This is important if you upgrade ownCloud and have non ported 3rd * party apps installed. * - * @return array * @throws \Exception */ - private function checkAppsRequirements(): array { + private function checkAppsRequirements(): void { $isCoreUpgrade = $this->isCodeUpgrade(); $apps = OC_App::getEnabledApps(); $version = implode('.', Util::getVersion()); - $disabledApps = []; $appManager = \OC::$server->getAppManager(); foreach ($apps as $app) { // check if the app is compatible with this version of Nextcloud @@ -378,23 +376,10 @@ class Updater extends BasicEmitter { if ($appManager->isShipped($app)) { throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update'); } - \OC::$server->getAppManager()->disableApp($app, true); + $appManager->disableApp($app, true); $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]); } - // no need to disable any app in case this is a non-core upgrade - if (!$isCoreUpgrade) { - continue; - } - // shipped apps will remain enabled - if ($appManager->isShipped($app)) { - continue; - } - // authentication and session apps will remain enabled as well - if (OC_App::isType($app, ['session', 'authentication'])) { - continue; - } } - return $disabledApps; } /** diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 1827be61a7a..3e30861f2a4 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -700,6 +700,7 @@ class Manager extends PublicEmitter implements IUserManager { * @since 9.1.0 */ public function getByEmail($email) { + // looking for 'email' only (and not primary_mail) is intentional $userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email); $users = array_map(function ($uid) { diff --git a/lib/private/User/User.php b/lib/private/User/User.php index f17824f51b9..5fa1272f95c 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -34,10 +34,12 @@ */ namespace OC\User; +use InvalidArgumentException; use OC\Accounts\AccountManager; use OC\Avatar\AvatarManager; use OC\Hooks\Emitter; use OC_Helper; +use OCP\Accounts\IAccountManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Events\BeforeUserRemovedEvent; use OCP\Group\Events\UserRemovedEvent; @@ -55,6 +57,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; class User implements IUser { + /** @var IAccountManager */ + protected $accountManager; /** @var string */ private $uid; @@ -165,25 +169,62 @@ class User implements IUser { } /** - * set the email address of the user - * - * @param string|null $mailAddress - * @return void - * @since 9.0.0 + * @inheritDoc */ public function setEMailAddress($mailAddress) { - $oldMailAddress = $this->getEMailAddress(); + $this->setSystemEMailAddress($mailAddress); + } + + /** + * @inheritDoc + */ + public function setSystemEMailAddress(string $mailAddress): void { + $oldMailAddress = $this->getSystemEMailAddress(); + + if ($mailAddress === '') { + $this->config->deleteUserValue($this->uid, 'settings', 'email'); + } else { + $this->config->setUserValue($this->uid, 'settings', 'email', $mailAddress); + } + + $primaryAddress = $this->getPrimaryEMailAddress(); + if ($primaryAddress === $mailAddress) { + // on match no dedicated primary settings is necessary + $this->setPrimaryEMailAddress(''); + } + if ($oldMailAddress !== $mailAddress) { - if ($mailAddress === '') { - $this->config->deleteUserValue($this->uid, 'settings', 'email'); - } else { - $this->config->setUserValue($this->uid, 'settings', 'email', $mailAddress); - } $this->triggerChange('eMailAddress', $mailAddress, $oldMailAddress); } } /** + * @inheritDoc + */ + public function setPrimaryEMailAddress(string $mailAddress): void { + if ($mailAddress === '') { + $this->config->deleteUserValue($this->uid, 'settings', 'primary_email'); + return; + } + + $this->ensureAccountManager(); + $account = $this->accountManager->getAccount($this); + $property = $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL) + ->getPropertyByValue($mailAddress); + + if ($property === null || $property->getLocallyVerified() !== IAccountManager::VERIFIED) { + throw new InvalidArgumentException('Only verified emails can be set as primary'); + } + $this->config->setUserValue($this->uid, 'settings', 'primary_email', $mailAddress); + } + + private function ensureAccountManager() { + if (!$this->accountManager instanceof IAccountManager) { + $this->accountManager = \OC::$server->get(IAccountManager::class); + } + } + + /** * returns the timestamp of the user's last login or 0 if the user did never * login * @@ -390,10 +431,24 @@ class User implements IUser { * @since 9.0.0 */ public function getEMailAddress() { + return $this->getPrimaryEMailAddress() ?? $this->getSystemEMailAddress(); + } + + /** + * @inheritDoc + */ + public function getSystemEMailAddress(): ?string { return $this->config->getUserValue($this->uid, 'settings', 'email', null); } /** + * @inheritDoc + */ + public function getPrimaryEMailAddress(): ?string { + return $this->config->getUserValue($this->uid, 'settings', 'primary_email', null); + } + + /** * get the users' quota * * @return string diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index bca0a3dd08e..811703570a2 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -61,6 +61,7 @@ use OCP\App\ManagerEvent; use OCP\AppFramework\QueryException; use OCP\Authentication\IAlternativeLogin; use OCP\ILogger; +use OCP\Settings\IManager as ISettingsManager; use Psr\Log\LoggerInterface; /** @@ -223,22 +224,22 @@ class OC_App { if (!empty($info['settings']['admin'])) { foreach ($info['settings']['admin'] as $setting) { - \OC::$server->getSettingsManager()->registerSetting('admin', $setting); + \OC::$server->get(ISettingsManager::class)->registerSetting('admin', $setting); } } if (!empty($info['settings']['admin-section'])) { foreach ($info['settings']['admin-section'] as $section) { - \OC::$server->getSettingsManager()->registerSection('admin', $section); + \OC::$server->get(ISettingsManager::class)->registerSection('admin', $section); } } if (!empty($info['settings']['personal'])) { foreach ($info['settings']['personal'] as $setting) { - \OC::$server->getSettingsManager()->registerSetting('personal', $setting); + \OC::$server->get(ISettingsManager::class)->registerSetting('personal', $setting); } } if (!empty($info['settings']['personal-section'])) { foreach ($info['settings']['personal-section'] as $section) { - \OC::$server->getSettingsManager()->registerSection('personal', $section); + \OC::$server->get(ISettingsManager::class)->registerSection('personal', $section); } } diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index f955c5c6938..848f460dac5 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -35,6 +35,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ + +use OC\User\LoginException; use OCP\EventDispatcher\IEventDispatcher; use OCP\ILogger; use OCP\IUserManager; @@ -170,6 +172,10 @@ class OC_User { if (self::getUser() !== $uid) { self::setUserId($uid); $userSession = \OC::$server->getUserSession(); + if ($userSession->getUser() && !$userSession->getUser()->isEnabled()) { + $message = \OC::$server->getL10N('lib')->t('User disabled'); + throw new LoginException($message); + } $userSession->setLoginName($uid); $request = OC::$server->getRequest(); $userSession->createSessionToken($request, $uid, $uid); diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index 333b621e359..35c81dd34e6 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -71,8 +71,8 @@ use OCP\Files\Template\ITemplateManager; use OCP\IConfig; use OCP\IGroupManager; use OCP\ILogger; +use OCP\IURLGenerator; use OCP\IUser; -use OCP\IUserSession; use OCP\Share\IManager; use Psr\Log\LoggerInterface; @@ -1090,46 +1090,9 @@ class OC_Util { * @suppress PhanDeprecatedFunction */ public static function getDefaultPageUrl() { - /** @var IConfig $config */ - $config = \OC::$server->get(IConfig::class); - $urlGenerator = \OC::$server->getURLGenerator(); - // Deny the redirect if the URL contains a @ - // This prevents unvalidated redirects like ?redirect_url=:user@domain.com - if (isset($_REQUEST['redirect_url']) && strpos($_REQUEST['redirect_url'], '@') === false) { - $location = $urlGenerator->getAbsoluteURL(urldecode($_REQUEST['redirect_url'])); - } else { - $defaultPage = \OC::$server->getConfig()->getAppValue('core', 'defaultpage'); - if ($defaultPage) { - $location = $urlGenerator->getAbsoluteURL($defaultPage); - } else { - $appId = 'files'; - $defaultApps = explode(',', $config->getSystemValue('defaultapp', 'dashboard,files')); - - /** @var IUserSession $userSession */ - $userSession = \OC::$server->get(IUserSession::class); - $user = $userSession->getUser(); - if ($user) { - $userDefaultApps = explode(',', $config->getUserValue($user->getUID(), 'core', 'defaultapp')); - $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps)); - } - - // find the first app that is enabled for the current user - foreach ($defaultApps as $defaultApp) { - $defaultApp = OC_App::cleanAppId(strip_tags($defaultApp)); - if (static::getAppManager()->isEnabledForUser($defaultApp)) { - $appId = $defaultApp; - break; - } - } - - if ($config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true') { - $location = $urlGenerator->getAbsoluteURL('/apps/' . $appId . '/'); - } else { - $location = $urlGenerator->getAbsoluteURL('/index.php/apps/' . $appId . '/'); - } - } - } - return $location; + /** @var IURLGenerator $urlGenerator */ + $urlGenerator = \OC::$server->get(IURLGenerator::class); + return $urlGenerator->linkToDefaultPageUrl(); } /** |