aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/Accounts/AccountManager.php131
-rw-r--r--lib/private/Accounts/AccountProperty.php21
-rw-r--r--lib/private/Accounts/AccountPropertyCollection.php9
-rw-r--r--lib/private/App/InfoParser.php2
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php5
-rw-r--r--lib/private/AppFramework/Http/Dispatcher.php2
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php47
-rw-r--r--lib/private/Collaboration/Collaborators/UserPlugin.php4
-rw-r--r--lib/private/Contacts/ContactsMenu/Entry.php1
-rw-r--r--lib/private/DB/AdapterMySQL.php12
-rw-r--r--lib/private/DB/ConnectionFactory.php4
-rw-r--r--lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php6
-rw-r--r--lib/private/Files/Stream/SeekableHttpStream.php11
-rw-r--r--lib/private/L10N/Factory.php14
-rw-r--r--lib/private/Mail/EMailTemplate.php2
-rw-r--r--lib/private/Preview/Movie.php31
-rw-r--r--lib/private/Preview/ProviderV2.php7
-rw-r--r--lib/private/RedisFactory.php12
-rw-r--r--lib/private/Route/Router.php5
-rw-r--r--lib/private/Security/RateLimiting/Backend/DatabaseBackend.php124
-rw-r--r--lib/private/Security/RateLimiting/Backend/IBackend.php6
-rw-r--r--lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php (renamed from lib/private/Security/RateLimiting/Backend/MemoryCache.php)20
-rw-r--r--lib/private/Security/RateLimiting/Limiter.php12
-rw-r--r--lib/private/Security/VerificationToken/CleanUpJob.php90
-rw-r--r--lib/private/Security/VerificationToken/VerificationToken.php129
-rw-r--r--lib/private/Server.php22
-rw-r--r--lib/private/Settings/AuthorizedGroup.php50
-rw-r--r--lib/private/Settings/AuthorizedGroupMapper.php125
-rw-r--r--lib/private/Settings/Manager.php109
-rw-r--r--lib/private/Setup.php2
-rw-r--r--lib/private/Share20/Manager.php11
-rw-r--r--lib/private/SystemConfig.php21
-rw-r--r--lib/private/Template/Base.php2
-rw-r--r--lib/private/Template/IconsCacher.php5
-rwxr-xr-xlib/private/Template/ResourceLocator.php2
-rw-r--r--lib/private/URLGenerator.php52
-rw-r--r--lib/private/Updater.php19
-rw-r--r--lib/private/User/Manager.php1
-rw-r--r--lib/private/User/User.php77
-rw-r--r--lib/private/legacy/OC_App.php9
-rw-r--r--lib/private/legacy/OC_User.php6
-rw-r--r--lib/private/legacy/OC_Util.php45
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();
}
/**