Compare commits

...

59 Commits

Author SHA1 Message Date
John Molakvoæ c67e1420c8 Merge pull request #27813 from nextcloud/version/22/final
22.0.0
2021-07-05 22:20:57 +02:00
Lukas Reschke 3f212b8857 Merge pull request #27814 from nextcloud/backport/27758/stable22
[stable22] Fix DnsPinMiddleware resolve pinning bug
2021-07-05 18:38:55 +02:00
Aaron Ball f8db7ce8f5 Fix DnsPinMiddleware resolve pinning bug
Libcurl expects the value of the CURLOPT_RESOLVE configurations to be an
array of strings, those strings containing a comma delimited list of
resolved IPs for each host:port combination.

The original code here does create that array with the host:port:ip
combination, but multiple ips for a single host:port result in
additional array entries, rather than adding them to the end of the
string with a comma. Per the libcurl docs, the `CURLOPT_RESOLVE` array
entries should match the syntax `host:port:address[,address]`.

This creates a function-scoped associative array which uses `host:port`
as the key (which are supposed to be unique and this ensures that), and
the value is an array containing IP strings (ipv4 or ipv6). Once the
associative array is populated, it is then set to the CURLOPT_RESOLVE
array, imploding the ip arrays using a comma delimiter so the array
syntax matches the expected by libcurl.

Note that this reorders the "foreach ip" and "foreach port" loops.
Rather than looping over ips then ports, we now loop over ports then
ips, since ports are part of the unique host:port map, and multiple ips
can exist therein.

Signed-off-by: Aaron Ball <nullspoon@oper.io>
2021-07-05 15:40:06 +00:00
John Molakvoæ c73070c19d Merge pull request #27752 from nextcloud/backport/27586/stable22
[stable22] Reset checksum when writing files to object store
2021-07-05 17:27:07 +02:00
Lukas Reschke fb8e681161 Merge pull request #27812 from nextcloud/backport/27810/stable22
[stable22] Add a text string to l10n
2021-07-05 17:21:20 +02:00
Arthur Schiwon 0ab8f2f15a 22.0.0
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-07-05 17:09:36 +02:00
Valdnet f6f85478a0 Add a text string to l10n 2021-07-05 14:38:12 +00:00
Valdnet 5e93d571e0 Add a text string to l10n 2021-07-05 14:38:12 +00:00
Lukas Reschke afb7126022 Merge pull request #27802 from nextcloud/backport/27198/stable22
[stable22] Run s3 tests again
2021-07-05 15:05:04 +02:00
Julius Härtl 6a4a5d888e Use minio for s3 tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-05 09:56:23 +00:00
Julius Härtl cb57285870 Run s3 tests again
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-05 09:56:23 +00:00
John Molakvoæ 268e6af00e Merge pull request #27775 from nextcloud/backport/27750/stable22 2021-07-05 08:45:24 +02:00
Robin Appelman cb0e76105e dont include folder being search in in the results
Signed-off-by: Robin Appelman <robin@icewind.nl>
2021-07-02 15:52:17 +00:00
blizzz 6c24e13d4b Merge pull request #27749 from nextcloud/backport/27737/stable22
[stable22] make contactsmenu icon bigger
2021-07-01 18:44:29 +02:00
Julius Härtl 226e0c4107 Reset checksum when writing files to object store
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-01 15:33:47 +00:00
szaimen b5b58faec0 make contactsmenu icon bigger
Signed-off-by: szaimen <szaimen@e.mail.de>
2021-07-01 13:26:26 +00:00
John Molakvoæ 863686c283 Merge pull request #27706 from nextcloud/version/22.0.0/rc2
22.0.0 RC2
2021-07-01 11:30:42 +02:00
John Molakvoæ e2c96aebd1 Merge pull request #27743 from nextcloud/backport/27739/stable22 2021-07-01 11:30:25 +02:00
szaimen b25e8e7030 design fixes to app-settings button
Signed-off-by: szaimen <szaimen@e.mail.de>
2021-07-01 08:46:48 +00:00
John Molakvoæ 858da2ed1d Merge pull request #27719 from nextcloud/backport/27474/stable22 2021-07-01 08:31:25 +02:00
blizzz 9882ae8986 Merge pull request #27736 from nextcloud/backport/27732/stable22
[stable22] Fix LDAPProviderFactory not found
2021-06-30 20:41:23 +02:00
Arthur Schiwon 87d8e8f6d3 unset ldap provider when disabling user_ldap
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 16:52:09 +00:00
Arthur Schiwon 1220e48301 ensure that factoryClass exisits before instantiation
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 16:52:09 +00:00
Vincent Petry 28a21048ac Merge pull request #27726 from nextcloud/backport/27638/stable22
[stable22] Downstream encryption:fix-encrypted-version for repairing "bad signature" errors
2021-06-30 16:56:26 +02:00
Vincent Petry 3ca664dda1 Prevent running FixEncryptedVersion without master key
Return an error when running occ encryption:fix-encrypted-version
when master key encryption is not enabled.

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Vincent Petry 48be209e92 Fix FixEncryptedVersionTest test
Fixed setup to use EncryptionTrait like other existing tests.
Fix expectations to not rely on side effects from previous test cases.

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Vincent Petry bf279980ac Fix warnings in FixEncryptedVersion command
Fixed code warnings

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Vincent Petry 5bde3d1836 Detect disabled signature check when reparing
When running occ encryption:fix-encrypted-version, detect whether the
setting 'encryption_skip_signature_check' is set and abort if it is,
because the repair cannot detect version mismatch errors with it
enabled.

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Vincent Petry 6b83de79c6 Downstream FixEncryptedVersionTest
Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Vincent Petry 727d2300b6 Downstream encryption:fix-encrypted-version
For fixing "Bad signature" errors.

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-30 12:21:06 +00:00
Arthur Schiwon 1f6f863d86 fix incredible off-by-one-typo-error
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 11:04:46 +02:00
Arthur Schiwon 1754c3d979 cleanup
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon 29b36c56f3 fix small issues in UsersController handling
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon cc1d24a008 adjust access permissions of new controller method
- fixes wrong veriable usage also

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon 3550b12e3e fix regex
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon 4565f5d3b4 fix provisioning test check
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon 9d6c11f7da adjust internal data handling logic to fix store and load
- format as stored previously in oc_accounts table is kept

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:50 +00:00
Arthur Schiwon 87efcd06ca create a property on editUser when it was not set before
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon b30ce04e15 adjust email verification checker
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon bcedd4031d fix code style
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon 63d2aad5d3 adjust verification state updater method
- also fixes scope of internal methods

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon 19d2367340 make AccountManager actually write multi value properties
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon 51cae9ba98 accounts event handler to use eventdispatcher, DI and Accounts API
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon 148a62939f prov api to be able to edit multivalue properties
- adding as usual
- deleting and scope setting via additional endpoint

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon eb5e445af4 prov api reports multiple mail as editable field
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:49 +00:00
Arthur Schiwon 4398cf85c1 prov api reports additional emails on getUser
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-30 05:15:48 +00:00
Julius Härtl e74f5aeec2 Merge pull request #27716 from nextcloud/backport/27715/stable22
[stable22] Revert "First attempt to check against core routes before loading all app routes"
2021-06-29 21:47:03 +02:00
Vincent Petry 98005fc331 Revert "First attempt to check against core routes before loading all app routes"
Signed-off-by: Vincent Petry <vincent@nextcloud.com>
2021-06-29 18:52:21 +00:00
Julius Härtl 7e506bab4c Merge pull request #27682 from nextcloud/backport/27668/stable22
[stable22] Harden bootstrap context registrations when apps are missing
2021-06-29 18:05:29 +02:00
Julius Härtl a614537edb Merge pull request #27678 from nextcloud/backport/27675/stable22
[stable22] Validate the theming color also on CLI
2021-06-29 15:16:54 +02:00
Arthur Schiwon f6e544119a 22.0.0 RC2
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-29 12:55:30 +02:00
John Molakvoæ 8ee15f9984 Merge pull request #27702 from nextcloud/backport/27698/stable22
[stable22] LDAP: determine shares of offline users only when needed
2021-06-29 08:27:58 +02:00
Arthur Schiwon b299369c15 LDAP: determine shares of offline users only when needed
- determine shares may via Sharing code result in user exists checks
- this may result in an infinite loop when user exists was called before
- the info is really only required at one occ command

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2021-06-28 17:15:51 +00:00
Joas Schilling 61a3132c76 Merge pull request #27649 from nextcloud/techdebt/noid/stable22-version-fixing
Fix branch selection in stable22
2021-06-28 14:42:25 +02:00
Christoph Wurst 3b5b2af215 Harden bootstrap context registrations when apps are missing
It's not expected that an app would be unavailable when the app
container is created but when services are registered, but Sentry tells
me on Nextcloud 21 there is an edge case where this can happen.
Therefore this patch hardens the code a bit to log a meaningful error
message and skipping the next code instead of logging a php notice for
the undefined index and an exception for calling a method on null.

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2021-06-25 16:46:57 +00:00
Joas Schilling dfc096737d Validate the theming color also on CLI
Signed-off-by: Joas Schilling <coding@schilljs.com>
2021-06-25 16:18:22 +00:00
Christoph Wurst 57a4ef9da8 Merge pull request #27669 from nextcloud/backport/27663/stable22
[stable22] Unshift crash reports when they are loaded, to break the recusion
2021-06-25 13:57:48 +02:00
Christoph Wurst 3d7e3185fc Unshift crash reports when they are loaded, to break the recusion
If, for whatever reason, during the loading of a crash reporter a new
log entry is generated, then the lazy loading mechanism will be invoked
*again* while it's already executed. This doesn't result in an endless
recursion, but means that the crash reporters will be built and
registered many times. This then means any further log entry will be
logged x times instead of once.

Unshift makes sure to take the class off the registration list right
away, so another invokation of the same method won't try to do the same
job.

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2021-06-25 06:34:13 +00:00
Joas Schilling 99b7b82d72 Fix branch selection in stable22
Signed-off-by: Joas Schilling <coding@schilljs.com>
2021-06-24 13:12:52 +02:00
49 changed files with 1691 additions and 734 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
codecov:
branch: master
branch: stable22
ci:
- drone.nextcloud.com
- !scrutinizer-ci.com
+12 -5
View File
@@ -1403,7 +1403,7 @@ steps:
commands:
# JavaScript files are not used in integration tests so it is not needed to
# build them.
- git clone --depth 1 https://github.com/nextcloud/spreed apps/spreed
- git clone --depth 1 --branch stable22 https://github.com/nextcloud/spreed apps/spreed
- name: integration-sharing-v1-video-verification
image: ghcr.io/nextcloud/continuous-integration-integration-php7.3:integration-php7.3-2
commands:
@@ -2117,6 +2117,15 @@ kind: pipeline
name: object-store-s3
steps:
- name: minio
image: ghcr.io/nextcloud/continuous-integration-minio:latest
detach: true
commands:
- mkdir /s3data
- minio server /s3data
environment:
MINIO_ROOT_USER: nextcloud
MINIO_ROOT_PASSWORD: nextcloud
- name: submodules
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
commands:
@@ -2124,6 +2133,7 @@ steps:
- name: object-store
image: ghcr.io/nextcloud/continuous-integration-php7.4:php7.4-3
environment:
OBJECT_STORE: s3
CODECOV_TOKEN:
from_secret: CODECOV_TOKEN
commands:
@@ -2133,10 +2143,6 @@ steps:
- wget https://codecov.io/bash -O codecov.sh
- bash codecov.sh -C $DRONE_COMMIT -f tests/autotest-clover-sqlite.xml
services:
- name: fake-s3
image: ghcr.io/nextcloud/continuous-integration-fake-s3:latest
trigger:
branch:
- master
@@ -2157,6 +2163,7 @@ steps:
- name: object-store
image: ghcr.io/nextcloud/continuous-integration-php7.4:php7.4-3
environment:
OBJECT_STORE: azure
CODECOV_TOKEN:
from_secret: CODECOV_TOKEN
commands:
+2 -2
View File
@@ -1,6 +1,6 @@
# Nextcloud Server ☁
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nextcloud/server/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nextcloud/server/?branch=master)
[![codecov](https://codecov.io/gh/nextcloud/server/branch/master/graph/badge.svg)](https://codecov.io/gh/nextcloud/server)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nextcloud/server/badges/quality-score.png?b=stable22)](https://scrutinizer-ci.com/g/nextcloud/server/?branch=stable22)
[![codecov](https://codecov.io/gh/nextcloud/server/branch/stable22/graph/badge.svg)](https://codecov.io/gh/nextcloud/server)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/209/badge)](https://bestpractices.coreinfrastructure.org/projects/209)
**A safe home for all your data.**
+1
View File
@@ -45,6 +45,7 @@
<command>OCA\Encryption\Command\DisableMasterKey</command>
<command>OCA\Encryption\Command\RecoverUser</command>
<command>OCA\Encryption\Command\ScanLegacyFormat</command>
<command>OCA\Encryption\Command\FixEncryptedVersion</command>
</commands>
<settings>
@@ -10,6 +10,7 @@ return array(
'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\FixEncryptedVersion' => $baseDir . '/../lib/Command/FixEncryptedVersion.php',
'OCA\\Encryption\\Command\\RecoverUser' => $baseDir . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Command\\ScanLegacyFormat' => $baseDir . '/../lib/Command/ScanLegacyFormat.php',
'OCA\\Encryption\\Controller\\RecoveryController' => $baseDir . '/../lib/Controller/RecoveryController.php',
@@ -25,6 +25,7 @@ class ComposerStaticInitEncryption
'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\FixEncryptedVersion' => __DIR__ . '/..' . '/../lib/Command/FixEncryptedVersion.php',
'OCA\\Encryption\\Command\\RecoverUser' => __DIR__ . '/..' . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Command\\ScanLegacyFormat' => __DIR__ . '/..' . '/../lib/Command/ScanLegacyFormat.php',
'OCA\\Encryption\\Controller\\RecoveryController' => __DIR__ . '/..' . '/../lib/Controller/RecoveryController.php',
@@ -0,0 +1,286 @@
<?php
/**
* @author Sujith Haridasan <sharidasan@owncloud.com>
* @author Ilja Neumann <ineumann@owncloud.com>
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Encryption\Command;
use OC\Files\View;
use OC\HintException;
use OCA\Encryption\Util;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\ILogger;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FixEncryptedVersion extends Command {
/** @var IConfig */
private $config;
/** @var ILogger */
private $logger;
/** @var IRootFolder */
private $rootFolder;
/** @var IUserManager */
private $userManager;
/** @var Util */
private $util;
/** @var View */
private $view;
public function __construct(
IConfig $config,
ILogger $logger,
IRootFolder $rootFolder,
IUserManager $userManager,
Util $util,
View $view
) {
$this->config = $config;
$this->logger = $logger;
$this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->util = $util;
$this->view = $view;
parent::__construct();
}
protected function configure(): void {
parent::configure();
$this
->setName('encryption:fix-encrypted-version')
->setDescription('Fix the encrypted version if the encrypted file(s) are not downloadable.')
->addArgument(
'user',
InputArgument::REQUIRED,
'The id of the user whose files need fixing'
)->addOption(
'path',
'p',
InputArgument::OPTIONAL,
'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.'
);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
if ($skipSignatureCheck) {
$output->writeln("<error>Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.</error>\n");
return 1;
}
if (!$this->util->isMasterKeyEnabled()) {
$output->writeln("<error>Repairing only works with master key encryption.</error>\n");
return 1;
}
$user = (string)$input->getArgument('user');
$pathToWalk = "/$user/files";
/**
* trim() returns an empty string when the argument is an unset/null
*/
$pathOption = \trim($input->getOption('path'), '/');
if ($pathOption !== "") {
$pathToWalk = "$pathToWalk/$pathOption";
}
if ($user === null) {
$output->writeln("<error>No user id provided.</error>\n");
return 1;
}
if ($this->userManager->get($user) === null) {
$output->writeln("<error>User id $user does not exist. Please provide a valid user id</error>");
return 1;
}
return $this->walkPathOfUser($user, $pathToWalk, $output);
}
/**
* @param string $user
* @param string $path
* @param OutputInterface $output
* @return int 0 for success, 1 for error
*/
private function walkPathOfUser($user, $path, OutputInterface $output): int {
$this->setupUserFs($user);
if (!$this->view->file_exists($path)) {
$output->writeln("<error>Path \"$path\" does not exist. Please provide a valid path.</error>");
return 1;
}
if ($this->view->is_file($path)) {
$output->writeln("Verifying the content of file \"$path\"");
$this->verifyFileContent($path, $output);
return 0;
}
$directories = [];
$directories[] = $path;
while ($root = \array_pop($directories)) {
$directoryContent = $this->view->getDirectoryContent($root);
foreach ($directoryContent as $file) {
$path = $root . '/' . $file['name'];
if ($this->view->is_dir($path)) {
$directories[] = $path;
} else {
$output->writeln("Verifying the content of file \"$path\"");
$this->verifyFileContent($path, $output);
}
}
}
return 0;
}
/**
* @param string $path
* @param OutputInterface $output
* @param bool $ignoreCorrectEncVersionCall, setting this variable to false avoids recursion
*/
private function verifyFileContent($path, OutputInterface $output, $ignoreCorrectEncVersionCall = true): bool {
try {
/**
* In encryption, the files are read in a block size of 8192 bytes
* Read block size of 8192 and a bit more (808 bytes)
* If there is any problem, the first block should throw the signature
* mismatch error. Which as of now, is enough to proceed ahead to
* correct the encrypted version.
*/
$handle = $this->view->fopen($path, 'rb');
if (\fread($handle, 9001) !== false) {
$output->writeln("<info>The file \"$path\" is: OK</info>");
}
\fclose($handle);
return true;
} catch (HintException $e) {
$this->logger->warning("Issue: " . $e->getMessage());
//If allowOnce is set to false, this becomes recursive.
if ($ignoreCorrectEncVersionCall === true) {
//Lets rectify the file by correcting encrypted version
$output->writeln("<info>Attempting to fix the path: \"$path\"</info>");
return $this->correctEncryptedVersion($path, $output);
}
return false;
}
}
/**
* @param string $path
* @param OutputInterface $output
* @return bool
*/
private function correctEncryptedVersion($path, OutputInterface $output): bool {
$fileInfo = $this->view->getFileInfo($path);
if (!$fileInfo) {
$output->writeln("<warning>File info not found for file: \"$path\"</warning>");
return true;
}
$fileId = $fileInfo->getId();
$encryptedVersion = $fileInfo->getEncryptedVersion();
$wrongEncryptedVersion = $encryptedVersion;
$storage = $fileInfo->getStorage();
$cache = $storage->getCache();
$fileCache = $cache->get($fileId);
if (!$fileCache) {
$output->writeln("<warning>File cache entry not found for file: \"$path\"</warning>");
return true;
}
if ($storage->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) {
$output->writeln("<info>The file: \"$path\" is a share. Please also run the script for the owner of the share</info>");
return true;
}
// Save original encrypted version so we can restore it if decryption fails with all version
$originalEncryptedVersion = $encryptedVersion;
if ($encryptedVersion >= 0) {
//test by decrementing the value till 1 and if nothing works try incrementing
$encryptedVersion--;
while ($encryptedVersion > 0) {
$cacheInfo = ['encryptedVersion' => $encryptedVersion, 'encrypted' => $encryptedVersion];
$cache->put($fileCache->getPath(), $cacheInfo);
$output->writeln("<info>Decrement the encrypted version to $encryptedVersion</info>");
if ($this->verifyFileContent($path, $output, false) === true) {
$output->writeln("<info>Fixed the file: \"$path\" with version " . $encryptedVersion . "</info>");
return true;
}
$encryptedVersion--;
}
//So decrementing did not work. Now lets increment. Max increment is till 5
$increment = 1;
while ($increment <= 5) {
/**
* The wrongEncryptedVersion would not be incremented so nothing to worry about here.
* Only the newEncryptedVersion is incremented.
* For example if the wrong encrypted version is 4 then
* cycle1 -> newEncryptedVersion = 5 ( 4 + 1)
* cycle2 -> newEncryptedVersion = 6 ( 4 + 2)
* cycle3 -> newEncryptedVersion = 7 ( 4 + 3)
*/
$newEncryptedVersion = $wrongEncryptedVersion + $increment;
$cacheInfo = ['encryptedVersion' => $newEncryptedVersion, 'encrypted' => $newEncryptedVersion];
$cache->put($fileCache->getPath(), $cacheInfo);
$output->writeln("<info>Increment the encrypted version to $newEncryptedVersion</info>");
if ($this->verifyFileContent($path, $output, false) === true) {
$output->writeln("<info>Fixed the file: \"$path\" with version " . $newEncryptedVersion . "</info>");
return true;
}
$increment++;
}
}
$cacheInfo = ['encryptedVersion' => $originalEncryptedVersion, 'encrypted' => $originalEncryptedVersion];
$cache->put($fileCache->getPath(), $cacheInfo);
$output->writeln("<info>No fix found for \"$path\", restored version to original: $originalEncryptedVersion</info>");
return false;
}
/**
* Setup user file system
* @param string $uid
*/
private function setupUserFs($uid): void {
\OC_Util::tearDownFS();
\OC_Util::setupFS($uid);
}
}
@@ -0,0 +1,346 @@
<?php
/**
* @author Sujith Haridasan <sharidasan@owncloud.com>
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Encryption\Tests\Command;
use OC\Files\View;
use OCA\Encryption\Command\FixEncryptedVersion;
use OCA\Encryption\Util;
use Symfony\Component\Console\Tester\CommandTester;
use Test\TestCase;
use Test\Traits\EncryptionTrait;
use Test\Traits\MountProviderTrait;
use Test\Traits\UserTrait;
/**
* Class FixEncryptedVersionTest
*
* @group DB
* @package OCA\Encryption\Tests\Command
*/
class FixEncryptedVersionTest extends TestCase {
use MountProviderTrait;
use EncryptionTrait;
use UserTrait;
private $userId;
/** @var FixEncryptedVersion */
private $fixEncryptedVersion;
/** @var CommandTester */
private $commandTester;
/** @var Util|\PHPUnit\Framework\MockObject\MockObject */
protected $util;
public function setUp(): void {
parent::setUp();
\OC::$server->getConfig()->setAppValue('encryption', 'useMasterKey', '1');
$this->util = $this->getMockBuilder(Util::class)
->disableOriginalConstructor()->getMock();
$this->userId = $this->getUniqueId('user_');
$this->createUser($this->userId, 'foo12345678');
$tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
$this->registerMount($this->userId, '\OC\Files\Storage\Local', '/' . $this->userId, ['datadir' => $tmpFolder]);
$this->setupForUser($this->userId, 'foo12345678');
$this->loginWithEncryption($this->userId);
$this->fixEncryptedVersion = new FixEncryptedVersion(
\OC::$server->getConfig(),
\OC::$server->getLogger(),
\OC::$server->getRootFolder(),
\OC::$server->getUserManager(),
$this->util,
new View('/')
);
$this->commandTester = new CommandTester($this->fixEncryptedVersion);
$this->assertTrue(\OC::$server->getEncryptionManager()->isEnabled());
$this->assertTrue(\OC::$server->getEncryptionManager()->isReady());
$this->assertTrue(\OC::$server->getEncryptionManager()->isReadyForUser($this->userId));
}
/**
* In this test the encrypted version of the file is less than the original value
* but greater than zero
*/
public function testEncryptedVersionLessThanOriginalValue() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$view = new View("/" . $this->userId . "/files");
$view->touch('hello.txt');
$view->touch('world.txt');
$view->touch('foo.txt');
$view->file_put_contents('hello.txt', 'a test string for hello');
$view->file_put_contents('hello.txt', 'Yet another value');
$view->file_put_contents('hello.txt', 'Lets modify again1');
$view->file_put_contents('hello.txt', 'Lets modify again2');
$view->file_put_contents('hello.txt', 'Lets modify again3');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('foo.txt', 'a foo test');
$fileInfo1 = $view->getFileInfo('hello.txt');
$storage1 = $fileInfo1->getStorage();
$cache1 = $storage1->getCache();
$fileCache1 = $cache1->get($fileInfo1->getId());
//Now change the encrypted version to two
$cacheInfo = ['encryptedVersion' => 2, 'encrypted' => 2];
$cache1->put($fileCache1->getPath(), $cacheInfo);
$fileInfo2 = $view->getFileInfo('world.txt');
$storage2 = $fileInfo2->getStorage();
$cache2 = $storage2->getCache();
$filecache2 = $cache2->get($fileInfo2->getId());
//Now change the encrypted version to 1
$cacheInfo = ['encryptedVersion' => 1, 'encrypted' => 1];
$cache2->put($filecache2->getPath(), $cacheInfo);
$this->commandTester->execute([
'user' => $this->userId
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/foo.txt\"
The file \"/$this->userId/files/foo.txt\" is: OK", $output);
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/hello.txt\"
Attempting to fix the path: \"/$this->userId/files/hello.txt\"
Decrement the encrypted version to 1
Increment the encrypted version to 3
Increment the encrypted version to 4
Increment the encrypted version to 5
The file \"/$this->userId/files/hello.txt\" is: OK
Fixed the file: \"/$this->userId/files/hello.txt\" with version 5", $output);
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/world.txt\"
Attempting to fix the path: \"/$this->userId/files/world.txt\"
Increment the encrypted version to 2
Increment the encrypted version to 3
Increment the encrypted version to 4
The file \"/$this->userId/files/world.txt\" is: OK
Fixed the file: \"/$this->userId/files/world.txt\" with version 4", $output);
}
/**
* In this test the encrypted version of the file is greater than the original value
* but greater than zero
*/
public function testEncryptedVersionGreaterThanOriginalValue() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$view = new View("/" . $this->userId . "/files");
$view->touch('hello.txt');
$view->touch('world.txt');
$view->touch('foo.txt');
$view->file_put_contents('hello.txt', 'a test string for hello');
$view->file_put_contents('hello.txt', 'Lets modify again2');
$view->file_put_contents('hello.txt', 'Lets modify again3');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('world.txt', 'a test string for world');
$view->file_put_contents('foo.txt', 'a foo test');
$fileInfo1 = $view->getFileInfo('hello.txt');
$storage1 = $fileInfo1->getStorage();
$cache1 = $storage1->getCache();
$fileCache1 = $cache1->get($fileInfo1->getId());
//Now change the encrypted version to fifteen
$cacheInfo = ['encryptedVersion' => 5, 'encrypted' => 5];
$cache1->put($fileCache1->getPath(), $cacheInfo);
$fileInfo2 = $view->getFileInfo('world.txt');
$storage2 = $fileInfo2->getStorage();
$cache2 = $storage2->getCache();
$filecache2 = $cache2->get($fileInfo2->getId());
//Now change the encrypted version to 1
$cacheInfo = ['encryptedVersion' => 6, 'encrypted' => 6];
$cache2->put($filecache2->getPath(), $cacheInfo);
$this->commandTester->execute([
'user' => $this->userId
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/foo.txt\"
The file \"/$this->userId/files/foo.txt\" is: OK", $output);
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/hello.txt\"
Attempting to fix the path: \"/$this->userId/files/hello.txt\"
Decrement the encrypted version to 4
Decrement the encrypted version to 3
The file \"/$this->userId/files/hello.txt\" is: OK
Fixed the file: \"/$this->userId/files/hello.txt\" with version 3", $output);
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/world.txt\"
Attempting to fix the path: \"/$this->userId/files/world.txt\"
Decrement the encrypted version to 5
Decrement the encrypted version to 4
The file \"/$this->userId/files/world.txt\" is: OK
Fixed the file: \"/$this->userId/files/world.txt\" with version 4", $output);
}
public function testVersionIsRestoredToOriginalIfNoFixIsFound() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$view = new View("/" . $this->userId . "/files");
$view->touch('bar.txt');
for ($i = 0; $i < 40; $i++) {
$view->file_put_contents('bar.txt', 'a test string for hello ' . $i);
}
$fileInfo = $view->getFileInfo('bar.txt');
$storage = $fileInfo->getStorage();
$cache = $storage->getCache();
$fileCache = $cache->get($fileInfo->getId());
$cacheInfo = ['encryptedVersion' => 15, 'encrypted' => 15];
$cache->put($fileCache->getPath(), $cacheInfo);
$this->commandTester->execute([
'user' => $this->userId
]);
$cacheInfo = $cache->get($fileInfo->getId());
$encryptedVersion = $cacheInfo["encryptedVersion"];
$this->assertEquals(15, $encryptedVersion);
}
/**
* Test commands with a file path
*/
public function testExecuteWithFilePathOption() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$view = new View("/" . $this->userId . "/files");
$view->touch('hello.txt');
$view->touch('world.txt');
$this->commandTester->execute([
'user' => $this->userId,
'--path' => "/hello.txt"
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/hello.txt\"
The file \"/$this->userId/files/hello.txt\" is: OK", $output);
$this->assertStringNotContainsString('world.txt', $output);
}
/**
* Test commands with a directory path
*/
public function testExecuteWithDirectoryPathOption() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$view = new View("/" . $this->userId . "/files");
$view->mkdir('sub');
$view->touch('sub/hello.txt');
$view->touch('world.txt');
$this->commandTester->execute([
'user' => $this->userId,
'--path' => "/sub"
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/sub/hello.txt\"
The file \"/$this->userId/files/sub/hello.txt\" is: OK", $output);
$this->assertStringNotContainsString('world.txt', $output);
}
/**
* Test commands with a directory path
*/
public function testExecuteWithNoUser() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$this->commandTester->execute([
'user' => null,
'--path' => "/"
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('does not exist', $output);
}
/**
* Test commands with a directory path
*/
public function testExecuteWithNonExistentPath() {
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(true);
$this->commandTester->execute([
'user' => $this->userId,
'--path' => '/non-exist'
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Please provide a valid path.', $output);
}
/**
* Test commands without master key
*/
public function testExecuteWithNoMasterKey() {
\OC::$server->getConfig()->setAppValue('encryption', 'useMasterKey', '0');
$this->util->expects($this->once())->method('isMasterKeyEnabled')
->willReturn(false);
$this->commandTester->execute([
'user' => $this->userId,
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('only works with master key', $output);
}
}
+1
View File
@@ -54,6 +54,7 @@ return [
['root' => '/cloud', 'name' => 'Users#getEditableFields', 'url' => '/user/fields', 'verb' => 'GET'],
['root' => '/cloud', 'name' => 'Users#getEditableFieldsForUser', 'url' => '/user/fields/{userId}', 'verb' => 'GET'],
['root' => '/cloud', 'name' => 'Users#editUser', 'url' => '/users/{userId}', 'verb' => 'PUT'],
['root' => '/cloud', 'name' => 'Users#editUserMultiValue', 'url' => '/users/{userId}/{collectionName}', 'verb' => 'PUT', 'requirements' => ['collectionName' => '^(?!enable$|disable$)[a-zA-Z0-9_]*$']],
['root' => '/cloud', 'name' => 'Users#wipeUserDevices', 'url' => '/users/{userId}/wipe', 'verb' => 'POST'],
['root' => '/cloud', 'name' => 'Users#deleteUser', 'url' => '/users/{userId}', 'verb' => 'DELETE'],
['root' => '/cloud', 'name' => 'Users#enableUser', 'url' => '/users/{userId}/enable', 'verb' => 'PUT'],
@@ -150,6 +150,20 @@ abstract class AUserData extends OCSController {
if ($includeScopes) {
$data[IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope();
}
$additionalEmails = $additionalEmailScopes = [];
$emailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
foreach ($emailCollection->getProperties() as $property) {
$additionalEmails[] = $property->getValue();
if ($includeScopes) {
$additionalEmailScopes[] = $property->getScope();
}
}
$data[IAccountManager::COLLECTION_EMAIL] = $additionalEmails;
if ($includeScopes) {
$data[IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX] = $additionalEmailScopes;
}
$data[IAccountManager::PROPERTY_DISPLAYNAME] = $targetUserObject->getDisplayName();
if ($includeScopes) {
$data[IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope();
@@ -52,7 +52,8 @@ use OC\KnownUser\KnownUserService;
use OC\User\Backend;
use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\Accounts\IAccountProperty;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSException;
@@ -75,8 +76,6 @@ use Psr\Log\LoggerInterface;
class UsersController extends AUserData {
/** @var IAppManager */
private $appManager;
/** @var IURLGenerator */
protected $urlGenerator;
/** @var LoggerInterface */
@@ -98,7 +97,6 @@ class UsersController extends AUserData {
IRequest $request,
IUserManager $userManager,
IConfig $config,
IAppManager $appManager,
IGroupManager $groupManager,
IUserSession $userSession,
IAccountManager $accountManager,
@@ -119,7 +117,6 @@ class UsersController extends AUserData {
$accountManager,
$l10nFactory);
$this->appManager = $appManager;
$this->urlGenerator = $urlGenerator;
$this->logger = $logger;
$this->l10nFactory = $l10nFactory;
@@ -592,6 +589,7 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
}
$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
$permittedFields[] = IAccountManager::PROPERTY_PHONE;
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
@@ -600,6 +598,92 @@ class UsersController extends AUserData {
return new DataResponse($permittedFields);
}
/**
* @NoAdminRequired
* @NoSubAdminRequired
* @PasswordConfirmationRequired
*
* @throws OCSException
*/
public function editUserMultiValue(
string $userId,
string $collectionName,
string $key,
string $value
): DataResponse {
$currentLoggedInUser = $this->userSession->getUser();
if ($currentLoggedInUser === null) {
throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
}
$targetUser = $this->userManager->get($userId);
if ($targetUser === null) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
}
$permittedFields = [];
if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
// Editing self (display, email)
$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
$permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
} else {
// Check if admin / subadmin
$subAdminManager = $this->groupManager->getSubAdmin();
if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())
|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
// They have permissions over the user
$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
} else {
// No rights
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
}
}
// Check if permitted to edit this field
if (!in_array($collectionName, $permittedFields)) {
throw new OCSException('', 103);
}
switch ($collectionName) {
case IAccountManager::COLLECTION_EMAIL:
$userAccount = $this->accountManager->getAccount($targetUser);
$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
$mailCollection->removePropertyByValue($key);
if ($value !== '') {
$mailCollection->addPropertyWithDefaults($value);
}
$this->accountManager->updateAccount($userAccount);
break;
case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX:
$userAccount = $this->accountManager->getAccount($targetUser);
$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
$targetProperty = null;
foreach ($mailCollection->getProperties() as $property) {
if ($property->getValue() === $key) {
$targetProperty = $property;
break;
}
}
if ($targetProperty instanceof IAccountProperty) {
try {
$targetProperty->setScope($value);
$this->accountManager->updateAccount($userAccount);
} catch (\InvalidArgumentException $e) {
throw new OCSException('', 102);
}
} else {
throw new OCSException('', 102);
}
break;
default:
throw new OCSException('', 103);
}
return new DataResponse();
}
/**
* @NoAdminRequired
* @NoSubAdminRequired
@@ -636,6 +720,8 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
$permittedFields[] = 'password';
if ($this->config->getSystemValue('force_language', false) === false ||
$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
@@ -674,6 +760,7 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
}
$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
$permittedFields[] = 'password';
$permittedFields[] = 'language';
$permittedFields[] = 'locale';
@@ -746,24 +833,42 @@ class UsersController extends AUserData {
throw new OCSException('', 102);
}
break;
case IAccountManager::COLLECTION_EMAIL:
if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getEMailAddress()) {
$userAccount = $this->accountManager->getAccount($targetUser);
$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
foreach ($mailCollection->getProperties() as $property) {
if ($property->getValue() === $value) {
break;
}
}
$mailCollection->addPropertyWithDefaults($value);
$this->accountManager->updateAccount($userAccount);
} else {
throw new OCSException('', 102);
}
break;
case IAccountManager::PROPERTY_PHONE:
case IAccountManager::PROPERTY_ADDRESS:
case IAccountManager::PROPERTY_WEBSITE:
case IAccountManager::PROPERTY_TWITTER:
$userAccount = $this->accountManager->getAccount($targetUser);
$userProperty = $userAccount->getProperty($key);
if ($userProperty->getValue() !== $value) {
try {
$userProperty->setValue($value);
$this->accountManager->updateAccount($userAccount);
if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
$this->knownUserService->deleteByContactUserId($targetUser->getUID());
try {
$userProperty = $userAccount->getProperty($key);
if ($userProperty->getValue() !== $value) {
try {
$userProperty->setValue($value);
if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
$this->knownUserService->deleteByContactUserId($targetUser->getUID());
}
} catch (\InvalidArgumentException $e) {
throw new OCSException('Invalid ' . $e->getMessage(), 102);
}
} catch (\InvalidArgumentException $e) {
throw new OCSException('Invalid ' . $e->getMessage(), 102);
}
} catch (PropertyDoesNotExistException $e) {
$userAccount->setProperty($key, $value, IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED);
}
$this->accountManager->updateAccount($userAccount);
break;
case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX:
@@ -50,7 +50,6 @@ use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\DataResponse;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
@@ -77,8 +76,6 @@ class UsersControllerTest extends TestCase {
protected $userManager;
/** @var IConfig|MockObject */
protected $config;
/** @var IAppManager|MockObject */
protected $appManager;
/** @var Manager|MockObject */
protected $groupManager;
/** @var IUserSession|MockObject */
@@ -111,7 +108,6 @@ class UsersControllerTest extends TestCase {
$this->userManager = $this->createMock(IUserManager::class);
$this->config = $this->createMock(IConfig::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->groupManager = $this->createMock(Manager::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -131,7 +127,6 @@ class UsersControllerTest extends TestCase {
$this->request,
$this->userManager,
$this->config,
$this->appManager,
$this->groupManager,
$this->userSession,
$this->accountManager,
@@ -395,7 +390,6 @@ class UsersControllerTest extends TestCase {
$this->request,
$this->userManager,
$this->config,
$this->appManager,
$this->groupManager,
$this->userSession,
$this->accountManager,
@@ -1071,7 +1065,8 @@ class UsersControllerTest extends TestCase {
'backendCapabilities' => [
'setDisplayName' => true,
'setPassword' => true,
]
],
'additional_mail' => [],
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@@ -1198,7 +1193,8 @@ class UsersControllerTest extends TestCase {
'backendCapabilities' => [
'setDisplayName' => true,
'setPassword' => true,
]
],
'additional_mail' => [],
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@@ -1363,7 +1359,8 @@ class UsersControllerTest extends TestCase {
'backendCapabilities' => [
'setDisplayName' => false,
'setPassword' => false,
]
],
'additional_mail' => [],
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@@ -3437,7 +3434,6 @@ class UsersControllerTest extends TestCase {
$this->request,
$this->userManager,
$this->config,
$this->appManager,
$this->groupManager,
$this->userSession,
$this->accountManager,
@@ -3510,7 +3506,6 @@ class UsersControllerTest extends TestCase {
$this->request,
$this->userManager,
$this->config,
$this->appManager,
$this->groupManager,
$this->userSession,
$this->accountManager,
@@ -3848,6 +3843,7 @@ class UsersControllerTest extends TestCase {
public function dataGetEditableFields() {
return [
[false, ISetDisplayNameBackend::class, [
IAccountManager::COLLECTION_EMAIL,
IAccountManager::PROPERTY_PHONE,
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
@@ -3856,6 +3852,7 @@ class UsersControllerTest extends TestCase {
[true, ISetDisplayNameBackend::class, [
IAccountManager::PROPERTY_DISPLAYNAME,
IAccountManager::PROPERTY_EMAIL,
IAccountManager::COLLECTION_EMAIL,
IAccountManager::PROPERTY_PHONE,
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
@@ -3863,6 +3860,7 @@ class UsersControllerTest extends TestCase {
]],
[true, UserInterface::class, [
IAccountManager::PROPERTY_EMAIL,
IAccountManager::COLLECTION_EMAIL,
IAccountManager::PROPERTY_PHONE,
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
@@ -127,6 +127,11 @@ class UpdateConfig extends Command {
$key = $key . 'Mime';
}
if ($key === 'color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$output->writeln('<error>The given color is invalid: ' . $value . '</error>');
return 1;
}
$this->themingDefaults->set($key, $value);
$output->writeln('<info>Updated ' . $key . ' to ' . $value . '</info>');
+5 -1
View File
@@ -220,7 +220,11 @@ class ThemingDefaults extends \OC_Defaults {
* @return string
*/
public function getColorPrimary() {
return $this->config->getAppValue('theming', 'color', $this->color);
$color = $this->config->getAppValue('theming', 'color', $this->color);
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
$color = '#0082c9';
}
return $color;
}
/**
+3
View File
@@ -37,6 +37,9 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc
<install>
<step>OCA\User_LDAP\Migration\SetDefaultProvider</step>
</install>
<uninstall>
<step>OCA\User_LDAP\Migration\UnsetDefaultProvider</step>
</uninstall>
<post-migration>
<step>OCA\User_LDAP\Migration\UUIDFixInsert</step>
<step>OCA\User_LDAP\Migration\RemoveRefreshTime</step>
@@ -59,6 +59,7 @@ return array(
'OCA\\User_LDAP\\Migration\\UUIDFixGroup' => $baseDir . '/../lib/Migration/UUIDFixGroup.php',
'OCA\\User_LDAP\\Migration\\UUIDFixInsert' => $baseDir . '/../lib/Migration/UUIDFixInsert.php',
'OCA\\User_LDAP\\Migration\\UUIDFixUser' => $baseDir . '/../lib/Migration/UUIDFixUser.php',
'OCA\\User_LDAP\\Migration\\UnsetDefaultProvider' => $baseDir . '/../lib/Migration/UnsetDefaultProvider.php',
'OCA\\User_LDAP\\Migration\\Version1010Date20200630192842' => $baseDir . '/../lib/Migration/Version1010Date20200630192842.php',
'OCA\\User_LDAP\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\User_LDAP\\PagedResults\\IAdapter' => $baseDir . '/../lib/PagedResults/IAdapter.php',
@@ -74,6 +74,7 @@ class ComposerStaticInitUser_LDAP
'OCA\\User_LDAP\\Migration\\UUIDFixGroup' => __DIR__ . '/..' . '/../lib/Migration/UUIDFixGroup.php',
'OCA\\User_LDAP\\Migration\\UUIDFixInsert' => __DIR__ . '/..' . '/../lib/Migration/UUIDFixInsert.php',
'OCA\\User_LDAP\\Migration\\UUIDFixUser' => __DIR__ . '/..' . '/../lib/Migration/UUIDFixUser.php',
'OCA\\User_LDAP\\Migration\\UnsetDefaultProvider' => __DIR__ . '/..' . '/../lib/Migration/UnsetDefaultProvider.php',
'OCA\\User_LDAP\\Migration\\Version1010Date20200630192842' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192842.php',
'OCA\\User_LDAP\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\User_LDAP\\PagedResults\\IAdapter' => __DIR__ . '/..' . '/../lib/PagedResults/IAdapter.php',
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_LDAP\Migration;
use OCA\User_LDAP\LDAPProviderFactory;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class UnsetDefaultProvider implements IRepairStep {
/** @var IConfig */
private $config;
public function __construct(IConfig $config) {
$this->config = $config;
}
public function getName(): string {
return 'Unset default LDAP provider';
}
public function run(IOutput $output): void {
$current = $this->config->getSystemValue('ldapProviderFactory', null);
if ($current === LDAPProviderFactory::class) {
$this->config->deleteSystemValue('ldapProviderFactory');
}
}
}
+3 -7
View File
@@ -146,7 +146,8 @@ class OfflineUser {
*/
public function getDN() {
if ($this->dn === null) {
$this->fetchDetails();
$dn = $this->mapping->getDNByName($this->ocName);
$this->dn = ($dn !== false) ? $dn : '';
}
return $this->dn;
}
@@ -212,7 +213,7 @@ class OfflineUser {
*/
public function getHasActiveShares() {
if ($this->hasActiveShares === null) {
$this->fetchDetails();
$this->determineShares();
}
return $this->hasActiveShares;
}
@@ -232,11 +233,6 @@ class OfflineUser {
foreach ($properties as $property => $app) {
$this->$property = $this->config->getUserValue($this->ocName, $app, $property, '');
}
$dn = $this->mapping->getDNByName($this->ocName);
$this->dn = ($dn !== false) ? $dn : '';
$this->determineShares();
}
/**
@@ -168,14 +168,19 @@ trait Provisioning {
$response = $client->get($fullUrl, $options);
foreach ($settings->getRows() as $setting) {
$value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
if (isset($value[0])) {
if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
$expectedValues = explode(';', $setting[1]);
foreach ($expectedValues as $expected) {
Assert::assertTrue(in_array($expected, $value['element'], true));
}
} elseif (isset($value[0])) {
Assert::assertEquals($setting[1], $value[0], "", 0.0, 10, true);
} else {
Assert::assertEquals('', $setting[1]);
}
}
}
/**
* @Then /^group "([^"]*)" has$/
*
@@ -194,7 +199,7 @@ trait Provisioning {
$options['headers'] = [
'OCS-APIREQUEST' => 'true',
];
$response = $client->get($fullUrl, $options);
$groupDetails = simplexml_load_string($response->getBody())->data[0]->groups[0]->element;
foreach ($settings->getRows() as $setting) {
@@ -206,7 +211,7 @@ trait Provisioning {
}
}
}
/**
* @Then /^user "([^"]*)" has editable fields$/
@@ -967,4 +972,38 @@ trait Provisioning {
}
$this->usingServer($previousServer);
}
/**
* @Then /^user "([^"]*)" has not$/
*/
public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
$client = new Client();
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
} else {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
$options['headers'] = [
'OCS-APIREQUEST' => 'true',
];
$response = $client->get($fullUrl, $options);
foreach ($settings->getRows() as $setting) {
$value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
if (isset($value[0])) {
if (in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
$expectedValues = explode(';', $setting[1]);
foreach ($expectedValues as $expected) {
Assert::assertFalse(in_array($expected, $value, true));
}
} else {
Assert::assertNotEquals($setting[1], $value[0], "", 0.0, 10, true);
}
} else {
Assert::assertNotEquals('', $setting[1]);
}
}
}
}
@@ -62,6 +62,7 @@ Feature: provisioning
Then user "brand-new-user" has editable fields
| displayname |
| email |
| additional_mail |
| phone |
| address |
| website |
@@ -70,6 +71,7 @@ Feature: provisioning
Then user "brand-new-user" has editable fields
| displayname |
| email |
| additional_mail |
| phone |
| address |
| website |
@@ -77,6 +79,7 @@ Feature: provisioning
Then user "self" has editable fields
| displayname |
| email |
| additional_mail |
| phone |
| address |
| website |
@@ -100,6 +103,16 @@ Feature: provisioning
| value | no-reply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | no.reply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | noreply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | phone |
| value | +49 711 / 25 24 28-90 |
@@ -124,6 +137,7 @@ Feature: provisioning
| id | brand-new-user |
| displayname | Brand New User |
| email | no-reply@nextcloud.com |
| additional_mail | no.reply@nextcloud.com;noreply@nextcloud.com |
| phone | +4971125242890 |
| address | Foo Bar Town |
| website | https://nextcloud.com |
@@ -177,6 +191,33 @@ Feature: provisioning
| displaynameScope | v2-federated |
| avatarScope | v2-local |
Scenario: Edit a user account multivalue property scopes
Given user "brand-new-user" exists
And As an "brand-new-user"
When sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | no.reply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | noreply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
| key | no.reply@nextcloud.com |
| value | v2-federated |
Then the OCS status code should be "100"
And the HTTP status code should be "200"
When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
| key | noreply@nextcloud.com |
| value | v2-published |
Then the OCS status code should be "100"
And the HTTP status code should be "200"
Then user "brand-new-user" has
| id | brand-new-user |
| additional_mailScope | v2-federated;v2-published |
Scenario: Edit a user account properties scopes with invalid or unsupported value
Given user "brand-new-user" exists
And As an "brand-new-user"
@@ -196,6 +237,43 @@ Feature: provisioning
Then the OCS status code should be "102"
And the HTTP status code should be "200"
Scenario: Edit a user account multi-value property scopes with invalid or unsupported value
Given user "brand-new-user" exists
And As an "brand-new-user"
When sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | no.reply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
| key | no.reply@nextcloud.com |
| value | invalid |
Then the OCS status code should be "102"
And the HTTP status code should be "200"
Scenario: Delete a user account multi-value property value
Given user "brand-new-user" exists
And As an "brand-new-user"
When sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | no.reply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | additional_mail |
| value | noreply@nextcloud.com |
And the OCS status code should be "100"
And the HTTP status code should be "200"
When sending "PUT" to "/cloud/users/brand-new-user/additional_mail" with
| key | no.reply@nextcloud.com |
| value | |
And the OCS status code should be "100"
And the HTTP status code should be "200"
Then user "brand-new-user" has
| additional_mail | noreply@nextcloud.com |
Then user "brand-new-user" has not
| additional_mail | no.reply@nextcloud.com |
Scenario: An admin cannot edit user account property scopes
Given As an "admin"
And user "brand-new-user" exists
@@ -233,7 +311,7 @@ Feature: provisioning
And group "new-group" exists
And group "new-group" has
| displayname | new-group |
Scenario: Create a group with custom display name
Given As an "admin"
And group "new-group" does not exist
+4 -4
View File
@@ -1,14 +1,14 @@
#!/bin/bash
# Update Nextcloud apps from latest git master
# Update Nextcloud apps from latest git stable22
# For local development environment
# Use from Nextcloud server folder with `./build/update-apps.sh`
#
# It automatically:
# - goes through all apps which are not shipped via server
# - shows the app name in bold and uses whitespace for separation
# - changes to master and pulls quietly
# - changes to stable22 and pulls quietly
# - shows the 3 most recent commits for context
# - removes branches merged into master
# - removes branches merged into stable22
# - … could even do the build steps if they are consistent for the apps (like `make`)
find apps* -maxdepth 2 -name .git -exec sh -c 'cd {}/../ && printf "\n\033[1m${PWD##*/}\033[0m\n" && git checkout master && git pull --quiet -p && git --no-pager log -3 --pretty=format:"%h %Cblue%ar%x09%an %Creset%s" && printf "\n" && git branch --merged master | grep -v "master$" | xargs git branch -d && cd ..' \;
find apps* -maxdepth 2 -name .git -exec sh -c 'cd {}/../ && printf "\n\033[1m${PWD##*/}\033[0m\n" && git checkout stable22 && git pull --quiet -p && git --no-pager log -3 --pretty=format:"%h %Cblue%ar%x09%an %Creset%s" && printf "\n" && git branch --merged stable22 | grep -v "stable22$" | xargs git branch -d && cd ..' \;
+3 -3
View File
@@ -1,15 +1,15 @@
#!/bin/bash
# Update Nextcloud server and apps from latest git master
# Update Nextcloud server and apps from latest git stable22
# For local development environment
# Use from Nextcloud server folder with `./build/update.sh`
# Update server
printf "\n\033[1m${PWD##*/}\033[0m\n"
git checkout master
git checkout stable22
git pull --quiet -p
git --no-pager log -3 --pretty=format:"%h %Cblue%ar%x09%an %Creset%s"
printf "\n"
git branch --merged master | grep -v "master$" | xargs git branch -d
git branch --merged stable22 | grep -v "stable22$" | xargs git branch -d
git submodule update --init
# Update apps
+3 -1
View File
@@ -729,8 +729,10 @@ $min-content-width: $breakpoint-mobile - $navigation-width - $list-min-width;
border: 0;
border-radius: 0;
text-align: left;
padding-left: 42px;
padding-left: 44px;
font-weight: normal;
font-size: 100%;
opacity: 0.8;
/* like app-navigation a */
color: var(--color-main-text);
+1 -1
View File
@@ -990,7 +990,7 @@ span.ui-icon {
#contactsmenu {
.menutoggle {
background-size: 16px 16px;
background-size: 20px 20px;
padding: 14px;
cursor: pointer;
+1 -1
View File
@@ -37,7 +37,7 @@ $urlGenerator = $_['urlGenerator'];
</p>
<span class="warning">
<h3><?php p('Security warning') ?></h3>
<h3><?php p($l->t('Security warning')) ?></h3>
<p>
<?php p($l->t('If you are not trying to set up a new device or app, someone is trying to trick you into granting them access to your data. In this case do not proceed and instead contact your system administrator.')) ?>
</p>
+1 -1
View File
@@ -36,7 +36,7 @@ $urlGenerator = $_['urlGenerator'];
</p>
<span class="warning">
<h3><?php p('Security warning') ?></h3>
<h3><?php p($l->t('Security warning')) ?></h3>
<p>
<?php p($l->t('If you are not trying to set up a new device or app, someone is trying to trick you into granting them access to your data. In this case do not proceed and instead contact your system administrator.')) ?>
</p>
+4 -2
View File
@@ -68,6 +68,7 @@ use OCP\Share;
use OC\Encryption\HookManager;
use OC\Files\Filesystem;
use OC\Share20\Hooks;
use OCP\User\Events\UserChangedEvent;
require_once 'public/Constants.php';
@@ -843,8 +844,9 @@ class OC {
}
private static function registerAccountHooks() {
$hookHandler = \OC::$server->get(\OC\Accounts\Hooks::class);
\OCP\Util::connectHook('OC_User', 'changeUser', $hookHandler, 'changeUserHook');
/** @var IEventDispatcher $dispatcher */
$dispatcher = \OC::$server->get(IEventDispatcher::class);
$dispatcher->addServiceListener(UserChangedEvent::class, \OC\Accounts\Hooks::class);
}
private static function registerAppRestrictionsHooks() {
+10 -6
View File
@@ -33,6 +33,7 @@ use OCP\Accounts\IAccountProperty;
use OCP\Accounts\IAccountPropertyCollection;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\IUser;
use RuntimeException;
class Account implements IAccount {
use TAccountsHelper;
@@ -116,13 +117,16 @@ class Account implements IAccount {
return $this;
}
public function getPropertyCollection(string $propertyCollection): IAccountPropertyCollection {
if (!array_key_exists($propertyCollection, $this->properties)) {
throw new PropertyDoesNotExistException($propertyCollection);
public function getPropertyCollection(string $propertyCollectionName): IAccountPropertyCollection {
if (!$this->isCollection($propertyCollectionName)) {
throw new PropertyDoesNotExistException($propertyCollectionName);
}
if (!$this->properties[$propertyCollection] instanceof IAccountPropertyCollection) {
throw new \RuntimeException('Requested collection is not an IAccountPropertyCollection');
if (!array_key_exists($propertyCollectionName, $this->properties)) {
$this->properties[$propertyCollectionName] = new AccountPropertyCollection($propertyCollectionName);
}
return $this->properties[$propertyCollection];
if (!$this->properties[$propertyCollectionName] instanceof IAccountPropertyCollection) {
throw new RuntimeException('Requested collection is not an IAccountPropertyCollection');
}
return $this->properties[$propertyCollectionName];
}
}
+292 -249
View File
@@ -32,6 +32,7 @@
*/
namespace OC\Accounts;
use InvalidArgumentException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
@@ -39,6 +40,9 @@ use libphonenumber\PhoneNumberUtil;
use OCA\Settings\BackgroundJobs\VerifyUserData;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\Accounts\IAccountPropertyCollection;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\BackgroundJob\IJobList;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
@@ -48,7 +52,9 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use function array_flip;
use function iterator_to_array;
use function json_decode;
use function json_encode;
use function json_last_error;
/**
@@ -98,7 +104,7 @@ class AccountManager implements IAccountManager {
/**
* @param string $input
* @return string Provided phone number in E.164 format when it was a valid number
* @throws \InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
* @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
*/
protected function parsePhoneNumber(string $input): string {
$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
@@ -106,7 +112,7 @@ class AccountManager implements IAccountManager {
if ($defaultRegion === '') {
// When no default region is set, only +49… numbers are valid
if (strpos($input, '+') !== 0) {
throw new \InvalidArgumentException(self::PROPERTY_PHONE);
throw new InvalidArgumentException(self::PROPERTY_PHONE);
}
$defaultRegion = 'EN';
@@ -121,140 +127,107 @@ class AccountManager implements IAccountManager {
} catch (NumberParseException $e) {
}
throw new \InvalidArgumentException(self::PROPERTY_PHONE);
throw new InvalidArgumentException(self::PROPERTY_PHONE);
}
/**
*
* @param string $input
* @return string
* @throws \InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
* @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
*/
protected function parseWebsite(string $input): string {
$parts = parse_url($input);
if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
}
if (!isset($parts['host']) || $parts['host'] === '') {
throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
}
return $input;
}
protected function sanitizeLength(array &$propertyData, bool $throwOnData = false): void {
if (isset($propertyData['value']) && strlen($propertyData['value']) > 2048) {
if ($throwOnData) {
throw new \InvalidArgumentException();
} else {
$propertyData['value'] = '';
}
}
}
protected function testValueLengths(array &$data, bool $throwOnData = false): void {
try {
foreach ($data as $propertyName => &$propertyData) {
if ($this->isCollection($propertyName)) {
$this->testValueLengths($propertyData, $throwOnData);
} else {
$this->sanitizeLength($propertyData, $throwOnData);
}
}
} catch (\InvalidArgumentException $e) {
throw new \InvalidArgumentException($propertyName);
}
}
protected function testPropertyScopes(array &$data, array $allowedScopes, bool $throwOnData = false, string $parentPropertyName = null): void {
foreach ($data as $propertyNameOrIndex => &$propertyData) {
if ($this->isCollection($propertyNameOrIndex)) {
$this->testPropertyScopes($propertyData, $allowedScopes, $throwOnData);
} elseif (isset($propertyData['scope'])) {
$effectivePropertyName = $parentPropertyName ?? $propertyNameOrIndex;
if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) {
throw new \InvalidArgumentException('scope');
}
if (
$propertyData['scope'] === self::SCOPE_PRIVATE
&& ($effectivePropertyName === self::PROPERTY_DISPLAYNAME || $effectivePropertyName === self::PROPERTY_EMAIL)
) {
if ($throwOnData) {
// v2-private is not available for these fields
throw new \InvalidArgumentException('scope');
} else {
// default to local
$data[$propertyNameOrIndex]['scope'] = self::SCOPE_LOCAL;
}
} else {
// migrate scope values to the new format
// invalid scopes are mapped to a default value
$data[$propertyNameOrIndex]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']);
}
}
}
}
/**
* update user record
*
* @param IUser $user
* @param array $data
* @param bool $throwOnData Set to true if you can inform the user about invalid data
* @return array The potentially modified data (e.g. phone numbers are converted to E.164 format)
* @throws \InvalidArgumentException Message is the property that was invalid
* @param IAccountProperty[] $properties
*/
public function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
$userData = $this->getUser($user);
protected function testValueLengths(array $properties, bool $throwOnData = false): void {
foreach ($properties as $property) {
if (strlen($property->getValue()) > 2048) {
if ($throwOnData) {
throw new InvalidArgumentException();
} else {
$property->setValue('');
}
}
}
}
protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
throw new InvalidArgumentException('scope');
}
if (
$property->getScope() === self::SCOPE_PRIVATE
&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
) {
if ($throwOnData) {
// v2-private is not available for these fields
throw new InvalidArgumentException('scope');
} else {
// default to local
$property->setScope(self::SCOPE_LOCAL);
}
} else {
// migrate scope values to the new format
// invalid scopes are mapped to a default value
$property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
}
}
protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
if ($property->getName() !== self::PROPERTY_PHONE) {
if ($throwOnData) {
throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
}
return;
}
if ($property->getValue() === '') {
return;
}
try {
$property->setValue($this->parsePhoneNumber($property->getValue()));
} catch (InvalidArgumentException $e) {
if ($throwOnData) {
throw $e;
}
$property->setValue('');
}
}
protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
if ($property->getName() !== self::PROPERTY_WEBSITE) {
if ($throwOnData) {
throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
}
}
try {
$property->setValue($this->parseWebsite($property->getValue()));
} catch (InvalidArgumentException $e) {
if ($throwOnData) {
throw $e;
}
$property->setValue('');
}
}
protected function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
$oldUserData = $this->getUser($user, false);
$updated = true;
if (isset($data[self::PROPERTY_PHONE]) && $data[self::PROPERTY_PHONE]['value'] !== '') {
// Sanitize null value.
$data[self::PROPERTY_PHONE]['value'] = $data[self::PROPERTY_PHONE]['value'] ?? '';
try {
$data[self::PROPERTY_PHONE]['value'] = $this->parsePhoneNumber($data[self::PROPERTY_PHONE]['value']);
} catch (\InvalidArgumentException $e) {
if ($throwOnData) {
throw $e;
}
$data[self::PROPERTY_PHONE]['value'] = '';
}
}
$this->testValueLengths($data);
if (isset($data[self::PROPERTY_WEBSITE]) && $data[self::PROPERTY_WEBSITE]['value'] !== '') {
try {
$data[self::PROPERTY_WEBSITE]['value'] = $this->parseWebsite($data[self::PROPERTY_WEBSITE]['value']);
} catch (\InvalidArgumentException $e) {
if ($throwOnData) {
throw $e;
}
$data[self::PROPERTY_WEBSITE]['value'] = '';
}
}
$allowedScopes = [
self::SCOPE_PRIVATE,
self::SCOPE_LOCAL,
self::SCOPE_FEDERATED,
self::SCOPE_PUBLISHED,
self::VISIBILITY_PRIVATE,
self::VISIBILITY_CONTACTS_ONLY,
self::VISIBILITY_PUBLIC,
];
$this->testPropertyScopes($data, $allowedScopes, $throwOnData);
if (empty($userData)) {
$this->insertNewUser($user, $data);
} elseif ($userData !== $data) {
$data = $this->checkEmailVerification($userData, $data, $user);
$data = $this->updateVerifyStatus($userData, $data);
if ($oldUserData !== $data) {
$this->updateExistingUser($user, $data);
} else {
// nothing needs to be done if new and old data set are the same
@@ -301,17 +274,15 @@ class AccountManager implements IAccountManager {
/**
* get stored data from a given user
*
* @deprecated use getAccount instead to make sure migrated properties work correctly
*/
public function getUser(IUser $user, bool $insertIfNotExists = true): array {
protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
$uid = $user->getUID();
$query = $this->connection->getQueryBuilder();
$query->select('data')
->from($this->table)
->where($query->expr()->eq('uid', $query->createParameter('uid')))
->setParameter('uid', $uid);
$result = $query->execute();
$result = $query->executeQuery();
$accountData = $result->fetchAll();
$result->closeCursor();
@@ -323,10 +294,8 @@ class AccountManager implements IAccountManager {
return $userData;
}
$userDataArray = json_decode($accountData[0]['data'], true);
$jsonError = json_last_error();
if ($userDataArray === null || $userDataArray === [] || $jsonError !== JSON_ERROR_NONE) {
$this->logger->critical("User data of $uid contained invalid JSON (error $jsonError), hence falling back to a default user record");
$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
if ($userDataArray === null || $userDataArray === []) {
return $this->buildDefaultUserRecord($user);
}
@@ -344,7 +313,7 @@ class AccountManager implements IAccountManager {
$matches = [];
foreach ($chunks as $chunk) {
$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
$result = $query->execute();
$result = $query->executeQuery();
while ($row = $result->fetch()) {
$matches[$row['uid']] = $row['value'];
@@ -369,100 +338,75 @@ class AccountManager implements IAccountManager {
/**
* check if we need to ask the server for email verification, if yes we create a cronjob
*
* @param $oldData
* @param $newData
* @param IUser $user
* @return array
*/
protected function checkEmailVerification($oldData, $newData, IUser $user): array {
if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) {
protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
try {
$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
} catch (PropertyDoesNotExistException $e) {
return;
}
$oldMail = isset($oldData[self::PROPERTY_EMAIL]) ? $oldData[self::PROPERTY_EMAIL]['value']['value'] : '';
if ($oldMail !== $property->getValue()) {
$this->jobList->add(VerifyUserData::class,
[
'verificationCode' => '',
'data' => $newData[self::PROPERTY_EMAIL]['value'],
'data' => $property->getValue(),
'type' => self::PROPERTY_EMAIL,
'uid' => $user->getUID(),
'uid' => $updatedAccount->getUser()->getUID(),
'try' => 0,
'lastRun' => time()
]
);
$newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS;
}
return $newData;
$property->setVerified(self::VERIFICATION_IN_PROGRESS);
}
}
/**
* make sure that all expected data are set
*
* @param array $userData
* @return array
*/
protected function addMissingDefaultValues(array $userData) {
foreach ($userData as $key => $value) {
if (!isset($userData[$key]['verified'])) {
$userData[$key]['verified'] = self::NOT_VERIFIED;
protected function addMissingDefaultValues(array $userData): array {
foreach ($userData as $i => $value) {
if (!isset($value['verified'])) {
$userData[$i]['verified'] = self::NOT_VERIFIED;
}
}
return $userData;
}
/**
* reset verification status if personal data changed
*
* @param array $oldData
* @param array $newData
* @return array
*/
protected function updateVerifyStatus(array $oldData, array $newData): array {
protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
static $propertiesVerifiableByLookupServer = [
self::PROPERTY_TWITTER,
self::PROPERTY_WEBSITE,
self::PROPERTY_EMAIL,
];
// which account was already verified successfully?
$twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED;
$websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED;
$emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED;
// keep old verification status if we don't have a new one
if (!isset($newData[self::PROPERTY_TWITTER]['verified'])) {
// keep old verification status if value didn't changed and an old value exists
$keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']);
$newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED;
foreach ($propertiesVerifiableByLookupServer as $propertyName) {
try {
$property = $updatedAccount->getProperty($propertyName);
} catch (PropertyDoesNotExistException $e) {
continue;
}
$wasVerified = isset($oldData[$propertyName])
&& isset($oldData[$propertyName]['verified'])
&& $oldData[$propertyName]['verified'] === self::VERIFIED;
if ((!isset($oldData[$propertyName])
|| !isset($oldData[$propertyName]['value'])
|| $property->getValue() !== $oldData[$propertyName]['value'])
&& ($property->getVerified() !== self::NOT_VERIFIED
|| $wasVerified)
) {
$property->setVerified(self::NOT_VERIFIED);
}
}
if (!isset($newData[self::PROPERTY_WEBSITE]['verified'])) {
// keep old verification status if value didn't changed and an old value exists
$keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']);
$newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED;
}
if (!isset($newData[self::PROPERTY_EMAIL]['verified'])) {
// keep old verification status if value didn't changed and an old value exists
$keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']);
$newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS;
}
// reset verification status if a value from a previously verified data was changed
if ($twitterVerified &&
$oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value']
) {
$newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED;
}
if ($websiteVerified &&
$oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value']
) {
$newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED;
}
if ($emailVerified &&
$oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']
) {
$newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED;
}
return $newData;
}
/**
* add new user to accounts table
*
@@ -471,7 +415,7 @@ class AccountManager implements IAccountManager {
*/
protected function insertNewUser(IUser $user, array $data): void {
$uid = $user->getUID();
$jsonEncodedData = json_encode($data);
$jsonEncodedData = $this->prepareJson($data);
$query = $this->connection->getQueryBuilder();
$query->insert($this->table)
->values(
@@ -480,12 +424,55 @@ class AccountManager implements IAccountManager {
'data' => $query->createNamedParameter($jsonEncodedData),
]
)
->execute();
->executeStatement();
$this->deleteUserData($user);
$this->writeUserData($user, $data);
}
protected function prepareJson(array $data): string {
$preparedData = [];
foreach ($data as $dataRow) {
$propertyName = $dataRow['name'];
unset($dataRow['name']);
if (!$this->isCollection($propertyName)) {
$preparedData[$propertyName] = $dataRow;
continue;
}
if (!isset($preparedData[$propertyName])) {
$preparedData[$propertyName] = [];
}
$preparedData[$propertyName][] = $dataRow;
}
return json_encode($preparedData);
}
protected function importFromJson(string $json, string $userId): ?array {
$result = [];
$jsonArray = json_decode($json, true);
$jsonError = json_last_error();
if ($jsonError !== JSON_ERROR_NONE) {
$this->logger->critical(
'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
[
'uid' => $userId,
'json_error' => $jsonError
]
);
return null;
}
foreach ($jsonArray as $propertyName => $row) {
if (!$this->isCollection($propertyName)) {
$result[] = array_merge($row, ['name' => $propertyName]);
continue;
}
foreach ($row as $singleRow) {
$result[] = array_merge($singleRow, ['name' => $propertyName]);
}
}
return $result;
}
/**
* update existing user in accounts table
*
@@ -494,12 +481,12 @@ class AccountManager implements IAccountManager {
*/
protected function updateExistingUser(IUser $user, array $data): void {
$uid = $user->getUID();
$jsonEncodedData = json_encode($data);
$jsonEncodedData = $this->prepareJson($data);
$query = $this->connection->getQueryBuilder();
$query->update($this->table)
->set('data', $query->createNamedParameter($jsonEncodedData))
->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
->execute();
->executeStatement();
$this->deleteUserData($user);
$this->writeUserData($user, $data);
@@ -518,19 +505,16 @@ class AccountManager implements IAccountManager {
$this->writeUserDataProperties($query, $data);
}
protected function writeUserDataProperties(IQueryBuilder $query, array $data, string $parentPropertyName = null): void {
foreach ($data as $propertyName => $property) {
if ($this->isCollection($propertyName)) {
$this->writeUserDataProperties($query, $property, $propertyName);
continue;
}
if (($parentPropertyName ?? $propertyName) === self::PROPERTY_AVATAR) {
protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
foreach ($data as $property) {
if ($property['name'] === self::PROPERTY_AVATAR) {
continue;
}
$query->setParameter('name', $parentPropertyName ?? $propertyName)
$query->setParameter('name', $property['name'])
->setParameter('value', $property['value'] ?? '');
$query->execute();
$query->executeStatement();
}
}
@@ -542,53 +526,80 @@ class AccountManager implements IAccountManager {
*/
protected function buildDefaultUserRecord(IUser $user) {
return [
self::PROPERTY_DISPLAYNAME =>
[
'value' => $user->getDisplayName(),
'scope' => self::SCOPE_FEDERATED,
'verified' => self::NOT_VERIFIED,
],
self::PROPERTY_ADDRESS =>
[
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
self::PROPERTY_WEBSITE =>
[
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
self::PROPERTY_EMAIL =>
[
'value' => $user->getEMailAddress(),
'scope' => self::SCOPE_FEDERATED,
'verified' => self::NOT_VERIFIED,
],
self::PROPERTY_AVATAR =>
[
'scope' => self::SCOPE_FEDERATED
],
self::PROPERTY_PHONE =>
[
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
self::PROPERTY_TWITTER =>
[
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_DISPLAYNAME,
'value' => $user->getDisplayName(),
'scope' => self::SCOPE_FEDERATED,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_ADDRESS,
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_WEBSITE,
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_EMAIL,
'value' => $user->getEMailAddress(),
'scope' => self::SCOPE_FEDERATED,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_AVATAR,
'scope' => self::SCOPE_FEDERATED
],
[
'name' => self::PROPERTY_PHONE,
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
[
'name' => self::PROPERTY_TWITTER,
'value' => '',
'scope' => self::SCOPE_LOCAL,
'verified' => self::NOT_VERIFIED,
],
];
}
private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
$collection = $account->getPropertyCollection($data['name']);
$p = new AccountProperty(
$data['name'],
$data['value'] ?? '',
$data['scope'] ?? self::SCOPE_LOCAL,
$data['verified'] ?? self::NOT_VERIFIED,
''
);
$collection->addProperty($p);
return $collection;
}
private function parseAccountData(IUser $user, $data): Account {
$account = new Account($user);
foreach ($data as $property => $accountData) {
$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
foreach ($data as $accountData) {
if ($this->isCollection($accountData['name'])) {
$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
} else {
$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
}
}
return $account;
}
@@ -598,10 +609,42 @@ class AccountManager implements IAccountManager {
}
public function updateAccount(IAccount $account): void {
$data = [];
$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
try {
$property = $account->getProperty(self::PROPERTY_PHONE);
$this->sanitizePhoneNumberValue($property);
} catch (PropertyDoesNotExistException $e) {
// valid case, nothing to do
}
foreach ($account->getProperties() as $property) {
$data[$property->getName()] = [
try {
$property = $account->getProperty(self::PROPERTY_WEBSITE);
$this->sanitizeWebsite($property);
} catch (PropertyDoesNotExistException $e) {
// valid case, nothing to do
}
static $allowedScopes = [
self::SCOPE_PRIVATE,
self::SCOPE_LOCAL,
self::SCOPE_FEDERATED,
self::SCOPE_PUBLISHED,
self::VISIBILITY_PRIVATE,
self::VISIBILITY_CONTACTS_ONLY,
self::VISIBILITY_PUBLIC,
];
foreach ($account->getAllProperties() as $property) {
$this->testPropertyScope($property, $allowedScopes, true);
}
$oldData = $this->getUser($account->getUser(), false);
$this->updateVerificationStatus($account, $oldData);
$this->checkEmailVerification($account, $oldData);
$data = [];
foreach ($account->getAllProperties() as $property) {
$data[] = [
'name' => $property->getName(),
'value' => $property->getValue(),
'scope' => $property->getScope(),
'verified' => $property->getVerified(),
@@ -27,6 +27,7 @@ declare(strict_types=1);
namespace OC\Accounts;
use InvalidArgumentException;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\Accounts\IAccountPropertyCollection;
@@ -63,6 +64,18 @@ class AccountPropertyCollection implements IAccountPropertyCollection {
return $this;
}
public function addPropertyWithDefaults(string $value): IAccountPropertyCollection {
$property = new AccountProperty(
$this->collectionName,
$value,
IAccountManager::SCOPE_LOCAL,
IAccountManager::NOT_VERIFIED,
''
);
$this->addProperty($property);
return $this;
}
public function removeProperty(IAccountProperty $property): IAccountPropertyCollection {
$ref = array_search($property, $this->properties, true);
if ($ref !== false) {
+28 -39
View File
@@ -25,66 +25,55 @@
namespace OC\Accounts;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUser;
use OCP\User\Events\UserChangedEvent;
use Psr\Log\LoggerInterface;
class Hooks {
class Hooks implements IEventListener {
/** @var AccountManager|null */
/** @var IAccountManager */
private $accountManager;
/** @var LoggerInterface */
private $logger;
public function __construct(LoggerInterface $logger) {
public function __construct(LoggerInterface $logger, IAccountManager $accountManager) {
$this->logger = $logger;
$this->accountManager = $accountManager;
}
/**
* update accounts table if email address or display name was changed from outside
*
* @param array $params
*/
public function changeUserHook($params) {
$accountManager = $this->getAccountManager();
public function changeUserHook(IUser $user, string $feature, $newValue): void {
$account = $this->accountManager->getAccount($user);
/** @var IUser $user */
$user = isset($params['user']) ? $params['user'] : null;
$feature = isset($params['feature']) ? $params['feature'] : null;
$newValue = isset($params['value']) ? $params['value'] : null;
if (is_null($user) || is_null($feature) || is_null($newValue)) {
$this->logger->warning('Missing expected parameters in change user hook');
try {
switch ($feature) {
case 'eMailAddress':
$property = $account->getProperty(IAccountManager::PROPERTY_EMAIL);
break;
case 'displayName':
$property = $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
break;
}
} catch (PropertyDoesNotExistException $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
return;
}
$accountData = $accountManager->getUser($user);
switch ($feature) {
case 'eMailAddress':
if ($accountData[IAccountManager::PROPERTY_EMAIL]['value'] !== $newValue) {
$accountData[IAccountManager::PROPERTY_EMAIL]['value'] = $newValue;
$accountManager->updateUser($user, $accountData);
}
break;
case 'displayName':
if ($accountData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] !== $newValue) {
$accountData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $newValue;
$accountManager->updateUser($user, $accountData);
}
break;
if (isset($property) && $property->getValue() !== (string)$newValue) {
$property->setValue($newValue);
$this->accountManager->updateAccount($account);
}
}
/**
* return instance of accountManager
*
* @return AccountManager
*/
protected function getAccountManager(): AccountManager {
if ($this->accountManager === null) {
$this->accountManager = \OC::$server->query(AccountManager::class);
public function handle(Event $event): void {
if (!$event instanceof UserChangedEvent) {
return;
}
return $this->accountManager;
$this->changeUserHook($event->getUser(), $event->getFeature(), $event->getValue());
}
}
@@ -305,12 +305,20 @@ class RegistrationContext {
*/
public function delegateCapabilityRegistrations(array $apps): void {
while (($registration = array_shift($this->capabilities)) !== null) {
$appId = $registration->getAppId();
if (!isset($apps[$appId])) {
// If we land here something really isn't right. But at least we caught the
// notice that is otherwise emitted for the undefined index
$this->logger->error("App $appId not loaded for the capability registration");
continue;
}
try {
$apps[$registration->getAppId()]
$apps[$appId]
->getContainer()
->registerCapability($registration->getService());
} catch (Throwable $e) {
$appId = $registration->getAppId();
$this->logger->error("Error during capability registration of $appId: " . $e->getMessage(), [
'exception' => $e,
]);
@@ -372,11 +380,20 @@ class RegistrationContext {
*/
public function delegateContainerRegistrations(array $apps): void {
while (($registration = array_shift($this->services)) !== null) {
$appId = $registration->getAppId();
if (!isset($apps[$appId])) {
// If we land here something really isn't right. But at least we caught the
// notice that is otherwise emitted for the undefined index
$this->logger->error("App $appId not loaded for the container service registration");
continue;
}
try {
/**
* Register the service and convert the callable into a \Closure if necessary
*/
$apps[$registration->getAppId()]
$apps[$appId]
->getContainer()
->registerService(
$registration->getName(),
@@ -384,7 +401,6 @@ class RegistrationContext {
$registration->isShared()
);
} catch (Throwable $e) {
$appId = $registration->getAppId();
$this->logger->error("Error during service registration of $appId: " . $e->getMessage(), [
'exception' => $e,
]);
@@ -392,15 +408,23 @@ class RegistrationContext {
}
while (($registration = array_shift($this->aliases)) !== null) {
$appId = $registration->getAppId();
if (!isset($apps[$appId])) {
// If we land here something really isn't right. But at least we caught the
// notice that is otherwise emitted for the undefined index
$this->logger->error("App $appId not loaded for the container alias registration");
continue;
}
try {
$apps[$registration->getAppId()]
$apps[$appId]
->getContainer()
->registerAlias(
$registration->getAlias(),
$registration->getTarget()
);
} catch (Throwable $e) {
$appId = $registration->getAppId();
$this->logger->error("Error during service alias registration of $appId: " . $e->getMessage(), [
'exception' => $e,
]);
@@ -408,16 +432,24 @@ class RegistrationContext {
}
while (($registration = array_shift($this->parameters)) !== null) {
$appId = $registration->getAppId();
if (!isset($apps[$appId])) {
// If we land here something really isn't right. But at least we caught the
// notice that is otherwise emitted for the undefined index
$this->logger->error("App $appId not loaded for the container parameter registration");
continue;
}
try {
$apps[$registration->getAppId()]
$apps[$appId]
->getContainer()
->registerParameter(
$registration->getName(),
$registration->getValue()
);
} catch (Throwable $e) {
$appId = $registration->getAppId();
$this->logger->error("Error during service alias registration of $appId: " . $e->getMessage(), [
$this->logger->error("Error during service parameter registration of $appId: " . $e->getMessage(), [
'exception' => $e,
]);
}
@@ -429,12 +461,20 @@ class RegistrationContext {
*/
public function delegateMiddlewareRegistrations(array $apps): void {
while (($middleware = array_shift($this->middlewares)) !== null) {
$appId = $middleware->getAppId();
if (!isset($apps[$appId])) {
// If we land here something really isn't right. But at least we caught the
// notice that is otherwise emitted for the undefined index
$this->logger->error("App $appId not loaded for the container middleware registration");
continue;
}
try {
$apps[$middleware->getAppId()]
$apps[$appId]
->getContainer()
->registerMiddleWare($middleware->getService());
} catch (Throwable $e) {
$appId = $middleware->getAppId();
$this->logger->error("Error during capability registration of $appId: " . $e->getMessage(), [
'exception' => $e,
]);
+8 -3
View File
@@ -39,6 +39,7 @@ use OC\KnownUser\KnownUserService;
use OC\User\Manager;
use OC\User\NoUserException;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
@@ -126,9 +127,13 @@ class AvatarManager implements IAvatarManager {
$folder = $this->appData->newFolder($userId);
}
$account = $this->accountManager->getAccount($user);
$avatarProperties = $account->getProperty(IAccountManager::PROPERTY_AVATAR);
$avatarScope = $avatarProperties->getScope();
try {
$account = $this->accountManager->getAccount($user);
$avatarProperties = $account->getProperty(IAccountManager::PROPERTY_AVATAR);
$avatarScope = $avatarProperties->getScope();
} catch (PropertyDoesNotExistException $e) {
$avatarScope = '';
}
if (
// v2-private scope hides the avatar from public access and from unknown users
+7 -1
View File
@@ -280,6 +280,11 @@ class Folder extends Node implements \OCP\Files\Folder {
}, $results);
}, array_values($resultsPerCache), array_keys($resultsPerCache)));
// don't include this folder in the results
$files = array_filter($files, function (FileInfo $file) {
return $file->getPath() !== $this->getPath();
});
// since results were returned per-cache, they are no longer fully sorted
$order = $query->getOrder();
if ($order) {
@@ -302,7 +307,8 @@ class Folder extends Node implements \OCP\Files\Folder {
private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
$cacheEntry['internalPath'] = $cacheEntry['path'];
$cacheEntry['path'] = $appendRoot . $cacheEntry->getPath();
return new \OC\Files\FileInfo($this->path . '/' . $cacheEntry['path'], $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
$subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : '';
return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
}
/**
@@ -465,6 +465,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common {
$stat['mimetype'] = $mimetype;
$stat['etag'] = $this->getETag($path);
$stat['checksum'] = '';
$exists = $this->getCache()->inCache($path);
$uploadPath = $exists ? $path : $path . '.part';
+12 -5
View File
@@ -112,15 +112,22 @@ class DnsPinMiddleware {
$targetIps = $this->dnsResolve($hostName, 0);
foreach ($targetIps as $ip) {
$this->localAddressChecker->ThrowIfLocalIp($ip);
$curlResolves = [];
foreach ($ports as $port) {
$curlEntry = $hostName . ':' . $port . ':' . $ip;
$options['curl'][CURLOPT_RESOLVE][] = $curlEntry;
foreach ($ports as $port) {
$curlResolves["$hostName:$port"] = [];
foreach ($targetIps as $ip) {
$this->localAddressChecker->ThrowIfLocalIp($ip);
$curlResolves["$hostName:$port"][] = $ip;
}
}
// Coalesce the per-host:port ips back into a comma separated list
foreach ($curlResolves as $hostport => $ips) {
$options['curl'][CURLOPT_RESOLVE][] = "$hostport:" . implode(',', $ips);
}
return $handler($request, $options);
};
};
+12 -14
View File
@@ -233,29 +233,32 @@ class Router implements IRouter {
* @throws \Exception
* @return array
*/
public function findMatchingRoute(string $url, bool $loadAll = false): array {
if (strpos($url, '/apps/') === 0) {
public function findMatchingRoute(string $url): array {
if (substr($url, 0, 6) === '/apps/') {
// empty string / 'apps' / $app / rest of the route
[, , $app,] = explode('/', $url, 4);
$app = \OC_App::cleanAppId($app);
\OC::$REQUESTEDAPP = $app;
$this->loadRoutes($app);
} elseif (strpos($url, '/ocsapp/apps/') === 0) {
} elseif (substr($url, 0, 13) === '/ocsapp/apps/') {
// empty string / 'ocsapp' / 'apps' / $app / rest of the route
[, , , $app,] = explode('/', $url, 5);
$app = \OC_App::cleanAppId($app);
\OC::$REQUESTEDAPP = $app;
$this->loadRoutes($app);
} elseif (strpos($url, '/settings/') === 0) {
} elseif (substr($url, 0, 10) === '/settings/') {
$this->loadRoutes('settings');
} elseif (substr($url, 0, 6) === '/core/') {
\OC::$REQUESTEDAPP = $url;
if (!\OC::$server->getConfig()->getSystemValueBool('maintenance') && !Util::needUpgrade()) {
\OC_App::loadApps();
}
$this->loadRoutes('core');
} else {
$this->loadRoutes();
}
\OC::$REQUESTEDAPP = $url;
if (!\OC::$server->getConfig()->getSystemValueBool('maintenance') && !Util::needUpgrade()) {
\OC_App::loadApps();
}
$this->loadRoutes('core');
$matcher = new UrlMatcher($this->root, $this->context);
try {
@@ -268,11 +271,6 @@ class Router implements IRouter {
try {
$parameters = $matcher->match($url . '/');
} catch (ResourceNotFoundException $newException) {
// Attempt to fallback to load all routes if none of the above route patterns matches and the route is not in core
if (!$loadAll) {
$this->loadRoutes();
return $this->findMatchingRoute($url, true);
}
// If we still didn't match a route, we throw the original exception
throw $e;
}
+1 -1
View File
@@ -1031,7 +1031,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerService(ILDAPProviderFactory::class, function (ContainerInterface $c) {
$config = $c->get(\OCP\IConfig::class);
$factoryClass = $config->getSystemValue('ldapProviderFactory', null);
if (is_null($factoryClass)) {
if (is_null($factoryClass) || !class_exists($factoryClass)) {
return new NullLDAPProviderFactory($this);
}
/** @var \OCP\LDAP\ILDAPProviderFactory $factory */
+2 -3
View File
@@ -35,6 +35,7 @@ use OCP\Support\CrashReport\IMessageReporter;
use OCP\Support\CrashReport\IRegistry;
use OCP\Support\CrashReport\IReporter;
use Throwable;
use function array_shift;
class Registry implements IRegistry {
@@ -119,8 +120,7 @@ class Registry implements IRegistry {
}
private function loadLazyProviders(): void {
$classes = $this->lazyReporters;
foreach ($classes as $class) {
while (($class = array_shift($this->lazyReporters)) !== null) {
try {
/** @var IReporter $reporter */
$reporter = $this->serverContainer->query($class);
@@ -151,6 +151,5 @@ class Registry implements IRegistry {
]);
}
}
$this->lazyReporters = [];
}
}
+2 -1
View File
@@ -94,9 +94,10 @@ interface IAccount extends \JsonSerializable {
/**
* Returns the requestes propery collection (multi-value properties)
*
* @throws PropertyDoesNotExistException against invalid collection name
* @since 22.0.0
*/
public function getPropertyCollection(string $propertyCollection): IAccountPropertyCollection;
public function getPropertyCollection(string $propertyCollectionName): IAccountPropertyCollection;
/**
* Get all properties that match the provided filters for scope and verification status
@@ -68,6 +68,14 @@ interface IAccountPropertyCollection extends JsonSerializable {
*/
public function addProperty(IAccountProperty $property): IAccountPropertyCollection;
/**
* adds a property to this collection with only specifying the value
*
* @throws InvalidArgumentException
* @since 22.0.0
*/
public function addPropertyWithDefaults(string $value): IAccountPropertyCollection;
/**
* removes a property of this collection
*
+1 -1
View File
@@ -64,7 +64,7 @@ if [ "$1" = "--acceptance-tests-dir" ]; then
fi
ACCEPTANCE_TESTS_CONFIG_DIR="../../$ACCEPTANCE_TESTS_DIR/config"
DEV_BRANCH="master"
DEV_BRANCH="stable22"
# "--timeout-multiplier N" option can be provided to set the timeout multiplier
# to be used in ActorContext.
+6
View File
@@ -12,6 +12,12 @@ function get_swift_token() {
fi
}
if [ "$OBJECT_STORE" == "s3" ]; then
echo "Waiting for minio to be ready"
timeout 60 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://minio:9000)" != "403" ]]; do sleep 5; done' || (
echo "Failed to wait for minio to be ready" && exit 1
)
fi
if [ "$OBJECT_STORE" == "swift" ]; then
echo "waiting for keystone"
until get_swift_token
+185 -286
View File
@@ -108,65 +108,191 @@ class AccountManagerTest extends TestCase {
[
'user' => $this->makeUser('j.doe', 'Jane Doe', 'jane.doe@acme.com'),
'data' => [
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Jane Doe', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'jane.doe@acme.com', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://acme.com', 'scope' => IAccountManager::SCOPE_PRIVATE],
[
'name' => IAccountManager::PROPERTY_DISPLAYNAME,
'value' => 'Jane Doe',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_EMAIL,
'value' => 'jane.doe@acme.com',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_TWITTER,
'value' => '@sometwitter',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_PHONE,
'value' => '+491601231212',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_ADDRESS,
'value' => 'some street',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_WEBSITE,
'value' => 'https://acme.com',
'scope' => IAccountManager::SCOPE_PRIVATE
],
],
],
[
'user' => $this->makeUser('a.allison', 'Alice Allison', 'a.allison@example.org'),
'data' => [
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Alice Allison', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_EMAIL => ['value' => 'a.allison@example.org', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_TWITTER => ['value' => '@a_alice', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491602312121', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'Dundee Road 45', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_LOCAL],
[
'name' => IAccountManager::PROPERTY_DISPLAYNAME,
'value' => 'Alice Allison',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_EMAIL,
'value' => 'a.allison@example.org',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_TWITTER,
'value' => '@a_alice',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_PHONE,
'value' => '+491602312121',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_ADDRESS,
'value' => 'Dundee Road 45',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_WEBSITE,
'value' => 'https://example.org',
'scope' => IAccountManager::SCOPE_LOCAL
],
],
],
[
'user' => $this->makeUser('b32c5a5b-1084-4380-8856-e5223b16de9f', 'Armel Oliseh', 'oliseh@example.com'),
'data' => [
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Armel Oliseh', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'oliseh@example.com', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_TWITTER => ['value' => '', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_PHONE => ['value' => '+491603121212', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'Sunflower Blvd. 77', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.com', 'scope' => IAccountManager::SCOPE_PUBLISHED],
[
'name' => IAccountManager::PROPERTY_DISPLAYNAME,
'value' => 'Armel Oliseh',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_EMAIL,
'value' => 'oliseh@example.com',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_TWITTER,
'value' => '',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_PHONE,
'value' => '+491603121212',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_ADDRESS,
'value' => 'Sunflower Blvd. 77',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_WEBSITE,
'value' => 'https://example.com',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
],
],
[
'user' => $this->makeUser('31b5316a-9b57-4b17-970a-315a4cbe73eb', 'K. Cheng', 'cheng@emca.com'),
'data' => [
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'K. Cheng', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'cheng@emca.com', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_TWITTER => ['value' => '', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_PHONE => ['value' => '+71601212123', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'Pinapple Street 22', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://emca.com', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::COLLECTION_EMAIL => [
['value' => 'k.cheng@emca.com', 'scope' => IAccountManager::SCOPE_LOCAL],
['value' => 'kai.cheng@emca.com', 'scope' => IAccountManager::SCOPE_LOCAL],
[
'name' => IAccountManager::PROPERTY_DISPLAYNAME,
'value' => 'K. Cheng',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_EMAIL,
'value' => 'cheng@emca.com',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_TWITTER,
'value' => '', '
scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_PHONE,
'value' => '+71601212123',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_ADDRESS,
'value' => 'Pinapple Street 22',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_WEBSITE,
'value' => 'https://emca.com',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::COLLECTION_EMAIL,
'value' => 'k.cheng@emca.com',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::COLLECTION_EMAIL,
'value' => 'kai.cheng@emca.com',
'scope' => IAccountManager::SCOPE_LOCAL
],
],
],
[
'user' => $this->makeUser('goodpal@elpmaxe.org', 'Goodpal, Kim', 'goodpal@elpmaxe.org'),
'data' => [
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Goodpal, Kim', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'goodpal@elpmaxe.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_TWITTER => ['value' => '', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_PHONE => ['value' => '+71602121231', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'Octopus Ave 17', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://elpmaxe.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
[
'name' => IAccountManager::PROPERTY_DISPLAYNAME,
'value' => 'Goodpal, Kim',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_EMAIL,
'value' => 'goodpal@elpmaxe.org',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
[
'name' => IAccountManager::PROPERTY_TWITTER,
'value' => '',
'scope' => IAccountManager::SCOPE_LOCAL
],
[
'name' => IAccountManager::PROPERTY_PHONE,
'value' => '+71602121231',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_ADDRESS,
'value' => 'Octopus Ave 17',
'scope' => IAccountManager::SCOPE_FEDERATED
],
[
'name' => IAccountManager::PROPERTY_WEBSITE,
'value' => 'https://elpmaxe.org',
'scope' => IAccountManager::SCOPE_PUBLISHED
],
],
],
];
foreach ($users as $userInfo) {
$this->accountManager->updateUser($userInfo['user'], $userInfo['data'], false);
$this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], false]);
}
}
@@ -198,7 +324,7 @@ class AccountManagerTest extends TestCase {
* @param bool $updateExisting
*/
public function testUpdateUser($newData, $oldData, $insertNew, $updateExisting) {
$accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser', 'updateVerifyStatus', 'checkEmailVerification']);
$accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']);
/** @var IUser $user */
$user = $this->createMock(IUser::class);
@@ -206,10 +332,6 @@ class AccountManagerTest extends TestCase {
$accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($oldData);
if ($updateExisting) {
$accountManager->expects($this->once())->method('checkEmailVerification')
->with($oldData, $newData, $user)->willReturn($newData);
$accountManager->expects($this->once())->method('updateVerifyStatus')
->with($oldData, $newData)->willReturn($newData);
$accountManager->expects($this->once())->method('updateExistingUser')
->with($user, $newData);
$accountManager->expects($this->never())->method('insertNewUser');
@@ -222,8 +344,6 @@ class AccountManagerTest extends TestCase {
if (!$insertNew && !$updateExisting) {
$accountManager->expects($this->never())->method('updateExistingUser');
$accountManager->expects($this->never())->method('checkEmailVerification');
$accountManager->expects($this->never())->method('updateVerifyStatus');
$accountManager->expects($this->never())->method('insertNewUser');
$this->eventDispatcher->expects($this->never())->method('dispatch');
} else {
@@ -239,231 +359,26 @@ class AccountManagerTest extends TestCase {
);
}
$accountManager->updateUser($user, $newData);
$this->invokePrivate($accountManager, 'updateUser', [$user, $newData]);
}
public function dataTrueFalse() {
return [
#$newData | $oldData | $insertNew | $updateExisting
[['myProperty' => ['value' => 'newData']], ['myProperty' => ['value' => 'oldData']], false, true],
[['myProperty' => ['value' => 'newData']], [], true, false],
[['myProperty' => ['value' => 'oldData']], ['myProperty' => ['value' => 'oldData']], false, false]
];
}
public function updateUserSetScopeProvider() {
return [
// regular scope switching
[
[
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_AVATAR => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_PRIVATE],
],
[
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PRIVATE],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
],
[
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PRIVATE],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
],
],
// legacy scope mapping, the given visibility values get converted to scopes
[
[
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_PRIVATE],
],
[
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::VISIBILITY_PUBLIC],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::VISIBILITY_CONTACTS_ONLY],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::VISIBILITY_PRIVATE],
],
[
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_LOCAL],
],
],
// invalid or unsupported scope values get converted to SCOPE_LOCAL
[
[
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
],
[
// SCOPE_PRIVATE is not allowed for display name and email
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_PRIVATE],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_PRIVATE],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_LOCAL],
],
[
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_LOCAL],
],
false, false,
],
// illegal scope values
[
[
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => IAccountManager::SCOPE_FEDERATED],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => IAccountManager::SCOPE_LOCAL],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => IAccountManager::SCOPE_PRIVATE],
],
[
IAccountManager::PROPERTY_PHONE => ['value' => '+491601231212', 'scope' => ''],
IAccountManager::PROPERTY_ADDRESS => ['value' => 'some street', 'scope' => 'v2-invalid'],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://example.org', 'scope' => 'invalid'],
],
[],
true, true
],
// invalid or unsupported scope values throw an exception when passing $throwOnData=true
[
[IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_PUBLISHED]],
[IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'Display Name', 'scope' => IAccountManager::SCOPE_PRIVATE]],
null,
// throw exception
true, true,
],
[
[IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_PUBLISHED]],
[IAccountManager::PROPERTY_EMAIL => ['value' => 'test@example.org', 'scope' => IAccountManager::SCOPE_PRIVATE]],
null,
// throw exception
true, true,
],
[
[IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => IAccountManager::SCOPE_PUBLISHED]],
[IAccountManager::PROPERTY_TWITTER => ['value' => '@sometwitter', 'scope' => 'invalid']],
null,
// throw exception
true, true,
],
];
}
/**
* @dataProvider updateUserSetScopeProvider
*/
public function testUpdateUserSetScope($oldData, $newData, $savedData, $throwOnData = true, $expectedThrow = false) {
$accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser', 'updateVerifyStatus', 'checkEmailVerification']);
/** @var IUser $user */
$user = $this->createMock(IUser::class);
$accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($oldData);
if ($expectedThrow) {
$accountManager->expects($this->never())->method('updateExistingUser');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('scope');
} else {
$accountManager->expects($this->once())->method('checkEmailVerification')
->with($oldData, $savedData, $user)->willReturn($savedData);
$accountManager->expects($this->once())->method('updateVerifyStatus')
->with($oldData, $savedData)->willReturn($savedData);
$accountManager->expects($this->once())->method('updateExistingUser')
->with($user, $savedData);
$accountManager->expects($this->never())->method('insertNewUser');
}
$accountManager->updateUser($user, $newData, $throwOnData);
}
/**
* @dataProvider dataTestGetUser
*
* @param string $setUser
* @param array $setData
* @param IUser $askUser
* @param array $expectedData
* @param bool $userAlreadyExists
*/
public function testGetUser($setUser, $setData, $askUser, $expectedData, $userAlreadyExists) {
$accountManager = $this->getInstance(['buildDefaultUserRecord', 'insertNewUser', 'addMissingDefaultValues']);
if (!$userAlreadyExists) {
$accountManager->expects($this->once())->method('buildDefaultUserRecord')
->with($askUser)->willReturn($expectedData);
$accountManager->expects($this->once())->method('insertNewUser')
->with($askUser, $expectedData);
}
if (empty($expectedData)) {
$accountManager->expects($this->never())->method('addMissingDefaultValues');
} else {
$accountManager->expects($this->once())->method('addMissingDefaultValues')->with($expectedData)
->willReturn($expectedData);
}
$this->addDummyValuesToTable($setUser, $setData);
$this->assertEquals($expectedData,
$accountManager->getUser($askUser)
);
}
public function dataTestGetUser() {
$user1 = $this->getMockBuilder(IUser::class)->getMock();
$user1->expects($this->any())->method('getUID')->willReturn('user1');
$user2 = $this->getMockBuilder(IUser::class)->getMock();
$user2->expects($this->any())->method('getUID')->willReturn('user2');
return [
['user1', ['key' => 'value'], $user1, ['key' => 'value'], true],
['user1', ['key' => 'value'], $user2, [], false],
];
}
public function testUpdateExistingUser() {
$user = $this->getMockBuilder(IUser::class)->getMock();
$user->expects($this->atLeastOnce())->method('getUID')->willReturn('uid');
$oldData = ['key' => ['value' => 'value']];
$newData = ['newKey' => ['value' => 'newValue']];
$this->addDummyValuesToTable('uid', $oldData);
$this->invokePrivate($this->accountManager, 'updateExistingUser', [$user, $newData]);
$newDataFromTable = $this->getDataFromTable('uid');
$this->assertEquals($newData, $newDataFromTable);
}
public function testInsertNewUser() {
$user = $this->getMockBuilder(IUser::class)->getMock();
$uid = 'uid';
$data = ['key' => ['value' => 'value']];
$user->expects($this->atLeastOnce())->method('getUID')->willReturn($uid);
$this->assertNull($this->getDataFromTable($uid));
$this->invokePrivate($this->accountManager, 'insertNewUser', [$user, $data]);
$dataFromDb = $this->getDataFromTable($uid);
$this->assertEquals($data, $dataFromDb);
}
public function testAddMissingDefaultValues() {
$input = [
'key1' => ['value' => 'value1', 'verified' => '0'],
'key2' => ['value' => 'value1'],
['value' => 'value1', 'verified' => '0', 'name' => 'key1'],
['value' => 'value1', 'name' => 'key2'],
];
$expected = [
'key1' => ['value' => 'value1', 'verified' => '0'],
'key2' => ['value' => 'value1', 'verified' => '0'],
['value' => 'value1', 'verified' => '0', 'name' => 'key1'],
['value' => 'value1', 'name' => 'key2', 'verified' => '0'],
];
$result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input]);
@@ -483,46 +398,30 @@ class AccountManagerTest extends TestCase {
->execute();
}
private function getDataFromTable($uid) {
$query = $this->connection->getQueryBuilder();
$query->select('data')->from($this->table)
->where($query->expr()->eq('uid', $query->createParameter('uid')))
->setParameter('uid', $uid);
$query->execute();
$qResult = $query->execute();
$result = $qResult->fetchAll();
$qResult->closeCursor();
if (!empty($result)) {
return json_decode($result[0]['data'], true);
}
}
public function testGetAccount() {
$accountManager = $this->getInstance(['getUser']);
/** @var IUser $user */
$user = $this->createMock(IUser::class);
$data = [
IAccountManager::PROPERTY_TWITTER =>
[
'value' => '@twitterhandle',
'scope' => IAccountManager::SCOPE_LOCAL,
'verified' => IAccountManager::NOT_VERIFIED,
],
IAccountManager::PROPERTY_EMAIL =>
[
'value' => 'test@example.com',
'scope' => IAccountManager::SCOPE_PUBLISHED,
'verified' => IAccountManager::VERIFICATION_IN_PROGRESS,
],
IAccountManager::PROPERTY_WEBSITE =>
[
'value' => 'https://example.com',
'scope' => IAccountManager::SCOPE_FEDERATED,
'verified' => IAccountManager::VERIFIED,
],
[
'value' => '@twitterhandle',
'scope' => IAccountManager::SCOPE_LOCAL,
'verified' => IAccountManager::NOT_VERIFIED,
'name' => IAccountManager::PROPERTY_TWITTER,
],
[
'value' => 'test@example.com',
'scope' => IAccountManager::SCOPE_PUBLISHED,
'verified' => IAccountManager::VERIFICATION_IN_PROGRESS,
'name' => IAccountManager::PROPERTY_EMAIL,
],
[
'value' => 'https://example.com',
'scope' => IAccountManager::SCOPE_FEDERATED,
'verified' => IAccountManager::VERIFIED,
'name' => IAccountManager::PROPERTY_WEBSITE,
],
];
$expected = new Account($user);
$expected->setProperty(IAccountManager::PROPERTY_TWITTER, '@twitterhandle', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
+42 -42
View File
@@ -23,7 +23,9 @@ namespace Test\Accounts;
use OC\Accounts\AccountManager;
use OC\Accounts\Hooks;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -43,7 +45,7 @@ class HooksTest extends TestCase {
/** @var AccountManager|MockObject */
private $accountManager;
/** @var Hooks|MockObject */
/** @var Hooks */
private $hooks;
protected function setUp(): void {
@@ -53,12 +55,7 @@ class HooksTest extends TestCase {
$this->accountManager = $this->getMockBuilder(AccountManager::class)
->disableOriginalConstructor()->getMock();
$this->hooks = $this->getMockBuilder(Hooks::class)
->setConstructorArgs([$this->logger])
->setMethods(['getAccountManager'])
->getMock();
$this->hooks->method('getAccountManager')->willReturn($this->accountManager);
$this->hooks = new Hooks($this->logger, $this->accountManager);
}
/**
@@ -72,48 +69,57 @@ class HooksTest extends TestCase {
*/
public function testChangeUserHook($params, $data, $setEmail, $setDisplayName, $error) {
if ($error) {
$this->accountManager->expects($this->never())->method('getUser');
$this->accountManager->expects($this->never())->method('updateUser');
$this->accountManager->expects($this->never())->method('updateAccount');
} else {
$this->accountManager->expects($this->once())->method('getUser')->willReturn($data);
$newData = $data;
$account = $this->createMock(IAccount::class);
$this->accountManager->expects($this->atLeastOnce())->method('getAccount')->willReturn($account);
if ($setEmail) {
$newData[IAccountManager::PROPERTY_EMAIL]['value'] = $params['value'];
$this->accountManager->expects($this->once())->method('updateUser')
->with($params['user'], $newData);
$property = $this->createMock(IAccountProperty::class);
$property->expects($this->atLeastOnce())
->method('getValue')
->willReturn($data[IAccountManager::PROPERTY_EMAIL]['value']);
$property->expects($this->atLeastOnce())
->method('setValue')
->with($params['value']);
$account->expects($this->atLeastOnce())
->method('getProperty')
->with(IAccountManager::PROPERTY_EMAIL)
->willReturn($property);
$this->accountManager->expects($this->once())
->method('updateAccount')
->with($account);
} elseif ($setDisplayName) {
$newData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $params['value'];
$this->accountManager->expects($this->once())->method('updateUser')
->with($params['user'], $newData);
$property = $this->createMock(IAccountProperty::class);
$property->expects($this->atLeastOnce())
->method('getValue')
->willReturn($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']);
$property->expects($this->atLeastOnce())
->method('setValue')
->with($params['value']);
$account->expects($this->atLeastOnce())
->method('getProperty')
->with(IAccountManager::PROPERTY_DISPLAYNAME)
->willReturn($property);
$this->accountManager->expects($this->once())
->method('updateAccount')
->with($account);
} else {
$this->accountManager->expects($this->never())->method('updateUser');
$this->accountManager->expects($this->never())->method('updateAccount');
}
}
$this->hooks->changeUserHook($params);
$this->hooks->changeUserHook($params['user'], $params['feature'], $params['value']);
}
public function dataTestChangeUserHook() {
$user = $this->createMock(IUser::class);
return [
[
['feature' => '', 'value' => ''],
[
IAccountManager::PROPERTY_EMAIL => ['value' => ''],
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => '']
],
false, false, true
],
[
['user' => $user, 'value' => ''],
[
IAccountManager::PROPERTY_EMAIL => ['value' => ''],
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => '']
],
false, false, true
],
[
['user' => $user, 'feature' => ''],
['user' => $user, 'feature' => '', 'value' => ''],
[
IAccountManager::PROPERTY_EMAIL => ['value' => ''],
IAccountManager::PROPERTY_DISPLAYNAME => ['value' => '']
@@ -146,10 +152,4 @@ class HooksTest extends TestCase {
],
];
}
public function testGetAccountManager() {
$hooks = new Hooks($this->logger);
$result = $this->invokePrivate($hooks, 'getAccountManager');
$this->assertInstanceOf(AccountManager::class, $result);
}
}
+1 -6
View File
@@ -62,7 +62,7 @@ class NonSeekableStream extends Wrapper {
class S3Test extends ObjectStoreTest {
protected function getInstance() {
$config = \OC::$server->getConfig()->getSystemValue('objectstore');
if (!is_array($config) || $config['class'] !== 'OC\\Files\\ObjectStore\\S3') {
if (!is_array($config) || $config['class'] !== S3::class) {
$this->markTestSkipped('objectstore not configured for s3');
}
@@ -70,11 +70,6 @@ class S3Test extends ObjectStoreTest {
}
public function testUploadNonSeekable() {
$config = \OC::$server->getConfig()->getSystemValue('objectstore');
if (!is_array($config) || $config['class'] !== 'OC\\Files\\ObjectStore\\S3') {
$this->markTestSkipped('objectstore not configured for s3');
}
$s3 = $this->getInstance();
$s3->writeObject('multiparttest', NonSeekableStream::wrap(fopen(__FILE__, 'r')));
+4 -4
View File
@@ -25,10 +25,10 @@ if (getenv('OBJECT_STORE') === 's3') {
'arguments' => [
'bucket' => 'nextcloud',
'autocreate' => true,
'key' => 'dummy',
'secret' => 'dummy',
'hostname' => getenv('DRONE') === 'true' ? 'fake-s3' : 'localhost',
'port' => 4569,
'key' => 'nextcloud',
'secret' => 'nextcloud',
'hostname' => getenv('DRONE') === 'true' ? 'minio' : 'localhost',
'port' => 9000,
'use_ssl' => false,
// required for some non amazon s3 implementations
'use_path_style' => true
+2 -2
View File
@@ -30,10 +30,10 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
$OC_Version = [22, 0, 0, 9];
$OC_Version = [22, 0, 0, 11];
// The human readable string
$OC_VersionString = '22.0.0 RC1';
$OC_VersionString = '22.0.0';
$OC_VersionCanBeUpgradedFrom = [
'nextcloud' => [