Completed
Push — master ( 3dac5b...93296c )
by John
47:41 queued 07:34
created
tests/lib/Accounts/AccountTest.php 1 patch
Indentation   +132 added lines, -132 removed lines patch added patch discarded remove patch
@@ -20,136 +20,136 @@
 block discarded – undo
20 20
  * @package Test\Accounts
21 21
  */
22 22
 class AccountTest extends TestCase {
23
-	public function testConstructor(): void {
24
-		$user = $this->createMock(IUser::class);
25
-		$account = new Account($user);
26
-		$this->assertEquals($user, $account->getUser());
27
-	}
28
-
29
-	public function testSetProperty(): void {
30
-		$user = $this->createMock(IUser::class);
31
-		$property = new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
32
-		$account = new Account($user);
33
-		$account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
34
-		$this->assertEquals($property, $account->getProperty(IAccountManager::PROPERTY_WEBSITE));
35
-	}
36
-
37
-	public function testGetAndGetAllProperties(): void {
38
-		$user = $this->createMock(IUser::class);
39
-		$properties = [
40
-			IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
41
-			IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, '')
42
-		];
43
-		$account = new Account($user);
44
-		$account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
45
-		$account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
46
-
47
-		$col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
48
-		$additionalProperty = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
49
-		$col->addProperty($additionalProperty);
50
-		$account->setPropertyCollection($col);
51
-
52
-		$this->assertEquals($properties, $account->getProperties());
53
-		$properties[] = $additionalProperty;
54
-		$this->assertEquals(array_values($properties), \iterator_to_array($account->getAllProperties()));
55
-	}
56
-
57
-	public function testSetAllPropertiesFromJson(): void {
58
-		$user = $this->createMock(IUser::class);
59
-		$properties = [
60
-			IAccountManager::PROPERTY_DISPLAYNAME => new AccountProperty(IAccountManager::PROPERTY_DISPLAYNAME, 'Steve', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
61
-			IAccountManager::PROPERTY_ADDRESS => new AccountProperty(IAccountManager::PROPERTY_ADDRESS, '123 Acorn Avenue', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
62
-			IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://www.example.org', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED, ''),
63
-			IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
64
-			IAccountManager::PROPERTY_AVATAR => new AccountProperty(IAccountManager::PROPERTY_AVATAR, '', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
65
-			IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '+358407991028', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
66
-			IAccountManager::PROPERTY_TWITTER => new AccountProperty(IAccountManager::PROPERTY_TWITTER, 'therealsteve', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
67
-			IAccountManager::PROPERTY_BLUESKY => new AccountProperty(IAccountManager::PROPERTY_BLUESKY, 'therealsteve.bsky.social', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
68
-			IAccountManager::PROPERTY_ORGANISATION => new AccountProperty(IAccountManager::PROPERTY_ORGANISATION, 'Steve Incorporated', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
69
-			IAccountManager::PROPERTY_ROLE => new AccountProperty(IAccountManager::PROPERTY_ROLE, 'Founder', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
70
-			IAccountManager::PROPERTY_HEADLINE => new AccountProperty(IAccountManager::PROPERTY_HEADLINE, 'I am Steve', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
71
-			IAccountManager::PROPERTY_BIOGRAPHY => new AccountProperty(IAccountManager::PROPERTY_BIOGRAPHY, 'Steve is the best', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
72
-			IAccountManager::PROPERTY_PROFILE_ENABLED => new AccountProperty(IAccountManager::PROPERTY_PROFILE_ENABLED, '1', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
73
-			IAccountManager::COLLECTION_EMAIL => [
74
-				new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
75
-				new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
76
-			],
77
-		];
78
-		$account = new Account($user);
79
-		$account->setAllPropertiesFromJson(json_decode(json_encode($properties), true));
80
-		$this->assertEquals($properties, $account->jsonSerialize());
81
-	}
82
-
83
-	public function testGetFilteredProperties(): void {
84
-		$user = $this->createMock(IUser::class);
85
-		$properties = [
86
-			IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
87
-			IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''),
88
-			IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
89
-		];
90
-		$account = new Account($user);
91
-		$account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
92
-		$account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
93
-		$account->setProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED);
94
-
95
-		$col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
96
-		$additionalProperty1 = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
97
-		$additionalProperty2 = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, '');
98
-		$col->addProperty($additionalProperty1);
99
-		$col->addProperty($additionalProperty2);
100
-		$account->setPropertyCollection($col);
101
-
102
-
103
-		$this->assertEquals(
104
-			[
105
-				IAccountManager::PROPERTY_WEBSITE => $properties[IAccountManager::PROPERTY_WEBSITE],
106
-				IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
107
-				IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty1,
108
-				IAccountManager::COLLECTION_EMAIL . '#1' => $additionalProperty2,
109
-			],
110
-			$account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED)
111
-		);
112
-		$this->assertEquals(
113
-			[
114
-				IAccountManager::PROPERTY_EMAIL => $properties[IAccountManager::PROPERTY_EMAIL],
115
-				IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
116
-				IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2,
117
-			],
118
-			$account->getFilteredProperties(null, IAccountManager::VERIFIED)
119
-		);
120
-		$this->assertEquals(
121
-			[
122
-				IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
123
-				IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2,
124
-			],
125
-			$account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED),
126
-		);
127
-	}
128
-
129
-	public function testJsonSerialize(): void {
130
-		$user = $this->createMock(IUser::class);
131
-		$properties = [
132
-			IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
133
-			IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''),
134
-			IAccountManager::COLLECTION_EMAIL => [
135
-				new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
136
-				new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
137
-				new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
138
-			],
139
-		];
140
-
141
-		$account = new Account($user);
142
-		$account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
143
-		$account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
144
-
145
-		$col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
146
-		$col->setProperties([
147
-			new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
148
-			new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
149
-			new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
150
-		]);
151
-		$account->setPropertyCollection($col);
152
-
153
-		$this->assertEquals($properties, $account->jsonSerialize());
154
-	}
23
+    public function testConstructor(): void {
24
+        $user = $this->createMock(IUser::class);
25
+        $account = new Account($user);
26
+        $this->assertEquals($user, $account->getUser());
27
+    }
28
+
29
+    public function testSetProperty(): void {
30
+        $user = $this->createMock(IUser::class);
31
+        $property = new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
32
+        $account = new Account($user);
33
+        $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
34
+        $this->assertEquals($property, $account->getProperty(IAccountManager::PROPERTY_WEBSITE));
35
+    }
36
+
37
+    public function testGetAndGetAllProperties(): void {
38
+        $user = $this->createMock(IUser::class);
39
+        $properties = [
40
+            IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
41
+            IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, '')
42
+        ];
43
+        $account = new Account($user);
44
+        $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
45
+        $account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
46
+
47
+        $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
48
+        $additionalProperty = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
49
+        $col->addProperty($additionalProperty);
50
+        $account->setPropertyCollection($col);
51
+
52
+        $this->assertEquals($properties, $account->getProperties());
53
+        $properties[] = $additionalProperty;
54
+        $this->assertEquals(array_values($properties), \iterator_to_array($account->getAllProperties()));
55
+    }
56
+
57
+    public function testSetAllPropertiesFromJson(): void {
58
+        $user = $this->createMock(IUser::class);
59
+        $properties = [
60
+            IAccountManager::PROPERTY_DISPLAYNAME => new AccountProperty(IAccountManager::PROPERTY_DISPLAYNAME, 'Steve', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
61
+            IAccountManager::PROPERTY_ADDRESS => new AccountProperty(IAccountManager::PROPERTY_ADDRESS, '123 Acorn Avenue', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
62
+            IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://www.example.org', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED, ''),
63
+            IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
64
+            IAccountManager::PROPERTY_AVATAR => new AccountProperty(IAccountManager::PROPERTY_AVATAR, '', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
65
+            IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '+358407991028', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
66
+            IAccountManager::PROPERTY_TWITTER => new AccountProperty(IAccountManager::PROPERTY_TWITTER, 'therealsteve', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
67
+            IAccountManager::PROPERTY_BLUESKY => new AccountProperty(IAccountManager::PROPERTY_BLUESKY, 'therealsteve.bsky.social', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
68
+            IAccountManager::PROPERTY_ORGANISATION => new AccountProperty(IAccountManager::PROPERTY_ORGANISATION, 'Steve Incorporated', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
69
+            IAccountManager::PROPERTY_ROLE => new AccountProperty(IAccountManager::PROPERTY_ROLE, 'Founder', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
70
+            IAccountManager::PROPERTY_HEADLINE => new AccountProperty(IAccountManager::PROPERTY_HEADLINE, 'I am Steve', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
71
+            IAccountManager::PROPERTY_BIOGRAPHY => new AccountProperty(IAccountManager::PROPERTY_BIOGRAPHY, 'Steve is the best', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
72
+            IAccountManager::PROPERTY_PROFILE_ENABLED => new AccountProperty(IAccountManager::PROPERTY_PROFILE_ENABLED, '1', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
73
+            IAccountManager::COLLECTION_EMAIL => [
74
+                new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
75
+                new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
76
+            ],
77
+        ];
78
+        $account = new Account($user);
79
+        $account->setAllPropertiesFromJson(json_decode(json_encode($properties), true));
80
+        $this->assertEquals($properties, $account->jsonSerialize());
81
+    }
82
+
83
+    public function testGetFilteredProperties(): void {
84
+        $user = $this->createMock(IUser::class);
85
+        $properties = [
86
+            IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
87
+            IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''),
88
+            IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
89
+        ];
90
+        $account = new Account($user);
91
+        $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
92
+        $account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
93
+        $account->setProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED);
94
+
95
+        $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
96
+        $additionalProperty1 = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, '');
97
+        $additionalProperty2 = new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, '');
98
+        $col->addProperty($additionalProperty1);
99
+        $col->addProperty($additionalProperty2);
100
+        $account->setPropertyCollection($col);
101
+
102
+
103
+        $this->assertEquals(
104
+            [
105
+                IAccountManager::PROPERTY_WEBSITE => $properties[IAccountManager::PROPERTY_WEBSITE],
106
+                IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
107
+                IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty1,
108
+                IAccountManager::COLLECTION_EMAIL . '#1' => $additionalProperty2,
109
+            ],
110
+            $account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED)
111
+        );
112
+        $this->assertEquals(
113
+            [
114
+                IAccountManager::PROPERTY_EMAIL => $properties[IAccountManager::PROPERTY_EMAIL],
115
+                IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
116
+                IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2,
117
+            ],
118
+            $account->getFilteredProperties(null, IAccountManager::VERIFIED)
119
+        );
120
+        $this->assertEquals(
121
+            [
122
+                IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE],
123
+                IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2,
124
+            ],
125
+            $account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED),
126
+        );
127
+    }
128
+
129
+    public function testJsonSerialize(): void {
130
+        $user = $this->createMock(IUser::class);
131
+        $properties = [
132
+            IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
133
+            IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''),
134
+            IAccountManager::COLLECTION_EMAIL => [
135
+                new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
136
+                new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
137
+                new AccountProperty(IAccountManager::COLLECTION_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
138
+            ],
139
+        ];
140
+
141
+        $account = new Account($user);
142
+        $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED);
143
+        $account->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED);
144
+
145
+        $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL);
146
+        $col->setProperties([
147
+            new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
148
+            new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''),
149
+            new AccountProperty($col->getName(), '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''),
150
+        ]);
151
+        $account->setPropertyCollection($col);
152
+
153
+        $this->assertEquals($properties, $account->jsonSerialize());
154
+    }
155 155
 }
Please login to merge, or discard this patch.
tests/lib/Accounts/AccountManagerTest.php 1 patch
Indentation   +1014 added lines, -1014 removed lines patch added patch discarded remove patch
@@ -42,1020 +42,1020 @@
 block discarded – undo
42 42
  */
43 43
 class AccountManagerTest extends TestCase {
44 44
 
45
-	/** accounts table name */
46
-	private string $table = 'accounts';
47
-	private AccountManager $accountManager;
48
-	private IDBConnection $connection;
49
-	private IPhoneNumberUtil $phoneNumberUtil;
50
-
51
-	protected IVerificationToken&MockObject $verificationToken;
52
-	protected IMailer&MockObject $mailer;
53
-	protected ICrypto&MockObject $crypto;
54
-	protected IURLGenerator&MockObject $urlGenerator;
55
-	protected Defaults&MockObject $defaults;
56
-	protected IFactory&MockObject $l10nFactory;
57
-	protected IConfig&MockObject $config;
58
-	protected IEventDispatcher&MockObject $eventDispatcher;
59
-	protected IJobList&MockObject $jobList;
60
-	private LoggerInterface&MockObject $logger;
61
-	private IClientService&MockObject $clientService;
62
-
63
-	protected function setUp(): void {
64
-		parent::setUp();
65
-		$this->connection = Server::get(IDBConnection::class);
66
-		$this->phoneNumberUtil = new PhoneNumberUtil();
67
-
68
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
69
-		$this->config = $this->createMock(IConfig::class);
70
-		$this->jobList = $this->createMock(IJobList::class);
71
-		$this->logger = $this->createMock(LoggerInterface::class);
72
-		$this->verificationToken = $this->createMock(IVerificationToken::class);
73
-		$this->mailer = $this->createMock(IMailer::class);
74
-		$this->defaults = $this->createMock(Defaults::class);
75
-		$this->l10nFactory = $this->createMock(IFactory::class);
76
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
77
-		$this->crypto = $this->createMock(ICrypto::class);
78
-		$this->clientService = $this->createMock(IClientService::class);
79
-
80
-		$this->accountManager = new AccountManager(
81
-			$this->connection,
82
-			$this->config,
83
-			$this->eventDispatcher,
84
-			$this->jobList,
85
-			$this->logger,
86
-			$this->verificationToken,
87
-			$this->mailer,
88
-			$this->defaults,
89
-			$this->l10nFactory,
90
-			$this->urlGenerator,
91
-			$this->crypto,
92
-			$this->phoneNumberUtil,
93
-			$this->clientService,
94
-		);
95
-	}
96
-
97
-	protected function tearDown(): void {
98
-		parent::tearDown();
99
-		$query = $this->connection->getQueryBuilder();
100
-		$query->delete($this->table)->executeStatement();
101
-	}
102
-
103
-	protected function makeUser(string $uid, string $name, ?string $email = null): IUser {
104
-		$user = $this->createMock(IUser::class);
105
-		$user->expects($this->any())
106
-			->method('getUid')
107
-			->willReturn($uid);
108
-		$user->expects($this->any())
109
-			->method('getDisplayName')
110
-			->willReturn($name);
111
-		if ($email !== null) {
112
-			$user->expects($this->any())
113
-				->method('getEMailAddress')
114
-				->willReturn($email);
115
-		}
116
-
117
-		return $user;
118
-	}
119
-
120
-	protected function populateOrUpdate(): void {
121
-		$users = [
122
-			[
123
-				'user' => $this->makeUser('j.doe', 'Jane Doe', '[email protected]'),
124
-				'data' => [
125
-					[
126
-						'name' => IAccountManager::PROPERTY_DISPLAYNAME,
127
-						'value' => 'Jane Doe',
128
-						'scope' => IAccountManager::SCOPE_PUBLISHED
129
-					],
130
-					[
131
-						'name' => IAccountManager::PROPERTY_EMAIL,
132
-						'value' => '[email protected]',
133
-						'scope' => IAccountManager::SCOPE_LOCAL
134
-					],
135
-					[
136
-						'name' => IAccountManager::PROPERTY_TWITTER,
137
-						'value' => '@sometwitter',
138
-						'scope' => IAccountManager::SCOPE_PUBLISHED
139
-					],
140
-					[
141
-						'name' => IAccountManager::PROPERTY_FEDIVERSE,
142
-						'value' => '@[email protected]',
143
-						'scope' => IAccountManager::SCOPE_PUBLISHED
144
-					],
145
-					[
146
-						'name' => IAccountManager::PROPERTY_PHONE,
147
-						'value' => '+491601231212',
148
-						'scope' => IAccountManager::SCOPE_FEDERATED
149
-					],
150
-					[
151
-						'name' => IAccountManager::PROPERTY_ADDRESS,
152
-						'value' => 'some street',
153
-						'scope' => IAccountManager::SCOPE_LOCAL
154
-					],
155
-					[
156
-						'name' => IAccountManager::PROPERTY_WEBSITE,
157
-						'value' => 'https://acme.com',
158
-						'scope' => IAccountManager::SCOPE_PRIVATE
159
-					],
160
-					[
161
-						'name' => IAccountManager::PROPERTY_ORGANISATION,
162
-						'value' => 'Some organisation',
163
-						'scope' => IAccountManager::SCOPE_LOCAL
164
-					],
165
-					[
166
-						'name' => IAccountManager::PROPERTY_ROLE,
167
-						'value' => 'Human',
168
-						'scope' => IAccountManager::SCOPE_LOCAL
169
-					],
170
-					[
171
-						'name' => IAccountManager::PROPERTY_HEADLINE,
172
-						'value' => 'Hi',
173
-						'scope' => IAccountManager::SCOPE_LOCAL
174
-					],
175
-					[
176
-						'name' => IAccountManager::PROPERTY_BIOGRAPHY,
177
-						'value' => 'Biography',
178
-						'scope' => IAccountManager::SCOPE_LOCAL
179
-					],
180
-				],
181
-			],
182
-			[
183
-				'user' => $this->makeUser('a.allison', 'Alice Allison', '[email protected]'),
184
-				'data' => [
185
-					[
186
-						'name' => IAccountManager::PROPERTY_DISPLAYNAME,
187
-						'value' => 'Alice Allison',
188
-						'scope' => IAccountManager::SCOPE_LOCAL
189
-					],
190
-					[
191
-						'name' => IAccountManager::PROPERTY_EMAIL,
192
-						'value' => '[email protected]',
193
-						'scope' => IAccountManager::SCOPE_LOCAL
194
-					],
195
-					[
196
-						'name' => IAccountManager::PROPERTY_TWITTER,
197
-						'value' => '@a_alice',
198
-						'scope' => IAccountManager::SCOPE_FEDERATED
199
-					],
200
-					[
201
-						'name' => IAccountManager::PROPERTY_FEDIVERSE,
202
-						'value' => '@[email protected]',
203
-						'scope' => IAccountManager::SCOPE_FEDERATED
204
-					],
205
-					[
206
-						'name' => IAccountManager::PROPERTY_PHONE,
207
-						'value' => '+491602312121',
208
-						'scope' => IAccountManager::SCOPE_LOCAL
209
-					],
210
-					[
211
-						'name' => IAccountManager::PROPERTY_ADDRESS,
212
-						'value' => 'Dundee Road 45',
213
-						'scope' => IAccountManager::SCOPE_LOCAL
214
-					],
215
-					[
216
-						'name' => IAccountManager::PROPERTY_WEBSITE,
217
-						'value' => 'https://example.org',
218
-						'scope' => IAccountManager::SCOPE_LOCAL
219
-					],
220
-					[
221
-						'name' => IAccountManager::PROPERTY_ORGANISATION,
222
-						'value' => 'Another organisation',
223
-						'scope' => IAccountManager::SCOPE_FEDERATED
224
-					],
225
-					[
226
-						'name' => IAccountManager::PROPERTY_ROLE,
227
-						'value' => 'Alien',
228
-						'scope' => IAccountManager::SCOPE_FEDERATED
229
-					],
230
-					[
231
-						'name' => IAccountManager::PROPERTY_HEADLINE,
232
-						'value' => 'Hello',
233
-						'scope' => IAccountManager::SCOPE_FEDERATED
234
-					],
235
-					[
236
-						'name' => IAccountManager::PROPERTY_BIOGRAPHY,
237
-						'value' => 'Different biography',
238
-						'scope' => IAccountManager::SCOPE_FEDERATED
239
-					],
240
-				],
241
-			],
242
-			[
243
-				'user' => $this->makeUser('b32c5a5b-1084-4380-8856-e5223b16de9f', 'Armel Oliseh', '[email protected]'),
244
-				'data' => [
245
-					[
246
-						'name' => IAccountManager::PROPERTY_DISPLAYNAME,
247
-						'value' => 'Armel Oliseh',
248
-						'scope' => IAccountManager::SCOPE_PUBLISHED
249
-					],
250
-					[
251
-						'name' => IAccountManager::PROPERTY_EMAIL,
252
-						'value' => '[email protected]',
253
-						'scope' => IAccountManager::SCOPE_PUBLISHED
254
-					],
255
-					[
256
-						'name' => IAccountManager::PROPERTY_TWITTER,
257
-						'value' => '',
258
-						'scope' => IAccountManager::SCOPE_LOCAL
259
-					],
260
-					[
261
-						'name' => IAccountManager::PROPERTY_FEDIVERSE,
262
-						'value' => '',
263
-						'scope' => IAccountManager::SCOPE_LOCAL
264
-					],
265
-					[
266
-						'name' => IAccountManager::PROPERTY_PHONE,
267
-						'value' => '+491603121212',
268
-						'scope' => IAccountManager::SCOPE_PUBLISHED
269
-					],
270
-					[
271
-						'name' => IAccountManager::PROPERTY_ADDRESS,
272
-						'value' => 'Sunflower Blvd. 77',
273
-						'scope' => IAccountManager::SCOPE_PUBLISHED
274
-					],
275
-					[
276
-						'name' => IAccountManager::PROPERTY_WEBSITE,
277
-						'value' => 'https://example.com',
278
-						'scope' => IAccountManager::SCOPE_PUBLISHED
279
-					],
280
-					[
281
-						'name' => IAccountManager::PROPERTY_ORGANISATION,
282
-						'value' => 'Yet another organisation',
283
-						'scope' => IAccountManager::SCOPE_PUBLISHED
284
-					],
285
-					[
286
-						'name' => IAccountManager::PROPERTY_ROLE,
287
-						'value' => 'Being',
288
-						'scope' => IAccountManager::SCOPE_PUBLISHED
289
-					],
290
-					[
291
-						'name' => IAccountManager::PROPERTY_HEADLINE,
292
-						'value' => 'This is a headline',
293
-						'scope' => IAccountManager::SCOPE_PUBLISHED
294
-					],
295
-					[
296
-						'name' => IAccountManager::PROPERTY_BIOGRAPHY,
297
-						'value' => 'Some long biography',
298
-						'scope' => IAccountManager::SCOPE_PUBLISHED
299
-					],
300
-				],
301
-			],
302
-			[
303
-				'user' => $this->makeUser('31b5316a-9b57-4b17-970a-315a4cbe73eb', 'K. Cheng', '[email protected]'),
304
-				'data' => [
305
-					[
306
-						'name' => IAccountManager::PROPERTY_DISPLAYNAME,
307
-						'value' => 'K. Cheng',
308
-						'scope' => IAccountManager::SCOPE_FEDERATED
309
-					],
310
-					[
311
-						'name' => IAccountManager::PROPERTY_EMAIL,
312
-						'value' => '[email protected]',
313
-						'scope' => IAccountManager::SCOPE_FEDERATED
314
-					],
315
-					[
316
-						'name' => IAccountManager::PROPERTY_TWITTER,
317
-						'value' => '', '
45
+    /** accounts table name */
46
+    private string $table = 'accounts';
47
+    private AccountManager $accountManager;
48
+    private IDBConnection $connection;
49
+    private IPhoneNumberUtil $phoneNumberUtil;
50
+
51
+    protected IVerificationToken&MockObject $verificationToken;
52
+    protected IMailer&MockObject $mailer;
53
+    protected ICrypto&MockObject $crypto;
54
+    protected IURLGenerator&MockObject $urlGenerator;
55
+    protected Defaults&MockObject $defaults;
56
+    protected IFactory&MockObject $l10nFactory;
57
+    protected IConfig&MockObject $config;
58
+    protected IEventDispatcher&MockObject $eventDispatcher;
59
+    protected IJobList&MockObject $jobList;
60
+    private LoggerInterface&MockObject $logger;
61
+    private IClientService&MockObject $clientService;
62
+
63
+    protected function setUp(): void {
64
+        parent::setUp();
65
+        $this->connection = Server::get(IDBConnection::class);
66
+        $this->phoneNumberUtil = new PhoneNumberUtil();
67
+
68
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
69
+        $this->config = $this->createMock(IConfig::class);
70
+        $this->jobList = $this->createMock(IJobList::class);
71
+        $this->logger = $this->createMock(LoggerInterface::class);
72
+        $this->verificationToken = $this->createMock(IVerificationToken::class);
73
+        $this->mailer = $this->createMock(IMailer::class);
74
+        $this->defaults = $this->createMock(Defaults::class);
75
+        $this->l10nFactory = $this->createMock(IFactory::class);
76
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
77
+        $this->crypto = $this->createMock(ICrypto::class);
78
+        $this->clientService = $this->createMock(IClientService::class);
79
+
80
+        $this->accountManager = new AccountManager(
81
+            $this->connection,
82
+            $this->config,
83
+            $this->eventDispatcher,
84
+            $this->jobList,
85
+            $this->logger,
86
+            $this->verificationToken,
87
+            $this->mailer,
88
+            $this->defaults,
89
+            $this->l10nFactory,
90
+            $this->urlGenerator,
91
+            $this->crypto,
92
+            $this->phoneNumberUtil,
93
+            $this->clientService,
94
+        );
95
+    }
96
+
97
+    protected function tearDown(): void {
98
+        parent::tearDown();
99
+        $query = $this->connection->getQueryBuilder();
100
+        $query->delete($this->table)->executeStatement();
101
+    }
102
+
103
+    protected function makeUser(string $uid, string $name, ?string $email = null): IUser {
104
+        $user = $this->createMock(IUser::class);
105
+        $user->expects($this->any())
106
+            ->method('getUid')
107
+            ->willReturn($uid);
108
+        $user->expects($this->any())
109
+            ->method('getDisplayName')
110
+            ->willReturn($name);
111
+        if ($email !== null) {
112
+            $user->expects($this->any())
113
+                ->method('getEMailAddress')
114
+                ->willReturn($email);
115
+        }
116
+
117
+        return $user;
118
+    }
119
+
120
+    protected function populateOrUpdate(): void {
121
+        $users = [
122
+            [
123
+                'user' => $this->makeUser('j.doe', 'Jane Doe', '[email protected]'),
124
+                'data' => [
125
+                    [
126
+                        'name' => IAccountManager::PROPERTY_DISPLAYNAME,
127
+                        'value' => 'Jane Doe',
128
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
129
+                    ],
130
+                    [
131
+                        'name' => IAccountManager::PROPERTY_EMAIL,
132
+                        'value' => '[email protected]',
133
+                        'scope' => IAccountManager::SCOPE_LOCAL
134
+                    ],
135
+                    [
136
+                        'name' => IAccountManager::PROPERTY_TWITTER,
137
+                        'value' => '@sometwitter',
138
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
139
+                    ],
140
+                    [
141
+                        'name' => IAccountManager::PROPERTY_FEDIVERSE,
142
+                        'value' => '@[email protected]',
143
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
144
+                    ],
145
+                    [
146
+                        'name' => IAccountManager::PROPERTY_PHONE,
147
+                        'value' => '+491601231212',
148
+                        'scope' => IAccountManager::SCOPE_FEDERATED
149
+                    ],
150
+                    [
151
+                        'name' => IAccountManager::PROPERTY_ADDRESS,
152
+                        'value' => 'some street',
153
+                        'scope' => IAccountManager::SCOPE_LOCAL
154
+                    ],
155
+                    [
156
+                        'name' => IAccountManager::PROPERTY_WEBSITE,
157
+                        'value' => 'https://acme.com',
158
+                        'scope' => IAccountManager::SCOPE_PRIVATE
159
+                    ],
160
+                    [
161
+                        'name' => IAccountManager::PROPERTY_ORGANISATION,
162
+                        'value' => 'Some organisation',
163
+                        'scope' => IAccountManager::SCOPE_LOCAL
164
+                    ],
165
+                    [
166
+                        'name' => IAccountManager::PROPERTY_ROLE,
167
+                        'value' => 'Human',
168
+                        'scope' => IAccountManager::SCOPE_LOCAL
169
+                    ],
170
+                    [
171
+                        'name' => IAccountManager::PROPERTY_HEADLINE,
172
+                        'value' => 'Hi',
173
+                        'scope' => IAccountManager::SCOPE_LOCAL
174
+                    ],
175
+                    [
176
+                        'name' => IAccountManager::PROPERTY_BIOGRAPHY,
177
+                        'value' => 'Biography',
178
+                        'scope' => IAccountManager::SCOPE_LOCAL
179
+                    ],
180
+                ],
181
+            ],
182
+            [
183
+                'user' => $this->makeUser('a.allison', 'Alice Allison', '[email protected]'),
184
+                'data' => [
185
+                    [
186
+                        'name' => IAccountManager::PROPERTY_DISPLAYNAME,
187
+                        'value' => 'Alice Allison',
188
+                        'scope' => IAccountManager::SCOPE_LOCAL
189
+                    ],
190
+                    [
191
+                        'name' => IAccountManager::PROPERTY_EMAIL,
192
+                        'value' => '[email protected]',
193
+                        'scope' => IAccountManager::SCOPE_LOCAL
194
+                    ],
195
+                    [
196
+                        'name' => IAccountManager::PROPERTY_TWITTER,
197
+                        'value' => '@a_alice',
198
+                        'scope' => IAccountManager::SCOPE_FEDERATED
199
+                    ],
200
+                    [
201
+                        'name' => IAccountManager::PROPERTY_FEDIVERSE,
202
+                        'value' => '@[email protected]',
203
+                        'scope' => IAccountManager::SCOPE_FEDERATED
204
+                    ],
205
+                    [
206
+                        'name' => IAccountManager::PROPERTY_PHONE,
207
+                        'value' => '+491602312121',
208
+                        'scope' => IAccountManager::SCOPE_LOCAL
209
+                    ],
210
+                    [
211
+                        'name' => IAccountManager::PROPERTY_ADDRESS,
212
+                        'value' => 'Dundee Road 45',
213
+                        'scope' => IAccountManager::SCOPE_LOCAL
214
+                    ],
215
+                    [
216
+                        'name' => IAccountManager::PROPERTY_WEBSITE,
217
+                        'value' => 'https://example.org',
218
+                        'scope' => IAccountManager::SCOPE_LOCAL
219
+                    ],
220
+                    [
221
+                        'name' => IAccountManager::PROPERTY_ORGANISATION,
222
+                        'value' => 'Another organisation',
223
+                        'scope' => IAccountManager::SCOPE_FEDERATED
224
+                    ],
225
+                    [
226
+                        'name' => IAccountManager::PROPERTY_ROLE,
227
+                        'value' => 'Alien',
228
+                        'scope' => IAccountManager::SCOPE_FEDERATED
229
+                    ],
230
+                    [
231
+                        'name' => IAccountManager::PROPERTY_HEADLINE,
232
+                        'value' => 'Hello',
233
+                        'scope' => IAccountManager::SCOPE_FEDERATED
234
+                    ],
235
+                    [
236
+                        'name' => IAccountManager::PROPERTY_BIOGRAPHY,
237
+                        'value' => 'Different biography',
238
+                        'scope' => IAccountManager::SCOPE_FEDERATED
239
+                    ],
240
+                ],
241
+            ],
242
+            [
243
+                'user' => $this->makeUser('b32c5a5b-1084-4380-8856-e5223b16de9f', 'Armel Oliseh', '[email protected]'),
244
+                'data' => [
245
+                    [
246
+                        'name' => IAccountManager::PROPERTY_DISPLAYNAME,
247
+                        'value' => 'Armel Oliseh',
248
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
249
+                    ],
250
+                    [
251
+                        'name' => IAccountManager::PROPERTY_EMAIL,
252
+                        'value' => '[email protected]',
253
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
254
+                    ],
255
+                    [
256
+                        'name' => IAccountManager::PROPERTY_TWITTER,
257
+                        'value' => '',
258
+                        'scope' => IAccountManager::SCOPE_LOCAL
259
+                    ],
260
+                    [
261
+                        'name' => IAccountManager::PROPERTY_FEDIVERSE,
262
+                        'value' => '',
263
+                        'scope' => IAccountManager::SCOPE_LOCAL
264
+                    ],
265
+                    [
266
+                        'name' => IAccountManager::PROPERTY_PHONE,
267
+                        'value' => '+491603121212',
268
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
269
+                    ],
270
+                    [
271
+                        'name' => IAccountManager::PROPERTY_ADDRESS,
272
+                        'value' => 'Sunflower Blvd. 77',
273
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
274
+                    ],
275
+                    [
276
+                        'name' => IAccountManager::PROPERTY_WEBSITE,
277
+                        'value' => 'https://example.com',
278
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
279
+                    ],
280
+                    [
281
+                        'name' => IAccountManager::PROPERTY_ORGANISATION,
282
+                        'value' => 'Yet another organisation',
283
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
284
+                    ],
285
+                    [
286
+                        'name' => IAccountManager::PROPERTY_ROLE,
287
+                        'value' => 'Being',
288
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
289
+                    ],
290
+                    [
291
+                        'name' => IAccountManager::PROPERTY_HEADLINE,
292
+                        'value' => 'This is a headline',
293
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
294
+                    ],
295
+                    [
296
+                        'name' => IAccountManager::PROPERTY_BIOGRAPHY,
297
+                        'value' => 'Some long biography',
298
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
299
+                    ],
300
+                ],
301
+            ],
302
+            [
303
+                'user' => $this->makeUser('31b5316a-9b57-4b17-970a-315a4cbe73eb', 'K. Cheng', '[email protected]'),
304
+                'data' => [
305
+                    [
306
+                        'name' => IAccountManager::PROPERTY_DISPLAYNAME,
307
+                        'value' => 'K. Cheng',
308
+                        'scope' => IAccountManager::SCOPE_FEDERATED
309
+                    ],
310
+                    [
311
+                        'name' => IAccountManager::PROPERTY_EMAIL,
312
+                        'value' => '[email protected]',
313
+                        'scope' => IAccountManager::SCOPE_FEDERATED
314
+                    ],
315
+                    [
316
+                        'name' => IAccountManager::PROPERTY_TWITTER,
317
+                        'value' => '', '
318 318
 						scope' => IAccountManager::SCOPE_LOCAL
319
-					],
320
-					[
321
-						'name' => IAccountManager::PROPERTY_FEDIVERSE,
322
-						'value' => '', '
319
+                    ],
320
+                    [
321
+                        'name' => IAccountManager::PROPERTY_FEDIVERSE,
322
+                        'value' => '', '
323 323
 						scope' => IAccountManager::SCOPE_LOCAL
324
-					],
325
-					[
326
-						'name' => IAccountManager::PROPERTY_PHONE,
327
-						'value' => '+71601212123',
328
-						'scope' => IAccountManager::SCOPE_LOCAL
329
-					],
330
-					[
331
-						'name' => IAccountManager::PROPERTY_ADDRESS,
332
-						'value' => 'Pinapple Street 22',
333
-						'scope' => IAccountManager::SCOPE_LOCAL
334
-					],
335
-					[
336
-						'name' => IAccountManager::PROPERTY_WEBSITE,
337
-						'value' => 'https://emca.com',
338
-						'scope' => IAccountManager::SCOPE_FEDERATED
339
-					],
340
-					[
341
-						'name' => IAccountManager::PROPERTY_ORGANISATION,
342
-						'value' => 'Organisation A',
343
-						'scope' => IAccountManager::SCOPE_LOCAL
344
-					],
345
-					[
346
-						'name' => IAccountManager::PROPERTY_ROLE,
347
-						'value' => 'Animal',
348
-						'scope' => IAccountManager::SCOPE_LOCAL
349
-					],
350
-					[
351
-						'name' => IAccountManager::PROPERTY_HEADLINE,
352
-						'value' => 'My headline',
353
-						'scope' => IAccountManager::SCOPE_LOCAL
354
-					],
355
-					[
356
-						'name' => IAccountManager::PROPERTY_BIOGRAPHY,
357
-						'value' => 'Short biography',
358
-						'scope' => IAccountManager::SCOPE_LOCAL
359
-					],
360
-					[
361
-						'name' => IAccountManager::COLLECTION_EMAIL,
362
-						'value' => '[email protected]',
363
-						'scope' => IAccountManager::SCOPE_LOCAL
364
-					],
365
-					[
366
-						'name' => IAccountManager::COLLECTION_EMAIL,
367
-						'value' => '[email protected]',
368
-						'scope' => IAccountManager::SCOPE_LOCAL
369
-					],
370
-				],
371
-			],
372
-			[
373
-				'user' => $this->makeUser('[email protected]', 'Goodpal, Kim', '[email protected]'),
374
-				'data' => [
375
-					[
376
-						'name' => IAccountManager::PROPERTY_DISPLAYNAME,
377
-						'value' => 'Goodpal, Kim',
378
-						'scope' => IAccountManager::SCOPE_PUBLISHED
379
-					],
380
-					[
381
-						'name' => IAccountManager::PROPERTY_EMAIL,
382
-						'value' => '[email protected]',
383
-						'scope' => IAccountManager::SCOPE_PUBLISHED
384
-					],
385
-					[
386
-						'name' => IAccountManager::PROPERTY_TWITTER,
387
-						'value' => '',
388
-						'scope' => IAccountManager::SCOPE_LOCAL
389
-					],
390
-					[
391
-						'name' => IAccountManager::PROPERTY_FEDIVERSE,
392
-						'value' => '',
393
-						'scope' => IAccountManager::SCOPE_LOCAL
394
-					],
395
-					[
396
-						'name' => IAccountManager::PROPERTY_PHONE,
397
-						'value' => '+71602121231',
398
-						'scope' => IAccountManager::SCOPE_FEDERATED
399
-					],
400
-					[
401
-						'name' => IAccountManager::PROPERTY_ADDRESS,
402
-						'value' => 'Octopus Ave 17',
403
-						'scope' => IAccountManager::SCOPE_FEDERATED
404
-					],
405
-					[
406
-						'name' => IAccountManager::PROPERTY_WEBSITE,
407
-						'value' => 'https://elpmaxe.org',
408
-						'scope' => IAccountManager::SCOPE_PUBLISHED
409
-					],
410
-					[
411
-						'name' => IAccountManager::PROPERTY_ORGANISATION,
412
-						'value' => 'Organisation B',
413
-						'scope' => IAccountManager::SCOPE_FEDERATED
414
-					],
415
-					[
416
-						'name' => IAccountManager::PROPERTY_ROLE,
417
-						'value' => 'Organism',
418
-						'scope' => IAccountManager::SCOPE_FEDERATED
419
-					],
420
-					[
421
-						'name' => IAccountManager::PROPERTY_HEADLINE,
422
-						'value' => 'Best headline',
423
-						'scope' => IAccountManager::SCOPE_FEDERATED
424
-					],
425
-					[
426
-						'name' => IAccountManager::PROPERTY_BIOGRAPHY,
427
-						'value' => 'Autobiography',
428
-						'scope' => IAccountManager::SCOPE_FEDERATED
429
-					],
430
-				],
431
-			],
432
-		];
433
-		$this->config->expects($this->exactly(count($users)))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
434
-		foreach ($users as $userInfo) {
435
-			$this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], null, false]);
436
-		}
437
-	}
438
-
439
-	/**
440
-	 * get a instance of the accountManager
441
-	 *
442
-	 * @return MockObject | AccountManager
443
-	 */
444
-	public function getInstance(?array $mockedMethods = null) {
445
-		return $this->getMockBuilder(AccountManager::class)
446
-			->setConstructorArgs([
447
-				$this->connection,
448
-				$this->config,
449
-				$this->eventDispatcher,
450
-				$this->jobList,
451
-				$this->logger,
452
-				$this->verificationToken,
453
-				$this->mailer,
454
-				$this->defaults,
455
-				$this->l10nFactory,
456
-				$this->urlGenerator,
457
-				$this->crypto,
458
-				$this->phoneNumberUtil,
459
-				$this->clientService,
460
-			])
461
-			->onlyMethods($mockedMethods)
462
-			->getMock();
463
-	}
464
-
465
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')]
466
-	public function testUpdateUser(array $newData, array $oldData, bool $insertNew, bool $updateExisting): void {
467
-		$accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']);
468
-		/** @var IUser $user */
469
-		$user = $this->createMock(IUser::class);
470
-
471
-		if ($updateExisting) {
472
-			$accountManager->expects($this->once())->method('updateExistingUser')
473
-				->with($user, $newData);
474
-			$accountManager->expects($this->never())->method('insertNewUser');
475
-		}
476
-		if ($insertNew) {
477
-			$accountManager->expects($this->once())->method('insertNewUser')
478
-				->with($user, $newData);
479
-			$accountManager->expects($this->never())->method('updateExistingUser');
480
-		}
481
-
482
-		if (!$insertNew && !$updateExisting) {
483
-			$accountManager->expects($this->never())->method('updateExistingUser');
484
-			$accountManager->expects($this->never())->method('insertNewUser');
485
-			$this->eventDispatcher->expects($this->never())->method('dispatchTyped');
486
-		} else {
487
-			$this->eventDispatcher->expects($this->once())->method('dispatchTyped')
488
-				->willReturnCallback(
489
-					function ($event) use ($user, $newData): void {
490
-						$this->assertInstanceOf(UserUpdatedEvent::class, $event);
491
-						$this->assertSame($user, $event->getUser());
492
-						$this->assertSame($newData, $event->getData());
493
-					}
494
-				);
495
-		}
496
-
497
-		$this->invokePrivate($accountManager, 'updateUser', [$user, $newData, $oldData]);
498
-	}
499
-
500
-	public static function dataTrueFalse(): array {
501
-		return [
502
-			#$newData | $oldData | $insertNew | $updateExisting
503
-			[['myProperty' => ['value' => 'newData']], ['myProperty' => ['value' => 'oldData']], false, true],
504
-			[['myProperty' => ['value' => 'oldData']], ['myProperty' => ['value' => 'oldData']], false, false]
505
-		];
506
-	}
507
-
508
-	public function testAddMissingDefaults(): void {
509
-		$user = $this->createMock(IUser::class);
510
-
511
-		$this->config
512
-			->expects($this->once())
513
-			->method('getAppValue')
514
-			->with('settings', 'profile_enabled_by_default', '1')
515
-			->willReturn('1');
516
-
517
-		$input = [
518
-			[
519
-				'name' => IAccountManager::PROPERTY_DISPLAYNAME,
520
-				'value' => 'bob',
521
-				'verified' => IAccountManager::NOT_VERIFIED,
522
-			],
523
-			[
524
-				'name' => IAccountManager::PROPERTY_EMAIL,
525
-				'value' => '[email protected]',
526
-			],
527
-		];
528
-
529
-		$expected = [
530
-			[
531
-				'name' => IAccountManager::PROPERTY_DISPLAYNAME,
532
-				'value' => 'bob',
533
-				'scope' => IAccountManager::SCOPE_FEDERATED,
534
-				'verified' => IAccountManager::NOT_VERIFIED,
535
-			],
536
-
537
-			[
538
-				'name' => IAccountManager::PROPERTY_EMAIL,
539
-				'value' => '[email protected]',
540
-				'scope' => IAccountManager::SCOPE_FEDERATED,
541
-				'verified' => IAccountManager::NOT_VERIFIED,
542
-			],
543
-
544
-			[
545
-				'name' => IAccountManager::PROPERTY_ADDRESS,
546
-				'value' => '',
547
-				'scope' => IAccountManager::SCOPE_LOCAL,
548
-				'verified' => IAccountManager::NOT_VERIFIED,
549
-			],
550
-
551
-			[
552
-				'name' => IAccountManager::PROPERTY_WEBSITE,
553
-				'value' => '',
554
-				'scope' => IAccountManager::SCOPE_LOCAL,
555
-				'verified' => IAccountManager::NOT_VERIFIED,
556
-			],
557
-
558
-			[
559
-				'name' => IAccountManager::PROPERTY_AVATAR,
560
-				'scope' => IAccountManager::SCOPE_FEDERATED
561
-			],
562
-
563
-			[
564
-				'name' => IAccountManager::PROPERTY_PHONE,
565
-				'value' => '',
566
-				'scope' => IAccountManager::SCOPE_LOCAL,
567
-				'verified' => IAccountManager::NOT_VERIFIED,
568
-			],
569
-
570
-			[
571
-				'name' => IAccountManager::PROPERTY_TWITTER,
572
-				'value' => '',
573
-				'scope' => IAccountManager::SCOPE_LOCAL,
574
-				'verified' => IAccountManager::NOT_VERIFIED,
575
-			],
576
-
577
-			[
578
-				'name' => IAccountManager::PROPERTY_BLUESKY,
579
-				'value' => '',
580
-				'scope' => IAccountManager::SCOPE_LOCAL,
581
-				'verified' => IAccountManager::NOT_VERIFIED,
582
-			],
583
-
584
-			[
585
-				'name' => IAccountManager::PROPERTY_FEDIVERSE,
586
-				'value' => '',
587
-				'scope' => IAccountManager::SCOPE_LOCAL,
588
-				'verified' => IAccountManager::NOT_VERIFIED,
589
-			],
590
-
591
-			[
592
-				'name' => IAccountManager::PROPERTY_ORGANISATION,
593
-				'value' => '',
594
-				'scope' => IAccountManager::SCOPE_LOCAL,
595
-			],
596
-
597
-			[
598
-				'name' => IAccountManager::PROPERTY_ROLE,
599
-				'value' => '',
600
-				'scope' => IAccountManager::SCOPE_LOCAL,
601
-			],
602
-
603
-			[
604
-				'name' => IAccountManager::PROPERTY_HEADLINE,
605
-				'value' => '',
606
-				'scope' => IAccountManager::SCOPE_LOCAL,
607
-			],
608
-
609
-			[
610
-				'name' => IAccountManager::PROPERTY_BIOGRAPHY,
611
-				'value' => '',
612
-				'scope' => IAccountManager::SCOPE_LOCAL,
613
-			],
614
-
615
-			[
616
-				'name' => IAccountManager::PROPERTY_BIRTHDATE,
617
-				'value' => '',
618
-				'scope' => IAccountManager::SCOPE_LOCAL,
619
-			],
620
-
621
-			[
622
-				'name' => IAccountManager::PROPERTY_PROFILE_ENABLED,
623
-				'value' => '1',
624
-			],
625
-
626
-			[
627
-				'name' => IAccountManager::PROPERTY_PRONOUNS,
628
-				'value' => '',
629
-				'scope' => IAccountManager::SCOPE_FEDERATED,
630
-			],
631
-		];
632
-		$this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
633
-
634
-		$defaultUserRecord = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
635
-		$result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input, $defaultUserRecord]);
636
-
637
-		$this->assertSame($expected, $result);
638
-	}
639
-
640
-	public function testGetAccount(): void {
641
-		$accountManager = $this->getInstance(['getUser']);
642
-		/** @var IUser $user */
643
-		$user = $this->createMock(IUser::class);
644
-
645
-		$data = [
646
-			[
647
-				'value' => '@twitterhandle',
648
-				'scope' => IAccountManager::SCOPE_LOCAL,
649
-				'verified' => IAccountManager::NOT_VERIFIED,
650
-				'name' => IAccountManager::PROPERTY_TWITTER,
651
-			],
652
-			[
653
-				'value' => '@[email protected]',
654
-				'scope' => IAccountManager::SCOPE_LOCAL,
655
-				'verified' => IAccountManager::NOT_VERIFIED,
656
-				'name' => IAccountManager::PROPERTY_FEDIVERSE,
657
-			],
658
-			[
659
-				'value' => '[email protected]',
660
-				'scope' => IAccountManager::SCOPE_PUBLISHED,
661
-				'verified' => IAccountManager::VERIFICATION_IN_PROGRESS,
662
-				'name' => IAccountManager::PROPERTY_EMAIL,
663
-			],
664
-			[
665
-				'value' => 'https://example.com',
666
-				'scope' => IAccountManager::SCOPE_FEDERATED,
667
-				'verified' => IAccountManager::VERIFIED,
668
-				'name' => IAccountManager::PROPERTY_WEBSITE,
669
-			],
670
-		];
671
-		$expected = new Account($user);
672
-		$expected->setProperty(IAccountManager::PROPERTY_TWITTER, '@twitterhandle', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
673
-		$expected->setProperty(IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
674
-		$expected->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS);
675
-		$expected->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED);
676
-
677
-		$accountManager->expects($this->once())
678
-			->method('getUser')
679
-			->willReturn($data);
680
-		$this->assertEquals($expected, $accountManager->getAccount($user));
681
-	}
682
-
683
-	public static function dataParsePhoneNumber(): array {
684
-		return [
685
-			['0711 / 25 24 28-90', 'DE', '+4971125242890'],
686
-			['0711 / 25 24 28-90', '', null],
687
-			['+49 711 / 25 24 28-90', '', '+4971125242890'],
688
-		];
689
-	}
690
-
691
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataParsePhoneNumber')]
692
-	public function testSanitizePhoneNumberOnUpdateAccount(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void {
693
-		$this->config->method('getSystemValueString')
694
-			->willReturn($defaultRegion);
695
-
696
-		$user = $this->createMock(IUser::class);
697
-		$account = new Account($user);
698
-		$account->setProperty(IAccountManager::PROPERTY_PHONE, $phoneInput, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
699
-		$manager = $this->getInstance(['getUser', 'updateUser']);
700
-		$manager->method('getUser')
701
-			->with($user, false)
702
-			->willReturn([]);
703
-		$manager->expects($phoneNumber === null ? self::never() : self::once())
704
-			->method('updateUser');
705
-
706
-		if ($phoneNumber === null) {
707
-			$this->expectException(\InvalidArgumentException::class);
708
-		}
709
-
710
-		$manager->updateAccount($account);
711
-
712
-		if ($phoneNumber !== null) {
713
-			self::assertEquals($phoneNumber, $account->getProperty(IAccountManager::PROPERTY_PHONE)->getValue());
714
-		}
715
-	}
716
-
717
-	public static function dataSanitizeOnUpdate(): array {
718
-		return [
719
-			[IAccountManager::PROPERTY_WEBSITE, 'https://nextcloud.com', 'https://nextcloud.com'],
720
-			[IAccountManager::PROPERTY_WEBSITE, 'http://nextcloud.com', 'http://nextcloud.com'],
721
-			[IAccountManager::PROPERTY_WEBSITE, 'ftp://nextcloud.com', null],
722
-			[IAccountManager::PROPERTY_WEBSITE, '//nextcloud.com/', null],
723
-			[IAccountManager::PROPERTY_WEBSITE, 'https:///?query', null],
724
-
725
-			[IAccountManager::PROPERTY_TWITTER, '@nextcloud', 'nextcloud'],
726
-			[IAccountManager::PROPERTY_TWITTER, '_nextcloud', '_nextcloud'],
727
-			[IAccountManager::PROPERTY_TWITTER, 'FooB4r', 'FooB4r'],
728
-			[IAccountManager::PROPERTY_TWITTER, 'X', null],
729
-			[IAccountManager::PROPERTY_TWITTER, 'next.cloud', null],
730
-			[IAccountManager::PROPERTY_TWITTER, 'ab/cd.zip', null],
731
-			[IAccountManager::PROPERTY_TWITTER, 'tooLongForTwitterAndX', null],
732
-
733
-			[IAccountManager::PROPERTY_FEDIVERSE, '[email protected]', '[email protected]'],
734
-			[IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', '[email protected]'],
735
-			[IAccountManager::PROPERTY_FEDIVERSE, '[email protected]', '[email protected]'],
736
-			[IAccountManager::PROPERTY_FEDIVERSE, 'invalid/[email protected]', null],
737
-			[IAccountManager::PROPERTY_FEDIVERSE, '[email protected]/malware.exe', null],
738
-			[IAccountManager::PROPERTY_FEDIVERSE, '@is-it-a-host-or-name', null],
739
-			[IAccountManager::PROPERTY_FEDIVERSE, 'only-a-name', null],
740
-		];
741
-	}
742
-
743
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeOnUpdate')]
744
-	public function testSanitizingOnUpdateAccount(string $property, string $input, ?string $output): void {
745
-
746
-		if ($property === IAccountManager::PROPERTY_FEDIVERSE) {
747
-			// We do not test the server response here we do this in the `testSanitizingFediverseServer`
748
-			$this->config
749
-				->method('getSystemValueBool')
750
-				->with('has_internet_connection', true)
751
-				->willReturn(false);
752
-		}
753
-
754
-		$user = $this->createMock(IUser::class);
755
-
756
-		$account = new Account($user);
757
-		$account->setProperty($property, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
758
-
759
-		$manager = $this->getInstance(['getUser', 'updateUser']);
760
-		$manager->method('getUser')
761
-			->with($user, false)
762
-			->willReturn([]);
763
-		$manager->expects($output === null ? self::never() : self::once())
764
-			->method('updateUser');
765
-
766
-		if ($output === null) {
767
-			$this->expectException(\InvalidArgumentException::class);
768
-			$this->expectExceptionMessage($property);
769
-		}
770
-
771
-		$manager->updateAccount($account);
772
-
773
-		if ($output !== null) {
774
-			self::assertEquals($output, $account->getProperty($property)->getValue());
775
-		}
776
-	}
777
-
778
-	public static function dataSanitizeFediverseServer(): array {
779
-		return [
780
-			'no internet' => [
781
-				'@[email protected]',
782
-				'[email protected]',
783
-				false,
784
-				null,
785
-			],
786
-			'no internet - no at' => [
787
-				'[email protected]',
788
-				'[email protected]',
789
-				false,
790
-				null,
791
-			],
792
-			'valid response' => [
793
-				'@[email protected]',
794
-				'[email protected]',
795
-				true,
796
-				json_encode([
797
-					'subject' => 'acct:[email protected]',
798
-					'links' => [
799
-						[
800
-							'rel' => 'self',
801
-							'type' => 'application/activity+json',
802
-							'href' => 'https://example.com/users/foo',
803
-						],
804
-					],
805
-				]),
806
-			],
807
-			'valid response - no at' => [
808
-				'[email protected]',
809
-				'[email protected]',
810
-				true,
811
-				json_encode([
812
-					'subject' => 'acct:[email protected]',
813
-					'links' => [
814
-						[
815
-							'rel' => 'self',
816
-							'type' => 'application/activity+json',
817
-							'href' => 'https://example.com/users/foo',
818
-						],
819
-					],
820
-				]),
821
-			],
822
-			// failures
823
-			'invalid response' => [
824
-				'@[email protected]',
825
-				null,
826
-				true,
827
-				json_encode([
828
-					'subject' => 'acct:[email protected]',
829
-					'links' => [],
830
-				]),
831
-			],
832
-			'no response' => [
833
-				'@[email protected]',
834
-				null,
835
-				true,
836
-				null,
837
-			],
838
-			'wrong user' => [
839
-				'@[email protected]',
840
-				null,
841
-				true,
842
-				json_encode([
843
-					'links' => [],
844
-				]),
845
-			],
846
-		];
847
-	}
848
-
849
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeFediverseServer')]
850
-	public function testSanitizingFediverseServer(string $input, ?string $output, bool $hasInternet, ?string $serverResponse): void {
851
-		$this->config->expects(self::once())
852
-			->method('getSystemValueBool')
853
-			->with('has_internet_connection', true)
854
-			->willReturn($hasInternet);
855
-
856
-		if ($hasInternet) {
857
-			$client = $this->createMock(IClient::class);
858
-			if ($serverResponse !== null) {
859
-				$response = $this->createMock(IResponse::class);
860
-				$response->method('getBody')
861
-					->willReturn($serverResponse);
862
-				$client->expects(self::once())
863
-					->method('get')
864
-					->with('https://example.com/.well-known/webfinger?resource=acct:[email protected]')
865
-					->willReturn($response);
866
-			} else {
867
-				$client->expects(self::once())
868
-					->method('get')
869
-					->with('https://example.com/.well-known/webfinger?resource=acct:[email protected]')
870
-					->willThrowException(new \Exception('404'));
871
-			}
872
-
873
-			$this->clientService
874
-				->expects(self::once())
875
-				->method('newClient')
876
-				->willReturn($client);
877
-		} else {
878
-			$this->clientService
879
-				->expects(self::never())
880
-				->method('newClient');
881
-		}
882
-
883
-		$user = $this->createMock(IUser::class);
884
-		$account = new Account($user);
885
-		$account->setProperty(IAccountManager::PROPERTY_FEDIVERSE, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
886
-
887
-		$manager = $this->getInstance(['getUser', 'updateUser']);
888
-		$manager->method('getUser')
889
-			->with($user, false)
890
-			->willReturn([]);
891
-		$manager->expects($output === null ? self::never() : self::once())
892
-			->method('updateUser');
893
-
894
-		if ($output === null) {
895
-			$this->expectException(\InvalidArgumentException::class);
896
-			$this->expectExceptionMessage(IAccountManager::PROPERTY_FEDIVERSE);
897
-		}
898
-
899
-		$manager->updateAccount($account);
900
-
901
-		if ($output !== null) {
902
-			self::assertEquals($output, $account->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue());
903
-		}
904
-	}
905
-
906
-	#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
907
-	public function testSearchUsers(string $property, array $values, array $expected): void {
908
-		$this->populateOrUpdate();
909
-
910
-		$matchedUsers = $this->accountManager->searchUsers($property, $values);
911
-		foreach ($expected as $expectedEntry) {
912
-			$this->assertContains($expectedEntry, $matchedUsers);
913
-		}
914
-		if (empty($expected)) {
915
-			$this->assertEmpty($matchedUsers);
916
-		}
917
-	}
918
-
919
-	public static function searchDataProvider(): array {
920
-		return [
921
-			[ #0 Search for an existing name
922
-				IAccountManager::PROPERTY_DISPLAYNAME,
923
-				['Jane Doe'],
924
-				['Jane Doe' => 'j.doe']
925
-			],
926
-			[ #1 Search for part of a name (no result)
927
-				IAccountManager::PROPERTY_DISPLAYNAME,
928
-				['Jane'],
929
-				[]
930
-			],
931
-			[ #2 Search for part of a name (no result, test wildcard)
932
-				IAccountManager::PROPERTY_DISPLAYNAME,
933
-				['Jane%'],
934
-				[]
935
-			],
936
-			[ #3 Search for phone
937
-				IAccountManager::PROPERTY_PHONE,
938
-				['+491603121212'],
939
-				['+491603121212' => 'b32c5a5b-1084-4380-8856-e5223b16de9f'],
940
-			],
941
-			[ #4 Search for twitter handles
942
-				IAccountManager::PROPERTY_TWITTER,
943
-				['@sometwitter', '@a_alice', '@unseen'],
944
-				['@sometwitter' => 'j.doe', '@a_alice' => 'a.allison'],
945
-			],
946
-			[ #5 Search for email
947
-				IAccountManager::PROPERTY_EMAIL,
948
-				['[email protected]'],
949
-				['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
950
-			],
951
-			[ #6 Search for email by additional email
952
-				IAccountManager::PROPERTY_EMAIL,
953
-				['[email protected]'],
954
-				['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
955
-			],
956
-			[ #7 Search for additional email
957
-				IAccountManager::COLLECTION_EMAIL,
958
-				['[email protected]', '[email protected]'],
959
-				['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
960
-			],
961
-			[ #8 Search for email by additional email (two valid search values, but the same user)
962
-				IAccountManager::PROPERTY_EMAIL,
963
-				['[email protected]', '[email protected]'],
964
-				[
965
-					'[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb',
966
-				],
967
-			],
968
-		];
969
-	}
970
-
971
-	public static function dataCheckEmailVerification(): array {
972
-		return [
973
-			[['steve', 'Steve Smith', '[email protected]'], null],
974
-			[['emma', 'Emma Morales', '[email protected]'], '[email protected]'],
975
-			[['[email protected]', 'Sarah Foster', '[email protected]'], null],
976
-			[['[email protected]', 'Cole Harrison', '[email protected]'], '[email protected]'],
977
-			[['8d29e358-cf69-4849-bbf9-28076c0b908b', 'Alice McPherson', '[email protected]'], '[email protected]'],
978
-			[['11da2744-3f4d-4c17-8c13-4c057a379237', 'James Loranger', '[email protected]'], ''],
979
-		];
980
-	}
981
-
982
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCheckEmailVerification')]
983
-	public function testCheckEmailVerification(array $userData, ?string $newEmail): void {
984
-		$user = $this->makeUser(...$userData);
985
-		// Once because of getAccount, once because of getUser
986
-		$this->config->expects($this->exactly(2))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
987
-		$account = $this->accountManager->getAccount($user);
988
-		$emailUpdated = false;
989
-
990
-		if (!empty($newEmail)) {
991
-			$account->getProperty(IAccountManager::PROPERTY_EMAIL)->setValue($newEmail);
992
-			$emailUpdated = true;
993
-		}
994
-
995
-		if ($emailUpdated) {
996
-			$this->jobList->expects($this->once())
997
-				->method('add')
998
-				->with(VerifyUserData::class);
999
-		} else {
1000
-			$this->jobList->expects($this->never())
1001
-				->method('add')
1002
-				->with(VerifyUserData::class);
1003
-		}
1004
-
1005
-		/** @var array $oldData */
1006
-		$oldData = $this->invokePrivate($this->accountManager, 'getUser', [$user, false]);
1007
-		$this->invokePrivate($this->accountManager, 'checkEmailVerification', [$account, $oldData]);
1008
-	}
1009
-
1010
-	public static function dataSetDefaultPropertyScopes(): array {
1011
-		return [
1012
-			[
1013
-				[],
1014
-				[
1015
-					IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1016
-					IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
1017
-					IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
1018
-					IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_LOCAL,
1019
-				]
1020
-			],
1021
-			[
1022
-				[
1023
-					IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1024
-					IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
1025
-					IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1026
-				], [
1027
-					IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1028
-					IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
1029
-					IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1030
-				]
1031
-			],
1032
-			[
1033
-				[
1034
-					IAccountManager::PROPERTY_ADDRESS => 'invalid scope',
1035
-					'invalid property' => IAccountManager::SCOPE_LOCAL,
1036
-					IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1037
-				],
1038
-				[
1039
-					IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
1040
-					IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
1041
-					IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1042
-				]
1043
-			],
1044
-		];
1045
-	}
1046
-
1047
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultPropertyScopes')]
1048
-	public function testSetDefaultPropertyScopes(array $propertyScopes, array $expectedResultScopes): void {
1049
-		$user = $this->makeUser('steve', 'Steve Smith', '[email protected]');
1050
-		$this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn($propertyScopes);
1051
-
1052
-		$result = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
1053
-		$resultProperties = array_column($result, 'name');
1054
-
1055
-		$this->assertEmpty(array_diff($resultProperties, IAccountManager::ALLOWED_PROPERTIES), 'Building default user record returned non-allowed properties');
1056
-		foreach ($expectedResultScopes as $expectedResultScopeKey => $expectedResultScopeValue) {
1057
-			$resultScope = $result[array_search($expectedResultScopeKey, $resultProperties)]['scope'];
1058
-			$this->assertEquals($expectedResultScopeValue, $resultScope, "The result scope doesn't follow the value set into the config or defaults correctly.");
1059
-		}
1060
-	}
324
+                    ],
325
+                    [
326
+                        'name' => IAccountManager::PROPERTY_PHONE,
327
+                        'value' => '+71601212123',
328
+                        'scope' => IAccountManager::SCOPE_LOCAL
329
+                    ],
330
+                    [
331
+                        'name' => IAccountManager::PROPERTY_ADDRESS,
332
+                        'value' => 'Pinapple Street 22',
333
+                        'scope' => IAccountManager::SCOPE_LOCAL
334
+                    ],
335
+                    [
336
+                        'name' => IAccountManager::PROPERTY_WEBSITE,
337
+                        'value' => 'https://emca.com',
338
+                        'scope' => IAccountManager::SCOPE_FEDERATED
339
+                    ],
340
+                    [
341
+                        'name' => IAccountManager::PROPERTY_ORGANISATION,
342
+                        'value' => 'Organisation A',
343
+                        'scope' => IAccountManager::SCOPE_LOCAL
344
+                    ],
345
+                    [
346
+                        'name' => IAccountManager::PROPERTY_ROLE,
347
+                        'value' => 'Animal',
348
+                        'scope' => IAccountManager::SCOPE_LOCAL
349
+                    ],
350
+                    [
351
+                        'name' => IAccountManager::PROPERTY_HEADLINE,
352
+                        'value' => 'My headline',
353
+                        'scope' => IAccountManager::SCOPE_LOCAL
354
+                    ],
355
+                    [
356
+                        'name' => IAccountManager::PROPERTY_BIOGRAPHY,
357
+                        'value' => 'Short biography',
358
+                        'scope' => IAccountManager::SCOPE_LOCAL
359
+                    ],
360
+                    [
361
+                        'name' => IAccountManager::COLLECTION_EMAIL,
362
+                        'value' => '[email protected]',
363
+                        'scope' => IAccountManager::SCOPE_LOCAL
364
+                    ],
365
+                    [
366
+                        'name' => IAccountManager::COLLECTION_EMAIL,
367
+                        'value' => '[email protected]',
368
+                        'scope' => IAccountManager::SCOPE_LOCAL
369
+                    ],
370
+                ],
371
+            ],
372
+            [
373
+                'user' => $this->makeUser('[email protected]', 'Goodpal, Kim', '[email protected]'),
374
+                'data' => [
375
+                    [
376
+                        'name' => IAccountManager::PROPERTY_DISPLAYNAME,
377
+                        'value' => 'Goodpal, Kim',
378
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
379
+                    ],
380
+                    [
381
+                        'name' => IAccountManager::PROPERTY_EMAIL,
382
+                        'value' => '[email protected]',
383
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
384
+                    ],
385
+                    [
386
+                        'name' => IAccountManager::PROPERTY_TWITTER,
387
+                        'value' => '',
388
+                        'scope' => IAccountManager::SCOPE_LOCAL
389
+                    ],
390
+                    [
391
+                        'name' => IAccountManager::PROPERTY_FEDIVERSE,
392
+                        'value' => '',
393
+                        'scope' => IAccountManager::SCOPE_LOCAL
394
+                    ],
395
+                    [
396
+                        'name' => IAccountManager::PROPERTY_PHONE,
397
+                        'value' => '+71602121231',
398
+                        'scope' => IAccountManager::SCOPE_FEDERATED
399
+                    ],
400
+                    [
401
+                        'name' => IAccountManager::PROPERTY_ADDRESS,
402
+                        'value' => 'Octopus Ave 17',
403
+                        'scope' => IAccountManager::SCOPE_FEDERATED
404
+                    ],
405
+                    [
406
+                        'name' => IAccountManager::PROPERTY_WEBSITE,
407
+                        'value' => 'https://elpmaxe.org',
408
+                        'scope' => IAccountManager::SCOPE_PUBLISHED
409
+                    ],
410
+                    [
411
+                        'name' => IAccountManager::PROPERTY_ORGANISATION,
412
+                        'value' => 'Organisation B',
413
+                        'scope' => IAccountManager::SCOPE_FEDERATED
414
+                    ],
415
+                    [
416
+                        'name' => IAccountManager::PROPERTY_ROLE,
417
+                        'value' => 'Organism',
418
+                        'scope' => IAccountManager::SCOPE_FEDERATED
419
+                    ],
420
+                    [
421
+                        'name' => IAccountManager::PROPERTY_HEADLINE,
422
+                        'value' => 'Best headline',
423
+                        'scope' => IAccountManager::SCOPE_FEDERATED
424
+                    ],
425
+                    [
426
+                        'name' => IAccountManager::PROPERTY_BIOGRAPHY,
427
+                        'value' => 'Autobiography',
428
+                        'scope' => IAccountManager::SCOPE_FEDERATED
429
+                    ],
430
+                ],
431
+            ],
432
+        ];
433
+        $this->config->expects($this->exactly(count($users)))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
434
+        foreach ($users as $userInfo) {
435
+            $this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], null, false]);
436
+        }
437
+    }
438
+
439
+    /**
440
+     * get a instance of the accountManager
441
+     *
442
+     * @return MockObject | AccountManager
443
+     */
444
+    public function getInstance(?array $mockedMethods = null) {
445
+        return $this->getMockBuilder(AccountManager::class)
446
+            ->setConstructorArgs([
447
+                $this->connection,
448
+                $this->config,
449
+                $this->eventDispatcher,
450
+                $this->jobList,
451
+                $this->logger,
452
+                $this->verificationToken,
453
+                $this->mailer,
454
+                $this->defaults,
455
+                $this->l10nFactory,
456
+                $this->urlGenerator,
457
+                $this->crypto,
458
+                $this->phoneNumberUtil,
459
+                $this->clientService,
460
+            ])
461
+            ->onlyMethods($mockedMethods)
462
+            ->getMock();
463
+    }
464
+
465
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')]
466
+    public function testUpdateUser(array $newData, array $oldData, bool $insertNew, bool $updateExisting): void {
467
+        $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']);
468
+        /** @var IUser $user */
469
+        $user = $this->createMock(IUser::class);
470
+
471
+        if ($updateExisting) {
472
+            $accountManager->expects($this->once())->method('updateExistingUser')
473
+                ->with($user, $newData);
474
+            $accountManager->expects($this->never())->method('insertNewUser');
475
+        }
476
+        if ($insertNew) {
477
+            $accountManager->expects($this->once())->method('insertNewUser')
478
+                ->with($user, $newData);
479
+            $accountManager->expects($this->never())->method('updateExistingUser');
480
+        }
481
+
482
+        if (!$insertNew && !$updateExisting) {
483
+            $accountManager->expects($this->never())->method('updateExistingUser');
484
+            $accountManager->expects($this->never())->method('insertNewUser');
485
+            $this->eventDispatcher->expects($this->never())->method('dispatchTyped');
486
+        } else {
487
+            $this->eventDispatcher->expects($this->once())->method('dispatchTyped')
488
+                ->willReturnCallback(
489
+                    function ($event) use ($user, $newData): void {
490
+                        $this->assertInstanceOf(UserUpdatedEvent::class, $event);
491
+                        $this->assertSame($user, $event->getUser());
492
+                        $this->assertSame($newData, $event->getData());
493
+                    }
494
+                );
495
+        }
496
+
497
+        $this->invokePrivate($accountManager, 'updateUser', [$user, $newData, $oldData]);
498
+    }
499
+
500
+    public static function dataTrueFalse(): array {
501
+        return [
502
+            #$newData | $oldData | $insertNew | $updateExisting
503
+            [['myProperty' => ['value' => 'newData']], ['myProperty' => ['value' => 'oldData']], false, true],
504
+            [['myProperty' => ['value' => 'oldData']], ['myProperty' => ['value' => 'oldData']], false, false]
505
+        ];
506
+    }
507
+
508
+    public function testAddMissingDefaults(): void {
509
+        $user = $this->createMock(IUser::class);
510
+
511
+        $this->config
512
+            ->expects($this->once())
513
+            ->method('getAppValue')
514
+            ->with('settings', 'profile_enabled_by_default', '1')
515
+            ->willReturn('1');
516
+
517
+        $input = [
518
+            [
519
+                'name' => IAccountManager::PROPERTY_DISPLAYNAME,
520
+                'value' => 'bob',
521
+                'verified' => IAccountManager::NOT_VERIFIED,
522
+            ],
523
+            [
524
+                'name' => IAccountManager::PROPERTY_EMAIL,
525
+                'value' => '[email protected]',
526
+            ],
527
+        ];
528
+
529
+        $expected = [
530
+            [
531
+                'name' => IAccountManager::PROPERTY_DISPLAYNAME,
532
+                'value' => 'bob',
533
+                'scope' => IAccountManager::SCOPE_FEDERATED,
534
+                'verified' => IAccountManager::NOT_VERIFIED,
535
+            ],
536
+
537
+            [
538
+                'name' => IAccountManager::PROPERTY_EMAIL,
539
+                'value' => '[email protected]',
540
+                'scope' => IAccountManager::SCOPE_FEDERATED,
541
+                'verified' => IAccountManager::NOT_VERIFIED,
542
+            ],
543
+
544
+            [
545
+                'name' => IAccountManager::PROPERTY_ADDRESS,
546
+                'value' => '',
547
+                'scope' => IAccountManager::SCOPE_LOCAL,
548
+                'verified' => IAccountManager::NOT_VERIFIED,
549
+            ],
550
+
551
+            [
552
+                'name' => IAccountManager::PROPERTY_WEBSITE,
553
+                'value' => '',
554
+                'scope' => IAccountManager::SCOPE_LOCAL,
555
+                'verified' => IAccountManager::NOT_VERIFIED,
556
+            ],
557
+
558
+            [
559
+                'name' => IAccountManager::PROPERTY_AVATAR,
560
+                'scope' => IAccountManager::SCOPE_FEDERATED
561
+            ],
562
+
563
+            [
564
+                'name' => IAccountManager::PROPERTY_PHONE,
565
+                'value' => '',
566
+                'scope' => IAccountManager::SCOPE_LOCAL,
567
+                'verified' => IAccountManager::NOT_VERIFIED,
568
+            ],
569
+
570
+            [
571
+                'name' => IAccountManager::PROPERTY_TWITTER,
572
+                'value' => '',
573
+                'scope' => IAccountManager::SCOPE_LOCAL,
574
+                'verified' => IAccountManager::NOT_VERIFIED,
575
+            ],
576
+
577
+            [
578
+                'name' => IAccountManager::PROPERTY_BLUESKY,
579
+                'value' => '',
580
+                'scope' => IAccountManager::SCOPE_LOCAL,
581
+                'verified' => IAccountManager::NOT_VERIFIED,
582
+            ],
583
+
584
+            [
585
+                'name' => IAccountManager::PROPERTY_FEDIVERSE,
586
+                'value' => '',
587
+                'scope' => IAccountManager::SCOPE_LOCAL,
588
+                'verified' => IAccountManager::NOT_VERIFIED,
589
+            ],
590
+
591
+            [
592
+                'name' => IAccountManager::PROPERTY_ORGANISATION,
593
+                'value' => '',
594
+                'scope' => IAccountManager::SCOPE_LOCAL,
595
+            ],
596
+
597
+            [
598
+                'name' => IAccountManager::PROPERTY_ROLE,
599
+                'value' => '',
600
+                'scope' => IAccountManager::SCOPE_LOCAL,
601
+            ],
602
+
603
+            [
604
+                'name' => IAccountManager::PROPERTY_HEADLINE,
605
+                'value' => '',
606
+                'scope' => IAccountManager::SCOPE_LOCAL,
607
+            ],
608
+
609
+            [
610
+                'name' => IAccountManager::PROPERTY_BIOGRAPHY,
611
+                'value' => '',
612
+                'scope' => IAccountManager::SCOPE_LOCAL,
613
+            ],
614
+
615
+            [
616
+                'name' => IAccountManager::PROPERTY_BIRTHDATE,
617
+                'value' => '',
618
+                'scope' => IAccountManager::SCOPE_LOCAL,
619
+            ],
620
+
621
+            [
622
+                'name' => IAccountManager::PROPERTY_PROFILE_ENABLED,
623
+                'value' => '1',
624
+            ],
625
+
626
+            [
627
+                'name' => IAccountManager::PROPERTY_PRONOUNS,
628
+                'value' => '',
629
+                'scope' => IAccountManager::SCOPE_FEDERATED,
630
+            ],
631
+        ];
632
+        $this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
633
+
634
+        $defaultUserRecord = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
635
+        $result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input, $defaultUserRecord]);
636
+
637
+        $this->assertSame($expected, $result);
638
+    }
639
+
640
+    public function testGetAccount(): void {
641
+        $accountManager = $this->getInstance(['getUser']);
642
+        /** @var IUser $user */
643
+        $user = $this->createMock(IUser::class);
644
+
645
+        $data = [
646
+            [
647
+                'value' => '@twitterhandle',
648
+                'scope' => IAccountManager::SCOPE_LOCAL,
649
+                'verified' => IAccountManager::NOT_VERIFIED,
650
+                'name' => IAccountManager::PROPERTY_TWITTER,
651
+            ],
652
+            [
653
+                'value' => '@[email protected]',
654
+                'scope' => IAccountManager::SCOPE_LOCAL,
655
+                'verified' => IAccountManager::NOT_VERIFIED,
656
+                'name' => IAccountManager::PROPERTY_FEDIVERSE,
657
+            ],
658
+            [
659
+                'value' => '[email protected]',
660
+                'scope' => IAccountManager::SCOPE_PUBLISHED,
661
+                'verified' => IAccountManager::VERIFICATION_IN_PROGRESS,
662
+                'name' => IAccountManager::PROPERTY_EMAIL,
663
+            ],
664
+            [
665
+                'value' => 'https://example.com',
666
+                'scope' => IAccountManager::SCOPE_FEDERATED,
667
+                'verified' => IAccountManager::VERIFIED,
668
+                'name' => IAccountManager::PROPERTY_WEBSITE,
669
+            ],
670
+        ];
671
+        $expected = new Account($user);
672
+        $expected->setProperty(IAccountManager::PROPERTY_TWITTER, '@twitterhandle', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
673
+        $expected->setProperty(IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
674
+        $expected->setProperty(IAccountManager::PROPERTY_EMAIL, '[email protected]', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS);
675
+        $expected->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED);
676
+
677
+        $accountManager->expects($this->once())
678
+            ->method('getUser')
679
+            ->willReturn($data);
680
+        $this->assertEquals($expected, $accountManager->getAccount($user));
681
+    }
682
+
683
+    public static function dataParsePhoneNumber(): array {
684
+        return [
685
+            ['0711 / 25 24 28-90', 'DE', '+4971125242890'],
686
+            ['0711 / 25 24 28-90', '', null],
687
+            ['+49 711 / 25 24 28-90', '', '+4971125242890'],
688
+        ];
689
+    }
690
+
691
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataParsePhoneNumber')]
692
+    public function testSanitizePhoneNumberOnUpdateAccount(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void {
693
+        $this->config->method('getSystemValueString')
694
+            ->willReturn($defaultRegion);
695
+
696
+        $user = $this->createMock(IUser::class);
697
+        $account = new Account($user);
698
+        $account->setProperty(IAccountManager::PROPERTY_PHONE, $phoneInput, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
699
+        $manager = $this->getInstance(['getUser', 'updateUser']);
700
+        $manager->method('getUser')
701
+            ->with($user, false)
702
+            ->willReturn([]);
703
+        $manager->expects($phoneNumber === null ? self::never() : self::once())
704
+            ->method('updateUser');
705
+
706
+        if ($phoneNumber === null) {
707
+            $this->expectException(\InvalidArgumentException::class);
708
+        }
709
+
710
+        $manager->updateAccount($account);
711
+
712
+        if ($phoneNumber !== null) {
713
+            self::assertEquals($phoneNumber, $account->getProperty(IAccountManager::PROPERTY_PHONE)->getValue());
714
+        }
715
+    }
716
+
717
+    public static function dataSanitizeOnUpdate(): array {
718
+        return [
719
+            [IAccountManager::PROPERTY_WEBSITE, 'https://nextcloud.com', 'https://nextcloud.com'],
720
+            [IAccountManager::PROPERTY_WEBSITE, 'http://nextcloud.com', 'http://nextcloud.com'],
721
+            [IAccountManager::PROPERTY_WEBSITE, 'ftp://nextcloud.com', null],
722
+            [IAccountManager::PROPERTY_WEBSITE, '//nextcloud.com/', null],
723
+            [IAccountManager::PROPERTY_WEBSITE, 'https:///?query', null],
724
+
725
+            [IAccountManager::PROPERTY_TWITTER, '@nextcloud', 'nextcloud'],
726
+            [IAccountManager::PROPERTY_TWITTER, '_nextcloud', '_nextcloud'],
727
+            [IAccountManager::PROPERTY_TWITTER, 'FooB4r', 'FooB4r'],
728
+            [IAccountManager::PROPERTY_TWITTER, 'X', null],
729
+            [IAccountManager::PROPERTY_TWITTER, 'next.cloud', null],
730
+            [IAccountManager::PROPERTY_TWITTER, 'ab/cd.zip', null],
731
+            [IAccountManager::PROPERTY_TWITTER, 'tooLongForTwitterAndX', null],
732
+
733
+            [IAccountManager::PROPERTY_FEDIVERSE, '[email protected]', '[email protected]'],
734
+            [IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', '[email protected]'],
735
+            [IAccountManager::PROPERTY_FEDIVERSE, '[email protected]', '[email protected]'],
736
+            [IAccountManager::PROPERTY_FEDIVERSE, 'invalid/[email protected]', null],
737
+            [IAccountManager::PROPERTY_FEDIVERSE, '[email protected]/malware.exe', null],
738
+            [IAccountManager::PROPERTY_FEDIVERSE, '@is-it-a-host-or-name', null],
739
+            [IAccountManager::PROPERTY_FEDIVERSE, 'only-a-name', null],
740
+        ];
741
+    }
742
+
743
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeOnUpdate')]
744
+    public function testSanitizingOnUpdateAccount(string $property, string $input, ?string $output): void {
745
+
746
+        if ($property === IAccountManager::PROPERTY_FEDIVERSE) {
747
+            // We do not test the server response here we do this in the `testSanitizingFediverseServer`
748
+            $this->config
749
+                ->method('getSystemValueBool')
750
+                ->with('has_internet_connection', true)
751
+                ->willReturn(false);
752
+        }
753
+
754
+        $user = $this->createMock(IUser::class);
755
+
756
+        $account = new Account($user);
757
+        $account->setProperty($property, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
758
+
759
+        $manager = $this->getInstance(['getUser', 'updateUser']);
760
+        $manager->method('getUser')
761
+            ->with($user, false)
762
+            ->willReturn([]);
763
+        $manager->expects($output === null ? self::never() : self::once())
764
+            ->method('updateUser');
765
+
766
+        if ($output === null) {
767
+            $this->expectException(\InvalidArgumentException::class);
768
+            $this->expectExceptionMessage($property);
769
+        }
770
+
771
+        $manager->updateAccount($account);
772
+
773
+        if ($output !== null) {
774
+            self::assertEquals($output, $account->getProperty($property)->getValue());
775
+        }
776
+    }
777
+
778
+    public static function dataSanitizeFediverseServer(): array {
779
+        return [
780
+            'no internet' => [
781
+                '@[email protected]',
782
+                '[email protected]',
783
+                false,
784
+                null,
785
+            ],
786
+            'no internet - no at' => [
787
+                '[email protected]',
788
+                '[email protected]',
789
+                false,
790
+                null,
791
+            ],
792
+            'valid response' => [
793
+                '@[email protected]',
794
+                '[email protected]',
795
+                true,
796
+                json_encode([
797
+                    'subject' => 'acct:[email protected]',
798
+                    'links' => [
799
+                        [
800
+                            'rel' => 'self',
801
+                            'type' => 'application/activity+json',
802
+                            'href' => 'https://example.com/users/foo',
803
+                        ],
804
+                    ],
805
+                ]),
806
+            ],
807
+            'valid response - no at' => [
808
+                '[email protected]',
809
+                '[email protected]',
810
+                true,
811
+                json_encode([
812
+                    'subject' => 'acct:[email protected]',
813
+                    'links' => [
814
+                        [
815
+                            'rel' => 'self',
816
+                            'type' => 'application/activity+json',
817
+                            'href' => 'https://example.com/users/foo',
818
+                        ],
819
+                    ],
820
+                ]),
821
+            ],
822
+            // failures
823
+            'invalid response' => [
824
+                '@[email protected]',
825
+                null,
826
+                true,
827
+                json_encode([
828
+                    'subject' => 'acct:[email protected]',
829
+                    'links' => [],
830
+                ]),
831
+            ],
832
+            'no response' => [
833
+                '@[email protected]',
834
+                null,
835
+                true,
836
+                null,
837
+            ],
838
+            'wrong user' => [
839
+                '@[email protected]',
840
+                null,
841
+                true,
842
+                json_encode([
843
+                    'links' => [],
844
+                ]),
845
+            ],
846
+        ];
847
+    }
848
+
849
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeFediverseServer')]
850
+    public function testSanitizingFediverseServer(string $input, ?string $output, bool $hasInternet, ?string $serverResponse): void {
851
+        $this->config->expects(self::once())
852
+            ->method('getSystemValueBool')
853
+            ->with('has_internet_connection', true)
854
+            ->willReturn($hasInternet);
855
+
856
+        if ($hasInternet) {
857
+            $client = $this->createMock(IClient::class);
858
+            if ($serverResponse !== null) {
859
+                $response = $this->createMock(IResponse::class);
860
+                $response->method('getBody')
861
+                    ->willReturn($serverResponse);
862
+                $client->expects(self::once())
863
+                    ->method('get')
864
+                    ->with('https://example.com/.well-known/webfinger?resource=acct:[email protected]')
865
+                    ->willReturn($response);
866
+            } else {
867
+                $client->expects(self::once())
868
+                    ->method('get')
869
+                    ->with('https://example.com/.well-known/webfinger?resource=acct:[email protected]')
870
+                    ->willThrowException(new \Exception('404'));
871
+            }
872
+
873
+            $this->clientService
874
+                ->expects(self::once())
875
+                ->method('newClient')
876
+                ->willReturn($client);
877
+        } else {
878
+            $this->clientService
879
+                ->expects(self::never())
880
+                ->method('newClient');
881
+        }
882
+
883
+        $user = $this->createMock(IUser::class);
884
+        $account = new Account($user);
885
+        $account->setProperty(IAccountManager::PROPERTY_FEDIVERSE, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
886
+
887
+        $manager = $this->getInstance(['getUser', 'updateUser']);
888
+        $manager->method('getUser')
889
+            ->with($user, false)
890
+            ->willReturn([]);
891
+        $manager->expects($output === null ? self::never() : self::once())
892
+            ->method('updateUser');
893
+
894
+        if ($output === null) {
895
+            $this->expectException(\InvalidArgumentException::class);
896
+            $this->expectExceptionMessage(IAccountManager::PROPERTY_FEDIVERSE);
897
+        }
898
+
899
+        $manager->updateAccount($account);
900
+
901
+        if ($output !== null) {
902
+            self::assertEquals($output, $account->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue());
903
+        }
904
+    }
905
+
906
+    #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
907
+    public function testSearchUsers(string $property, array $values, array $expected): void {
908
+        $this->populateOrUpdate();
909
+
910
+        $matchedUsers = $this->accountManager->searchUsers($property, $values);
911
+        foreach ($expected as $expectedEntry) {
912
+            $this->assertContains($expectedEntry, $matchedUsers);
913
+        }
914
+        if (empty($expected)) {
915
+            $this->assertEmpty($matchedUsers);
916
+        }
917
+    }
918
+
919
+    public static function searchDataProvider(): array {
920
+        return [
921
+            [ #0 Search for an existing name
922
+                IAccountManager::PROPERTY_DISPLAYNAME,
923
+                ['Jane Doe'],
924
+                ['Jane Doe' => 'j.doe']
925
+            ],
926
+            [ #1 Search for part of a name (no result)
927
+                IAccountManager::PROPERTY_DISPLAYNAME,
928
+                ['Jane'],
929
+                []
930
+            ],
931
+            [ #2 Search for part of a name (no result, test wildcard)
932
+                IAccountManager::PROPERTY_DISPLAYNAME,
933
+                ['Jane%'],
934
+                []
935
+            ],
936
+            [ #3 Search for phone
937
+                IAccountManager::PROPERTY_PHONE,
938
+                ['+491603121212'],
939
+                ['+491603121212' => 'b32c5a5b-1084-4380-8856-e5223b16de9f'],
940
+            ],
941
+            [ #4 Search for twitter handles
942
+                IAccountManager::PROPERTY_TWITTER,
943
+                ['@sometwitter', '@a_alice', '@unseen'],
944
+                ['@sometwitter' => 'j.doe', '@a_alice' => 'a.allison'],
945
+            ],
946
+            [ #5 Search for email
947
+                IAccountManager::PROPERTY_EMAIL,
948
+                ['[email protected]'],
949
+                ['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
950
+            ],
951
+            [ #6 Search for email by additional email
952
+                IAccountManager::PROPERTY_EMAIL,
953
+                ['[email protected]'],
954
+                ['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
955
+            ],
956
+            [ #7 Search for additional email
957
+                IAccountManager::COLLECTION_EMAIL,
958
+                ['[email protected]', '[email protected]'],
959
+                ['[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'],
960
+            ],
961
+            [ #8 Search for email by additional email (two valid search values, but the same user)
962
+                IAccountManager::PROPERTY_EMAIL,
963
+                ['[email protected]', '[email protected]'],
964
+                [
965
+                    '[email protected]' => '31b5316a-9b57-4b17-970a-315a4cbe73eb',
966
+                ],
967
+            ],
968
+        ];
969
+    }
970
+
971
+    public static function dataCheckEmailVerification(): array {
972
+        return [
973
+            [['steve', 'Steve Smith', '[email protected]'], null],
974
+            [['emma', 'Emma Morales', '[email protected]'], '[email protected]'],
975
+            [['[email protected]', 'Sarah Foster', '[email protected]'], null],
976
+            [['[email protected]', 'Cole Harrison', '[email protected]'], '[email protected]'],
977
+            [['8d29e358-cf69-4849-bbf9-28076c0b908b', 'Alice McPherson', '[email protected]'], '[email protected]'],
978
+            [['11da2744-3f4d-4c17-8c13-4c057a379237', 'James Loranger', '[email protected]'], ''],
979
+        ];
980
+    }
981
+
982
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckEmailVerification')]
983
+    public function testCheckEmailVerification(array $userData, ?string $newEmail): void {
984
+        $user = $this->makeUser(...$userData);
985
+        // Once because of getAccount, once because of getUser
986
+        $this->config->expects($this->exactly(2))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]);
987
+        $account = $this->accountManager->getAccount($user);
988
+        $emailUpdated = false;
989
+
990
+        if (!empty($newEmail)) {
991
+            $account->getProperty(IAccountManager::PROPERTY_EMAIL)->setValue($newEmail);
992
+            $emailUpdated = true;
993
+        }
994
+
995
+        if ($emailUpdated) {
996
+            $this->jobList->expects($this->once())
997
+                ->method('add')
998
+                ->with(VerifyUserData::class);
999
+        } else {
1000
+            $this->jobList->expects($this->never())
1001
+                ->method('add')
1002
+                ->with(VerifyUserData::class);
1003
+        }
1004
+
1005
+        /** @var array $oldData */
1006
+        $oldData = $this->invokePrivate($this->accountManager, 'getUser', [$user, false]);
1007
+        $this->invokePrivate($this->accountManager, 'checkEmailVerification', [$account, $oldData]);
1008
+    }
1009
+
1010
+    public static function dataSetDefaultPropertyScopes(): array {
1011
+        return [
1012
+            [
1013
+                [],
1014
+                [
1015
+                    IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1016
+                    IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
1017
+                    IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
1018
+                    IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_LOCAL,
1019
+                ]
1020
+            ],
1021
+            [
1022
+                [
1023
+                    IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1024
+                    IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
1025
+                    IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1026
+                ], [
1027
+                    IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED,
1028
+                    IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL,
1029
+                    IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1030
+                ]
1031
+            ],
1032
+            [
1033
+                [
1034
+                    IAccountManager::PROPERTY_ADDRESS => 'invalid scope',
1035
+                    'invalid property' => IAccountManager::SCOPE_LOCAL,
1036
+                    IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1037
+                ],
1038
+                [
1039
+                    IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL,
1040
+                    IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED,
1041
+                    IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE,
1042
+                ]
1043
+            ],
1044
+        ];
1045
+    }
1046
+
1047
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultPropertyScopes')]
1048
+    public function testSetDefaultPropertyScopes(array $propertyScopes, array $expectedResultScopes): void {
1049
+        $user = $this->makeUser('steve', 'Steve Smith', '[email protected]');
1050
+        $this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn($propertyScopes);
1051
+
1052
+        $result = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]);
1053
+        $resultProperties = array_column($result, 'name');
1054
+
1055
+        $this->assertEmpty(array_diff($resultProperties, IAccountManager::ALLOWED_PROPERTIES), 'Building default user record returned non-allowed properties');
1056
+        foreach ($expectedResultScopes as $expectedResultScopeKey => $expectedResultScopeValue) {
1057
+            $resultScope = $result[array_search($expectedResultScopeKey, $resultProperties)]['scope'];
1058
+            $this->assertEquals($expectedResultScopeValue, $resultScope, "The result scope doesn't follow the value set into the config or defaults correctly.");
1059
+        }
1060
+    }
1061 1061
 }
Please login to merge, or discard this patch.
apps/settings/tests/Controller/UsersControllerTest.php 1 patch
Indentation   +951 added lines, -951 removed lines patch added patch discarded remove patch
@@ -42,955 +42,955 @@
 block discarded – undo
42 42
  * @package Tests\Settings\Controller
43 43
  */
44 44
 class UsersControllerTest extends \Test\TestCase {
45
-	private IGroupManager&MockObject $groupManager;
46
-	private UserManager&MockObject $userManager;
47
-	private IUserSession&MockObject $userSession;
48
-	private IConfig&MockObject $config;
49
-	private IMailer&MockObject $mailer;
50
-	private IFactory&MockObject $l10nFactory;
51
-	private IAppManager&MockObject $appManager;
52
-	private IL10N&MockObject $l;
53
-	private AccountManager&MockObject $accountManager;
54
-	private IJobList&MockObject $jobList;
55
-	private \OC\Security\IdentityProof\Manager&MockObject $securityManager;
56
-	private IManager&MockObject $encryptionManager;
57
-	private KnownUserService&MockObject $knownUserService;
58
-	private IEncryptionModule&MockObject $encryptionModule;
59
-	private IEventDispatcher&MockObject $dispatcher;
60
-	private IInitialState&MockObject $initialState;
61
-
62
-	protected function setUp(): void {
63
-		parent::setUp();
64
-
65
-		$this->userManager = $this->createMock(UserManager::class);
66
-		$this->groupManager = $this->createMock(Manager::class);
67
-		$this->userSession = $this->createMock(IUserSession::class);
68
-		$this->config = $this->createMock(IConfig::class);
69
-		$this->l = $this->createMock(IL10N::class);
70
-		$this->mailer = $this->createMock(IMailer::class);
71
-		$this->l10nFactory = $this->createMock(IFactory::class);
72
-		$this->appManager = $this->createMock(IAppManager::class);
73
-		$this->accountManager = $this->createMock(AccountManager::class);
74
-		$this->securityManager = $this->createMock(\OC\Security\IdentityProof\Manager::class);
75
-		$this->jobList = $this->createMock(IJobList::class);
76
-		$this->encryptionManager = $this->createMock(IManager::class);
77
-		$this->knownUserService = $this->createMock(KnownUserService::class);
78
-		$this->dispatcher = $this->createMock(IEventDispatcher::class);
79
-		$this->initialState = $this->createMock(IInitialState::class);
80
-
81
-		$this->l->method('t')
82
-			->willReturnCallback(function ($text, $parameters = []) {
83
-				return vsprintf($text, $parameters);
84
-			});
85
-
86
-		$this->encryptionModule = $this->createMock(IEncryptionModule::class);
87
-		$this->encryptionManager->expects($this->any())->method('getEncryptionModules')
88
-			->willReturn(['encryptionModule' => ['callback' => function () {
89
-				return $this->encryptionModule;
90
-			}]]);
91
-	}
92
-
93
-	/**
94
-	 * @param bool $isAdmin
95
-	 * @return UsersController|MockObject
96
-	 */
97
-	protected function getController(bool $isAdmin = false, array $mockedMethods = []) {
98
-		$this->groupManager->expects($this->any())
99
-			->method('isAdmin')
100
-			->willReturn($isAdmin);
101
-
102
-		if (empty($mockedMethods)) {
103
-			return new UsersController(
104
-				'settings',
105
-				$this->createMock(IRequest::class),
106
-				$this->userManager,
107
-				$this->groupManager,
108
-				$this->userSession,
109
-				$this->config,
110
-				$this->l,
111
-				$this->mailer,
112
-				$this->l10nFactory,
113
-				$this->appManager,
114
-				$this->accountManager,
115
-				$this->securityManager,
116
-				$this->jobList,
117
-				$this->encryptionManager,
118
-				$this->knownUserService,
119
-				$this->dispatcher,
120
-				$this->initialState,
121
-			);
122
-		} else {
123
-			return $this->getMockBuilder(UsersController::class)
124
-				->setConstructorArgs(
125
-					[
126
-						'settings',
127
-						$this->createMock(IRequest::class),
128
-						$this->userManager,
129
-						$this->groupManager,
130
-						$this->userSession,
131
-						$this->config,
132
-						$this->l,
133
-						$this->mailer,
134
-						$this->l10nFactory,
135
-						$this->appManager,
136
-						$this->accountManager,
137
-						$this->securityManager,
138
-						$this->jobList,
139
-						$this->encryptionManager,
140
-						$this->knownUserService,
141
-						$this->dispatcher,
142
-						$this->initialState,
143
-					]
144
-				)
145
-				->onlyMethods($mockedMethods)
146
-				->getMock();
147
-		}
148
-	}
149
-
150
-	protected function buildPropertyMock(string $name, string $value, string $scope, string $verified = IAccountManager::VERIFIED): MockObject {
151
-		$property = $this->createMock(IAccountProperty::class);
152
-		$property->expects($this->any())
153
-			->method('getName')
154
-			->willReturn($name);
155
-		$property->expects($this->any())
156
-			->method('getValue')
157
-			->willReturn($value);
158
-		$property->expects($this->any())
159
-			->method('getScope')
160
-			->willReturn($scope);
161
-		$property->expects($this->any())
162
-			->method('getVerified')
163
-			->willReturn($verified);
164
-
165
-		return $property;
166
-	}
167
-
168
-	protected function getDefaultAccountMock(): MockObject {
169
-		$propertyMocks = [
170
-			IAccountManager::PROPERTY_DISPLAYNAME => $this->buildPropertyMock(
171
-				IAccountManager::PROPERTY_DISPLAYNAME,
172
-				'Default display name',
173
-				IAccountManager::SCOPE_FEDERATED,
174
-			),
175
-			IAccountManager::PROPERTY_ADDRESS => $this->buildPropertyMock(
176
-				IAccountManager::PROPERTY_ADDRESS,
177
-				'Default address',
178
-				IAccountManager::SCOPE_LOCAL,
179
-			),
180
-			IAccountManager::PROPERTY_WEBSITE => $this->buildPropertyMock(
181
-				IAccountManager::PROPERTY_WEBSITE,
182
-				'Default website',
183
-				IAccountManager::SCOPE_LOCAL,
184
-			),
185
-			IAccountManager::PROPERTY_EMAIL => $this->buildPropertyMock(
186
-				IAccountManager::PROPERTY_EMAIL,
187
-				'Default email',
188
-				IAccountManager::SCOPE_FEDERATED,
189
-			),
190
-			IAccountManager::PROPERTY_AVATAR => $this->buildPropertyMock(
191
-				IAccountManager::PROPERTY_AVATAR,
192
-				'',
193
-				IAccountManager::SCOPE_FEDERATED,
194
-			),
195
-			IAccountManager::PROPERTY_PHONE => $this->buildPropertyMock(
196
-				IAccountManager::PROPERTY_PHONE,
197
-				'Default phone',
198
-				IAccountManager::SCOPE_LOCAL,
199
-			),
200
-			IAccountManager::PROPERTY_TWITTER => $this->buildPropertyMock(
201
-				IAccountManager::PROPERTY_TWITTER,
202
-				'Default twitter',
203
-				IAccountManager::SCOPE_LOCAL,
204
-			),
205
-			IAccountManager::PROPERTY_BLUESKY => $this->buildPropertyMock(
206
-				IAccountManager::PROPERTY_BLUESKY,
207
-				'Default bluesky',
208
-				IAccountManager::SCOPE_LOCAL,
209
-			),
210
-			IAccountManager::PROPERTY_FEDIVERSE => $this->buildPropertyMock(
211
-				IAccountManager::PROPERTY_FEDIVERSE,
212
-				'Default fediverse',
213
-				IAccountManager::SCOPE_LOCAL,
214
-			),
215
-			IAccountManager::PROPERTY_BIRTHDATE => $this->buildPropertyMock(
216
-				IAccountManager::PROPERTY_BIRTHDATE,
217
-				'Default birthdate',
218
-				IAccountManager::SCOPE_LOCAL,
219
-			),
220
-			IAccountManager::PROPERTY_PRONOUNS => $this->buildPropertyMock(
221
-				IAccountManager::PROPERTY_PRONOUNS,
222
-				'Default pronouns',
223
-				IAccountManager::SCOPE_LOCAL,
224
-			),
225
-		];
226
-
227
-		$account = $this->createMock(IAccount::class);
228
-		$account->expects($this->any())
229
-			->method('getProperty')
230
-			->willReturnCallback(function (string $propertyName) use ($propertyMocks) {
231
-				if (isset($propertyMocks[$propertyName])) {
232
-					return $propertyMocks[$propertyName];
233
-				}
234
-				throw new PropertyDoesNotExistException($propertyName);
235
-			});
236
-		$account->expects($this->any())
237
-			->method('getProperties')
238
-			->willReturn($propertyMocks);
239
-
240
-		return $account;
241
-	}
242
-
243
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestSetUserSettings')]
244
-	public function testSetUserSettings(string $email, bool $validEmail, int $expectedStatus): void {
245
-		$controller = $this->getController(false, ['saveUserSettings']);
246
-		$user = $this->createMock(IUser::class);
247
-		$user->method('getUID')->willReturn('johndoe');
248
-
249
-		$this->userSession->method('getUser')->willReturn($user);
250
-
251
-		if (!empty($email) && $validEmail) {
252
-			$this->mailer->expects($this->once())->method('validateMailAddress')
253
-				->willReturn($validEmail);
254
-		}
255
-
256
-		$saveData = (!empty($email) && $validEmail) || empty($email);
257
-
258
-		if ($saveData) {
259
-			$this->accountManager->expects($this->once())
260
-				->method('getAccount')
261
-				->with($user)
262
-				->willReturn($this->getDefaultAccountMock());
263
-
264
-			$controller->expects($this->once())
265
-				->method('saveUserSettings');
266
-		} else {
267
-			$controller->expects($this->never())->method('saveUserSettings');
268
-		}
269
-
270
-		$result = $controller->setUserSettings(
271
-			AccountManager::SCOPE_FEDERATED,
272
-			'displayName',
273
-			AccountManager::SCOPE_FEDERATED,
274
-			'47658468',
275
-			AccountManager::SCOPE_FEDERATED,
276
-			$email,
277
-			AccountManager::SCOPE_FEDERATED,
278
-			'nextcloud.com',
279
-			AccountManager::SCOPE_FEDERATED,
280
-			'street and city',
281
-			AccountManager::SCOPE_FEDERATED,
282
-			'@nextclouders',
283
-			AccountManager::SCOPE_FEDERATED,
284
-			'@nextclouders',
285
-			AccountManager::SCOPE_FEDERATED,
286
-			'2020-01-01',
287
-			AccountManager::SCOPE_FEDERATED,
288
-			'they/them',
289
-			AccountManager::SCOPE_FEDERATED,
290
-		);
291
-
292
-		$this->assertSame($expectedStatus, $result->getStatus());
293
-	}
294
-
295
-	public static function dataTestSetUserSettings(): array {
296
-		return [
297
-			['', true, Http::STATUS_OK],
298
-			['', false, Http::STATUS_OK],
299
-			['example.com', false, Http::STATUS_UNPROCESSABLE_ENTITY],
300
-			['[email protected]', true, Http::STATUS_OK],
301
-		];
302
-	}
303
-
304
-	public function testSetUserSettingsWhenUserDisplayNameChangeNotAllowed(): void {
305
-		$controller = $this->getController(false, ['saveUserSettings']);
306
-
307
-		$avatarScope = IAccountManager::SCOPE_PUBLISHED;
308
-		$displayName = 'Display name';
309
-		$displayNameScope = IAccountManager::SCOPE_PUBLISHED;
310
-		$phone = '47658468';
311
-		$phoneScope = IAccountManager::SCOPE_PUBLISHED;
312
-		$email = '[email protected]';
313
-		$emailScope = IAccountManager::SCOPE_PUBLISHED;
314
-		$website = 'nextcloud.com';
315
-		$websiteScope = IAccountManager::SCOPE_PUBLISHED;
316
-		$address = 'street and city';
317
-		$addressScope = IAccountManager::SCOPE_PUBLISHED;
318
-		$twitter = '@nextclouders';
319
-		$twitterScope = IAccountManager::SCOPE_PUBLISHED;
320
-		$fediverse = '@[email protected]';
321
-		$fediverseScope = IAccountManager::SCOPE_PUBLISHED;
322
-		$birtdate = '2020-01-01';
323
-		$birthdateScope = IAccountManager::SCOPE_PUBLISHED;
324
-		$pronouns = 'she/her';
325
-		$pronounsScope = IAccountManager::SCOPE_PUBLISHED;
326
-
327
-		$user = $this->createMock(IUser::class);
328
-		$user->method('getUID')->willReturn('johndoe');
329
-
330
-		$this->userSession->method('getUser')->willReturn($user);
331
-
332
-		/** @var MockObject|IAccount $userAccount */
333
-		$userAccount = $this->getDefaultAccountMock();
334
-		$this->accountManager->expects($this->once())
335
-			->method('getAccount')
336
-			->with($user)
337
-			->willReturn($userAccount);
338
-
339
-		/** @var MockObject|IAccountProperty $avatarProperty */
340
-		$avatarProperty = $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR);
341
-		$avatarProperty->expects($this->atLeastOnce())
342
-			->method('setScope')
343
-			->with($avatarScope)
344
-			->willReturnSelf();
345
-
346
-		/** @var MockObject|IAccountProperty $avatarProperty */
347
-		$avatarProperty = $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS);
348
-		$avatarProperty->expects($this->atLeastOnce())
349
-			->method('setScope')
350
-			->with($addressScope)
351
-			->willReturnSelf();
352
-		$avatarProperty->expects($this->atLeastOnce())
353
-			->method('setValue')
354
-			->with($address)
355
-			->willReturnSelf();
356
-
357
-		/** @var MockObject|IAccountProperty $emailProperty */
358
-		$emailProperty = $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL);
359
-		$emailProperty->expects($this->never())
360
-			->method('setValue');
361
-		$emailProperty->expects($this->never())
362
-			->method('setScope');
363
-
364
-		/** @var MockObject|IAccountProperty $emailProperty */
365
-		$emailProperty = $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
366
-		$emailProperty->expects($this->never())
367
-			->method('setValue');
368
-		$emailProperty->expects($this->never())
369
-			->method('setScope');
370
-
371
-		$this->config->expects($this->once())
372
-			->method('getSystemValueBool')
373
-			->with('allow_user_to_change_display_name')
374
-			->willReturn(false);
375
-
376
-		$this->appManager->expects($this->any())
377
-			->method('isEnabledForUser')
378
-			->with('federatedfilesharing')
379
-			->willReturn(true);
380
-
381
-		$this->mailer->expects($this->once())->method('validateMailAddress')
382
-			->willReturn(true);
383
-
384
-		$controller->expects($this->once())
385
-			->method('saveUserSettings');
386
-
387
-		$controller->setUserSettings(
388
-			$avatarScope,
389
-			$displayName,
390
-			$displayNameScope,
391
-			$phone,
392
-			$phoneScope,
393
-			$email,
394
-			$emailScope,
395
-			$website,
396
-			$websiteScope,
397
-			$address,
398
-			$addressScope,
399
-			$twitter,
400
-			$twitterScope,
401
-			$fediverse,
402
-			$fediverseScope,
403
-			$birtdate,
404
-			$birthdateScope,
405
-			$pronouns,
406
-			$pronounsScope,
407
-		);
408
-	}
409
-
410
-	public function testSetUserSettingsWhenFederatedFilesharingNotEnabled(): void {
411
-		$controller = $this->getController(false, ['saveUserSettings']);
412
-		$user = $this->createMock(IUser::class);
413
-		$user->method('getUID')->willReturn('johndoe');
414
-
415
-		$this->userSession->method('getUser')->willReturn($user);
416
-
417
-		$defaultProperties = []; //$this->getDefaultAccountMock();
418
-
419
-		$userAccount = $this->getDefaultAccountMock();
420
-		$this->accountManager->expects($this->once())
421
-			->method('getAccount')
422
-			->with($user)
423
-			->willReturn($userAccount);
424
-
425
-		$this->appManager->expects($this->any())
426
-			->method('isEnabledForUser')
427
-			->with('federatedfilesharing')
428
-			->willReturn(false);
429
-
430
-		$avatarScope = IAccountManager::SCOPE_PUBLISHED;
431
-		$displayName = 'Display name';
432
-		$displayNameScope = IAccountManager::SCOPE_PUBLISHED;
433
-		$phone = '47658468';
434
-		$phoneScope = IAccountManager::SCOPE_PUBLISHED;
435
-		$email = '[email protected]';
436
-		$emailScope = IAccountManager::SCOPE_PUBLISHED;
437
-		$website = 'nextcloud.com';
438
-		$websiteScope = IAccountManager::SCOPE_PUBLISHED;
439
-		$address = 'street and city';
440
-		$addressScope = IAccountManager::SCOPE_PUBLISHED;
441
-		$twitter = '@nextclouders';
442
-		$twitterScope = IAccountManager::SCOPE_PUBLISHED;
443
-		$bluesky = 'nextclouders.net';
444
-		$blueskyScope = IAccountManager::SCOPE_PUBLISHED;
445
-		$fediverse = '@[email protected]';
446
-		$fediverseScope = IAccountManager::SCOPE_PUBLISHED;
447
-		$birthdate = '2020-01-01';
448
-		$birthdateScope = IAccountManager::SCOPE_PUBLISHED;
449
-		$pronouns = 'she/her';
450
-		$pronounsScope = IAccountManager::SCOPE_PUBLISHED;
451
-
452
-		// All settings are changed (in the past phone, website, address and
453
-		// twitter were not changed).
454
-		$expectedProperties = $defaultProperties;
455
-		$expectedProperties[IAccountManager::PROPERTY_AVATAR]['scope'] = $avatarScope;
456
-		$expectedProperties[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $displayName;
457
-		$expectedProperties[IAccountManager::PROPERTY_DISPLAYNAME]['scope'] = $displayNameScope;
458
-		$expectedProperties[IAccountManager::PROPERTY_EMAIL]['value'] = $email;
459
-		$expectedProperties[IAccountManager::PROPERTY_EMAIL]['scope'] = $emailScope;
460
-		$expectedProperties[IAccountManager::PROPERTY_PHONE]['value'] = $phone;
461
-		$expectedProperties[IAccountManager::PROPERTY_PHONE]['scope'] = $phoneScope;
462
-		$expectedProperties[IAccountManager::PROPERTY_WEBSITE]['value'] = $website;
463
-		$expectedProperties[IAccountManager::PROPERTY_WEBSITE]['scope'] = $websiteScope;
464
-		$expectedProperties[IAccountManager::PROPERTY_ADDRESS]['value'] = $address;
465
-		$expectedProperties[IAccountManager::PROPERTY_ADDRESS]['scope'] = $addressScope;
466
-		$expectedProperties[IAccountManager::PROPERTY_TWITTER]['value'] = $twitter;
467
-		$expectedProperties[IAccountManager::PROPERTY_TWITTER]['scope'] = $twitterScope;
468
-		$expectedProperties[IAccountManager::PROPERTY_BLUESKY]['value'] = $bluesky;
469
-		$expectedProperties[IAccountManager::PROPERTY_BLUESKY]['scope'] = $blueskyScope;
470
-		$expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['value'] = $fediverse;
471
-		$expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['scope'] = $fediverseScope;
472
-		$expectedProperties[IAccountManager::PROPERTY_BIRTHDATE]['value'] = $birthdate;
473
-		$expectedProperties[IAccountManager::PROPERTY_BIRTHDATE]['scope'] = $birthdateScope;
474
-		$expectedProperties[IAccountManager::PROPERTY_PRONOUNS]['value'] = $pronouns;
475
-		$expectedProperties[IAccountManager::PROPERTY_PRONOUNS]['scope'] = $pronounsScope;
476
-
477
-		$this->mailer->expects($this->once())->method('validateMailAddress')
478
-			->willReturn(true);
479
-
480
-		$controller->expects($this->once())
481
-			->method('saveUserSettings')
482
-			->with($userAccount);
483
-
484
-		$controller->setUserSettings(
485
-			$avatarScope,
486
-			$displayName,
487
-			$displayNameScope,
488
-			$phone,
489
-			$phoneScope,
490
-			$email,
491
-			$emailScope,
492
-			$website,
493
-			$websiteScope,
494
-			$address,
495
-			$addressScope,
496
-			$twitter,
497
-			$twitterScope,
498
-			$bluesky,
499
-			$blueskyScope,
500
-			$fediverse,
501
-			$fediverseScope,
502
-			$birthdate,
503
-			$birthdateScope,
504
-			$pronouns,
505
-			$pronounsScope,
506
-		);
507
-	}
508
-
509
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestSetUserSettingsSubset')]
510
-	public function testSetUserSettingsSubset(string $property, string $propertyValue): void {
511
-		$controller = $this->getController(false, ['saveUserSettings']);
512
-		$user = $this->createMock(IUser::class);
513
-		$user->method('getUID')->willReturn('johndoe');
514
-
515
-		$this->userSession->method('getUser')->willReturn($user);
516
-
517
-		/** @var IAccount&MockObject $userAccount */
518
-		$userAccount = $this->getDefaultAccountMock();
519
-
520
-		$this->accountManager->expects($this->once())
521
-			->method('getAccount')
522
-			->with($user)
523
-			->willReturn($userAccount);
524
-
525
-		$avatarScope = ($property === 'avatarScope') ? $propertyValue : null;
526
-		$displayName = ($property === 'displayName') ? $propertyValue : null;
527
-		$displayNameScope = ($property === 'displayNameScope') ? $propertyValue : null;
528
-		$phone = ($property === 'phone') ? $propertyValue : null;
529
-		$phoneScope = ($property === 'phoneScope') ? $propertyValue : null;
530
-		$email = ($property === 'email') ? $propertyValue : null;
531
-		$emailScope = ($property === 'emailScope') ? $propertyValue : null;
532
-		$website = ($property === 'website') ? $propertyValue : null;
533
-		$websiteScope = ($property === 'websiteScope') ? $propertyValue : null;
534
-		$address = ($property === 'address') ? $propertyValue : null;
535
-		$addressScope = ($property === 'addressScope') ? $propertyValue : null;
536
-		$twitter = ($property === 'twitter') ? $propertyValue : null;
537
-		$twitterScope = ($property === 'twitterScope') ? $propertyValue : null;
538
-		$bluesky = ($property === 'bluesky') ? $propertyValue : null;
539
-		$blueskyScope = ($property === 'blueskyScope') ? $propertyValue : null;
540
-		$fediverse = ($property === 'fediverse') ? $propertyValue : null;
541
-		$fediverseScope = ($property === 'fediverseScope') ? $propertyValue : null;
542
-		$birthdate = ($property === 'birthdate') ? $propertyValue : null;
543
-		$birthdateScope = ($property === 'birthdateScope') ? $propertyValue : null;
544
-		$pronouns = ($property === 'pronouns') ? $propertyValue : null;
545
-		$pronounsScope = ($property === 'pronounsScope') ? $propertyValue : null;
546
-
547
-		/** @var IAccountProperty[]&MockObject[] $expectedProperties */
548
-		$expectedProperties = $userAccount->getProperties();
549
-		$isScope = strrpos($property, 'Scope') === strlen($property) - strlen('5');
550
-		switch ($property) {
551
-			case 'avatarScope':
552
-				$propertyId = IAccountManager::PROPERTY_AVATAR;
553
-				break;
554
-			case 'displayName':
555
-			case 'displayNameScope':
556
-				$propertyId = IAccountManager::PROPERTY_DISPLAYNAME;
557
-				break;
558
-			case 'phone':
559
-			case 'phoneScope':
560
-				$propertyId = IAccountManager::PROPERTY_PHONE;
561
-				break;
562
-			case 'email':
563
-			case 'emailScope':
564
-				$propertyId = IAccountManager::PROPERTY_EMAIL;
565
-				break;
566
-			case 'website':
567
-			case 'websiteScope':
568
-				$propertyId = IAccountManager::PROPERTY_WEBSITE;
569
-				break;
570
-			case 'address':
571
-			case 'addressScope':
572
-				$propertyId = IAccountManager::PROPERTY_ADDRESS;
573
-				break;
574
-			case 'twitter':
575
-			case 'twitterScope':
576
-				$propertyId = IAccountManager::PROPERTY_TWITTER;
577
-				break;
578
-			case 'bluesky':
579
-			case 'blueskyScope':
580
-				$propertyId = IAccountManager::PROPERTY_BLUESKY;
581
-				break;
582
-			case 'fediverse':
583
-			case 'fediverseScope':
584
-				$propertyId = IAccountManager::PROPERTY_FEDIVERSE;
585
-				break;
586
-			case 'birthdate':
587
-			case 'birthdateScope':
588
-				$propertyId = IAccountManager::PROPERTY_BIRTHDATE;
589
-				break;
590
-			case 'pronouns':
591
-			case 'pronounsScope':
592
-				$propertyId = IAccountManager::PROPERTY_PRONOUNS;
593
-				break;
594
-			default:
595
-				$propertyId = '404';
596
-		}
597
-		$expectedProperties[$propertyId]->expects($this->any())
598
-			->method($isScope ? 'getScope' : 'getValue')
599
-			->willReturn($propertyValue);
600
-
601
-		if (!empty($email)) {
602
-			$this->mailer->expects($this->once())->method('validateMailAddress')
603
-				->willReturn(true);
604
-		}
605
-
606
-		$controller->expects($this->once())
607
-			->method('saveUserSettings')
608
-			->with($userAccount);
609
-
610
-		$controller->setUserSettings(
611
-			$avatarScope,
612
-			$displayName,
613
-			$displayNameScope,
614
-			$phone,
615
-			$phoneScope,
616
-			$email,
617
-			$emailScope,
618
-			$website,
619
-			$websiteScope,
620
-			$address,
621
-			$addressScope,
622
-			$twitter,
623
-			$twitterScope,
624
-			$bluesky,
625
-			$blueskyScope,
626
-			$fediverse,
627
-			$fediverseScope,
628
-			$birthdate,
629
-			$birthdateScope,
630
-			$pronouns,
631
-			$pronounsScope,
632
-		);
633
-	}
634
-
635
-	public static function dataTestSetUserSettingsSubset(): array {
636
-		return [
637
-			['avatarScope', IAccountManager::SCOPE_PUBLISHED],
638
-			['displayName', 'Display name'],
639
-			['displayNameScope', IAccountManager::SCOPE_PUBLISHED],
640
-			['phone', '47658468'],
641
-			['phoneScope', IAccountManager::SCOPE_PUBLISHED],
642
-			['email', '[email protected]'],
643
-			['emailScope', IAccountManager::SCOPE_PUBLISHED],
644
-			['website', 'nextcloud.com'],
645
-			['websiteScope', IAccountManager::SCOPE_PUBLISHED],
646
-			['address', 'street and city'],
647
-			['addressScope', IAccountManager::SCOPE_PUBLISHED],
648
-			['twitter', '@nextclouders'],
649
-			['twitterScope', IAccountManager::SCOPE_PUBLISHED],
650
-			['bluesky', 'nextclouders.net'],
651
-			['blueskyScope', IAccountManager::SCOPE_PUBLISHED],
652
-			['fediverse', '@[email protected]'],
653
-			['fediverseScope', IAccountManager::SCOPE_PUBLISHED],
654
-			['birthdate', '2020-01-01'],
655
-			['birthdateScope', IAccountManager::SCOPE_PUBLISHED],
656
-			['pronouns', 'he/him'],
657
-			['pronounsScope', IAccountManager::SCOPE_PUBLISHED],
658
-		];
659
-	}
660
-
661
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestSaveUserSettings')]
662
-	public function testSaveUserSettings(array $data, ?string $oldEmailAddress, ?string $oldDisplayName): void {
663
-		$controller = $this->getController();
664
-		$user = $this->createMock(IUser::class);
665
-
666
-		$user->method('getDisplayName')->willReturn($oldDisplayName);
667
-		$user->method('getSystemEMailAddress')->willReturn($oldEmailAddress);
668
-		$user->method('canChangeDisplayName')->willReturn(true);
669
-
670
-		if (strtolower($data[IAccountManager::PROPERTY_EMAIL]['value']) === strtolower($oldEmailAddress ?? '')) {
671
-			$user->expects($this->never())->method('setSystemEMailAddress');
672
-		} else {
673
-			$user->expects($this->once())->method('setSystemEMailAddress')
674
-				->with($data[IAccountManager::PROPERTY_EMAIL]['value']);
675
-		}
676
-
677
-		if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] === $oldDisplayName ?? '') {
678
-			$user->expects($this->never())->method('setDisplayName');
679
-		} else {
680
-			$user->expects($this->once())->method('setDisplayName')
681
-				->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
682
-				->willReturn(true);
683
-		}
684
-
685
-		$properties = [];
686
-		foreach ($data as $propertyName => $propertyData) {
687
-			$properties[$propertyName] = $this->createMock(IAccountProperty::class);
688
-			$properties[$propertyName]->expects($this->any())
689
-				->method('getValue')
690
-				->willReturn($propertyData['value']);
691
-		}
692
-
693
-		$account = $this->createMock(IAccount::class);
694
-		$account->expects($this->any())
695
-			->method('getUser')
696
-			->willReturn($user);
697
-		$account->expects($this->any())
698
-			->method('getProperty')
699
-			->willReturnCallback(function (string $propertyName) use ($properties) {
700
-				return $properties[$propertyName];
701
-			});
702
-
703
-		$this->accountManager->expects($this->any())
704
-			->method('getAccount')
705
-			->willReturn($account);
706
-
707
-		$this->accountManager->expects($this->once())
708
-			->method('updateAccount')
709
-			->with($account);
710
-
711
-		$this->invokePrivate($controller, 'saveUserSettings', [$account]);
712
-	}
713
-
714
-	public static function dataTestSaveUserSettings(): array {
715
-		return [
716
-			[
717
-				[
718
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
719
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
720
-				],
721
-				'[email protected]',
722
-				'john doe'
723
-			],
724
-			[
725
-				[
726
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
727
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
728
-				],
729
-				'[email protected]',
730
-				'john New doe'
731
-			],
732
-			[
733
-				[
734
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
735
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
736
-				],
737
-				'[email protected]',
738
-				'john doe'
739
-			],
740
-			[
741
-				[
742
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
743
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
744
-				],
745
-				'[email protected]',
746
-				'john New doe'
747
-			],
748
-			[
749
-				[
750
-					IAccountManager::PROPERTY_EMAIL => ['value' => ''],
751
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
752
-				],
753
-				null,
754
-				'john New doe'
755
-			],
756
-			[
757
-				[
758
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
759
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
760
-				],
761
-				'[email protected]',
762
-				null
763
-			],
764
-			[
765
-				[
766
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
767
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
768
-				],
769
-				'[email protected]',
770
-				null
771
-			],
772
-
773
-		];
774
-	}
775
-
776
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestSaveUserSettingsException')]
777
-	public function testSaveUserSettingsException(
778
-		array $data,
779
-		string $oldEmailAddress,
780
-		string $oldDisplayName,
781
-		bool $setDisplayNameResult,
782
-		bool $canChangeEmail,
783
-	): void {
784
-		$this->expectException(ForbiddenException::class);
785
-
786
-		$controller = $this->getController();
787
-		$user = $this->createMock(IUser::class);
788
-
789
-		$user->method('getDisplayName')->willReturn($oldDisplayName);
790
-		$user->method('getEMailAddress')->willReturn($oldEmailAddress);
791
-
792
-		/** @var MockObject|IAccount $userAccount */
793
-		$userAccount = $this->createMock(IAccount::class);
794
-		$userAccount->expects($this->any())
795
-			->method('getUser')
796
-			->willReturn($user);
797
-		$propertyMocks = [];
798
-		foreach ($data as $propertyName => $propertyData) {
799
-			/** @var MockObject|IAccountProperty $property */
800
-			$propertyMocks[$propertyName] = $this->buildPropertyMock($propertyName, $propertyData['value'], '');
801
-		}
802
-		$userAccount->expects($this->any())
803
-			->method('getProperty')
804
-			->willReturnCallback(function (string $propertyName) use ($propertyMocks) {
805
-				return $propertyMocks[$propertyName];
806
-			});
807
-
808
-		if ($data[IAccountManager::PROPERTY_EMAIL]['value'] !== $oldEmailAddress) {
809
-			$user->method('canChangeDisplayName')
810
-				->willReturn($canChangeEmail);
811
-		}
812
-
813
-		if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] !== $oldDisplayName) {
814
-			$user->method('setDisplayName')
815
-				->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
816
-				->willReturn($setDisplayNameResult);
817
-		}
818
-
819
-		$this->invokePrivate($controller, 'saveUserSettings', [$userAccount]);
820
-	}
821
-
822
-
823
-	public static function dataTestSaveUserSettingsException(): array {
824
-		return [
825
-			[
826
-				[
827
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
828
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
829
-				],
830
-				'[email protected]',
831
-				'john New doe',
832
-				true,
833
-				false
834
-			],
835
-			[
836
-				[
837
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
838
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
839
-				],
840
-				'[email protected]',
841
-				'john New doe',
842
-				false,
843
-				true
844
-			],
845
-			[
846
-				[
847
-					IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
848
-					IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
849
-				],
850
-				'[email protected]',
851
-				'john New doe',
852
-				false,
853
-				false
854
-			],
855
-
856
-		];
857
-	}
858
-
859
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetVerificationCode')]
860
-	public function testGetVerificationCode(string $account, string $type, array $dataBefore, array $expectedData, bool $onlyVerificationCode): void {
861
-		$message = 'Use my Federated Cloud ID to share with me: [email protected]';
862
-		$signature = 'theSignature';
863
-
864
-		$code = $message . ' ' . $signature;
865
-		if ($type === IAccountManager::PROPERTY_TWITTER || $type === IAccountManager::PROPERTY_FEDIVERSE) {
866
-			$code = $message . ' ' . md5($signature);
867
-		}
868
-
869
-		$controller = $this->getController(false, ['signMessage', 'getCurrentTime']);
870
-
871
-		$user = $this->createMock(IUser::class);
872
-
873
-		$property = $this->buildPropertyMock($type, $dataBefore[$type]['value'], '', IAccountManager::NOT_VERIFIED);
874
-		$property->expects($this->atLeastOnce())
875
-			->method('setVerified')
876
-			->with(IAccountManager::VERIFICATION_IN_PROGRESS)
877
-			->willReturnSelf();
878
-		$property->expects($this->atLeastOnce())
879
-			->method('setVerificationData')
880
-			->with($signature)
881
-			->willReturnSelf();
882
-
883
-		$userAccount = $this->createMock(IAccount::class);
884
-		$userAccount->expects($this->any())
885
-			->method('getUser')
886
-			->willReturn($user);
887
-		$userAccount->expects($this->any())
888
-			->method('getProperty')
889
-			->willReturn($property);
890
-
891
-		$this->userSession->expects($this->once())->method('getUser')->willReturn($user);
892
-		$this->accountManager->expects($this->once())->method('getAccount')->with($user)->willReturn($userAccount);
893
-		$user->expects($this->any())->method('getCloudId')->willReturn('[email protected]');
894
-		$user->expects($this->any())->method('getUID')->willReturn('uid');
895
-		$controller->expects($this->once())->method('signMessage')->with($user, $message)->willReturn($signature);
896
-		$controller->expects($this->any())->method('getCurrentTime')->willReturn(1234567);
897
-
898
-		if ($onlyVerificationCode === false) {
899
-			$this->accountManager->expects($this->once())->method('updateAccount')->with($userAccount)->willReturnArgument(1);
900
-			$this->jobList->expects($this->once())->method('add')
901
-				->with('OCA\Settings\BackgroundJobs\VerifyUserData',
902
-					[
903
-						'verificationCode' => $code,
904
-						'data' => $dataBefore[$type]['value'],
905
-						'type' => $type,
906
-						'uid' => 'uid',
907
-						'try' => 0,
908
-						'lastRun' => 1234567
909
-					]);
910
-		}
911
-
912
-		$result = $controller->getVerificationCode($account, $onlyVerificationCode);
913
-
914
-		$data = $result->getData();
915
-		$this->assertSame(Http::STATUS_OK, $result->getStatus());
916
-		$this->assertSame($code, $data['code']);
917
-	}
918
-
919
-	public static function dataTestGetVerificationCode(): array {
920
-		$accountDataBefore = [
921
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::NOT_VERIFIED],
922
-			IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::NOT_VERIFIED, 'signature' => 'theSignature'],
923
-		];
924
-
925
-		$accountDataAfterWebsite = [
926
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'],
927
-			IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::NOT_VERIFIED, 'signature' => 'theSignature'],
928
-		];
929
-
930
-		$accountDataAfterTwitter = [
931
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::NOT_VERIFIED],
932
-			IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'],
933
-		];
934
-
935
-		return [
936
-			['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, false],
937
-			['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, false],
938
-			['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, true],
939
-			['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, true],
940
-		];
941
-	}
942
-
943
-	/**
944
-	 * test get verification code in case no valid user was given
945
-	 */
946
-	public function testGetVerificationCodeInvalidUser(): void {
947
-		$controller = $this->getController();
948
-		$this->userSession->expects($this->once())->method('getUser')->willReturn(null);
949
-		$result = $controller->getVerificationCode('account', false);
950
-
951
-		$this->assertSame(Http::STATUS_BAD_REQUEST, $result->getStatus());
952
-	}
953
-
954
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestCanAdminChangeUserPasswords')]
955
-	public function testCanAdminChangeUserPasswords(
956
-		bool $encryptionEnabled,
957
-		bool $encryptionModuleLoaded,
958
-		bool $masterKeyEnabled,
959
-		bool $expected,
960
-	): void {
961
-		$controller = $this->getController();
962
-
963
-		$this->encryptionManager->expects($this->any())
964
-			->method('isEnabled')
965
-			->willReturn($encryptionEnabled);
966
-		$this->encryptionManager->expects($this->any())
967
-			->method('getEncryptionModule')
968
-			->willReturnCallback(function () use ($encryptionModuleLoaded) {
969
-				if ($encryptionModuleLoaded) {
970
-					return $this->encryptionModule;
971
-				} else {
972
-					throw new ModuleDoesNotExistsException();
973
-				}
974
-			});
975
-		$this->encryptionModule->expects($this->any())
976
-			->method('needDetailedAccessList')
977
-			->willReturn(!$masterKeyEnabled);
978
-
979
-		$result = $this->invokePrivate($controller, 'canAdminChangeUserPasswords', []);
980
-		$this->assertSame($expected, $result);
981
-	}
982
-
983
-	public static function dataTestCanAdminChangeUserPasswords(): array {
984
-		return [
985
-			// encryptionEnabled, encryptionModuleLoaded, masterKeyEnabled, expectedResult
986
-			[true, true, true, true],
987
-			[false, true, true, true],
988
-			[true, false, true, false],
989
-			[false, false, true, true],
990
-			[true, true, false, false],
991
-			[false, true, false, false],
992
-			[true, false, false, false],
993
-			[false, false, false, true],
994
-		];
995
-	}
45
+    private IGroupManager&MockObject $groupManager;
46
+    private UserManager&MockObject $userManager;
47
+    private IUserSession&MockObject $userSession;
48
+    private IConfig&MockObject $config;
49
+    private IMailer&MockObject $mailer;
50
+    private IFactory&MockObject $l10nFactory;
51
+    private IAppManager&MockObject $appManager;
52
+    private IL10N&MockObject $l;
53
+    private AccountManager&MockObject $accountManager;
54
+    private IJobList&MockObject $jobList;
55
+    private \OC\Security\IdentityProof\Manager&MockObject $securityManager;
56
+    private IManager&MockObject $encryptionManager;
57
+    private KnownUserService&MockObject $knownUserService;
58
+    private IEncryptionModule&MockObject $encryptionModule;
59
+    private IEventDispatcher&MockObject $dispatcher;
60
+    private IInitialState&MockObject $initialState;
61
+
62
+    protected function setUp(): void {
63
+        parent::setUp();
64
+
65
+        $this->userManager = $this->createMock(UserManager::class);
66
+        $this->groupManager = $this->createMock(Manager::class);
67
+        $this->userSession = $this->createMock(IUserSession::class);
68
+        $this->config = $this->createMock(IConfig::class);
69
+        $this->l = $this->createMock(IL10N::class);
70
+        $this->mailer = $this->createMock(IMailer::class);
71
+        $this->l10nFactory = $this->createMock(IFactory::class);
72
+        $this->appManager = $this->createMock(IAppManager::class);
73
+        $this->accountManager = $this->createMock(AccountManager::class);
74
+        $this->securityManager = $this->createMock(\OC\Security\IdentityProof\Manager::class);
75
+        $this->jobList = $this->createMock(IJobList::class);
76
+        $this->encryptionManager = $this->createMock(IManager::class);
77
+        $this->knownUserService = $this->createMock(KnownUserService::class);
78
+        $this->dispatcher = $this->createMock(IEventDispatcher::class);
79
+        $this->initialState = $this->createMock(IInitialState::class);
80
+
81
+        $this->l->method('t')
82
+            ->willReturnCallback(function ($text, $parameters = []) {
83
+                return vsprintf($text, $parameters);
84
+            });
85
+
86
+        $this->encryptionModule = $this->createMock(IEncryptionModule::class);
87
+        $this->encryptionManager->expects($this->any())->method('getEncryptionModules')
88
+            ->willReturn(['encryptionModule' => ['callback' => function () {
89
+                return $this->encryptionModule;
90
+            }]]);
91
+    }
92
+
93
+    /**
94
+     * @param bool $isAdmin
95
+     * @return UsersController|MockObject
96
+     */
97
+    protected function getController(bool $isAdmin = false, array $mockedMethods = []) {
98
+        $this->groupManager->expects($this->any())
99
+            ->method('isAdmin')
100
+            ->willReturn($isAdmin);
101
+
102
+        if (empty($mockedMethods)) {
103
+            return new UsersController(
104
+                'settings',
105
+                $this->createMock(IRequest::class),
106
+                $this->userManager,
107
+                $this->groupManager,
108
+                $this->userSession,
109
+                $this->config,
110
+                $this->l,
111
+                $this->mailer,
112
+                $this->l10nFactory,
113
+                $this->appManager,
114
+                $this->accountManager,
115
+                $this->securityManager,
116
+                $this->jobList,
117
+                $this->encryptionManager,
118
+                $this->knownUserService,
119
+                $this->dispatcher,
120
+                $this->initialState,
121
+            );
122
+        } else {
123
+            return $this->getMockBuilder(UsersController::class)
124
+                ->setConstructorArgs(
125
+                    [
126
+                        'settings',
127
+                        $this->createMock(IRequest::class),
128
+                        $this->userManager,
129
+                        $this->groupManager,
130
+                        $this->userSession,
131
+                        $this->config,
132
+                        $this->l,
133
+                        $this->mailer,
134
+                        $this->l10nFactory,
135
+                        $this->appManager,
136
+                        $this->accountManager,
137
+                        $this->securityManager,
138
+                        $this->jobList,
139
+                        $this->encryptionManager,
140
+                        $this->knownUserService,
141
+                        $this->dispatcher,
142
+                        $this->initialState,
143
+                    ]
144
+                )
145
+                ->onlyMethods($mockedMethods)
146
+                ->getMock();
147
+        }
148
+    }
149
+
150
+    protected function buildPropertyMock(string $name, string $value, string $scope, string $verified = IAccountManager::VERIFIED): MockObject {
151
+        $property = $this->createMock(IAccountProperty::class);
152
+        $property->expects($this->any())
153
+            ->method('getName')
154
+            ->willReturn($name);
155
+        $property->expects($this->any())
156
+            ->method('getValue')
157
+            ->willReturn($value);
158
+        $property->expects($this->any())
159
+            ->method('getScope')
160
+            ->willReturn($scope);
161
+        $property->expects($this->any())
162
+            ->method('getVerified')
163
+            ->willReturn($verified);
164
+
165
+        return $property;
166
+    }
167
+
168
+    protected function getDefaultAccountMock(): MockObject {
169
+        $propertyMocks = [
170
+            IAccountManager::PROPERTY_DISPLAYNAME => $this->buildPropertyMock(
171
+                IAccountManager::PROPERTY_DISPLAYNAME,
172
+                'Default display name',
173
+                IAccountManager::SCOPE_FEDERATED,
174
+            ),
175
+            IAccountManager::PROPERTY_ADDRESS => $this->buildPropertyMock(
176
+                IAccountManager::PROPERTY_ADDRESS,
177
+                'Default address',
178
+                IAccountManager::SCOPE_LOCAL,
179
+            ),
180
+            IAccountManager::PROPERTY_WEBSITE => $this->buildPropertyMock(
181
+                IAccountManager::PROPERTY_WEBSITE,
182
+                'Default website',
183
+                IAccountManager::SCOPE_LOCAL,
184
+            ),
185
+            IAccountManager::PROPERTY_EMAIL => $this->buildPropertyMock(
186
+                IAccountManager::PROPERTY_EMAIL,
187
+                'Default email',
188
+                IAccountManager::SCOPE_FEDERATED,
189
+            ),
190
+            IAccountManager::PROPERTY_AVATAR => $this->buildPropertyMock(
191
+                IAccountManager::PROPERTY_AVATAR,
192
+                '',
193
+                IAccountManager::SCOPE_FEDERATED,
194
+            ),
195
+            IAccountManager::PROPERTY_PHONE => $this->buildPropertyMock(
196
+                IAccountManager::PROPERTY_PHONE,
197
+                'Default phone',
198
+                IAccountManager::SCOPE_LOCAL,
199
+            ),
200
+            IAccountManager::PROPERTY_TWITTER => $this->buildPropertyMock(
201
+                IAccountManager::PROPERTY_TWITTER,
202
+                'Default twitter',
203
+                IAccountManager::SCOPE_LOCAL,
204
+            ),
205
+            IAccountManager::PROPERTY_BLUESKY => $this->buildPropertyMock(
206
+                IAccountManager::PROPERTY_BLUESKY,
207
+                'Default bluesky',
208
+                IAccountManager::SCOPE_LOCAL,
209
+            ),
210
+            IAccountManager::PROPERTY_FEDIVERSE => $this->buildPropertyMock(
211
+                IAccountManager::PROPERTY_FEDIVERSE,
212
+                'Default fediverse',
213
+                IAccountManager::SCOPE_LOCAL,
214
+            ),
215
+            IAccountManager::PROPERTY_BIRTHDATE => $this->buildPropertyMock(
216
+                IAccountManager::PROPERTY_BIRTHDATE,
217
+                'Default birthdate',
218
+                IAccountManager::SCOPE_LOCAL,
219
+            ),
220
+            IAccountManager::PROPERTY_PRONOUNS => $this->buildPropertyMock(
221
+                IAccountManager::PROPERTY_PRONOUNS,
222
+                'Default pronouns',
223
+                IAccountManager::SCOPE_LOCAL,
224
+            ),
225
+        ];
226
+
227
+        $account = $this->createMock(IAccount::class);
228
+        $account->expects($this->any())
229
+            ->method('getProperty')
230
+            ->willReturnCallback(function (string $propertyName) use ($propertyMocks) {
231
+                if (isset($propertyMocks[$propertyName])) {
232
+                    return $propertyMocks[$propertyName];
233
+                }
234
+                throw new PropertyDoesNotExistException($propertyName);
235
+            });
236
+        $account->expects($this->any())
237
+            ->method('getProperties')
238
+            ->willReturn($propertyMocks);
239
+
240
+        return $account;
241
+    }
242
+
243
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSetUserSettings')]
244
+    public function testSetUserSettings(string $email, bool $validEmail, int $expectedStatus): void {
245
+        $controller = $this->getController(false, ['saveUserSettings']);
246
+        $user = $this->createMock(IUser::class);
247
+        $user->method('getUID')->willReturn('johndoe');
248
+
249
+        $this->userSession->method('getUser')->willReturn($user);
250
+
251
+        if (!empty($email) && $validEmail) {
252
+            $this->mailer->expects($this->once())->method('validateMailAddress')
253
+                ->willReturn($validEmail);
254
+        }
255
+
256
+        $saveData = (!empty($email) && $validEmail) || empty($email);
257
+
258
+        if ($saveData) {
259
+            $this->accountManager->expects($this->once())
260
+                ->method('getAccount')
261
+                ->with($user)
262
+                ->willReturn($this->getDefaultAccountMock());
263
+
264
+            $controller->expects($this->once())
265
+                ->method('saveUserSettings');
266
+        } else {
267
+            $controller->expects($this->never())->method('saveUserSettings');
268
+        }
269
+
270
+        $result = $controller->setUserSettings(
271
+            AccountManager::SCOPE_FEDERATED,
272
+            'displayName',
273
+            AccountManager::SCOPE_FEDERATED,
274
+            '47658468',
275
+            AccountManager::SCOPE_FEDERATED,
276
+            $email,
277
+            AccountManager::SCOPE_FEDERATED,
278
+            'nextcloud.com',
279
+            AccountManager::SCOPE_FEDERATED,
280
+            'street and city',
281
+            AccountManager::SCOPE_FEDERATED,
282
+            '@nextclouders',
283
+            AccountManager::SCOPE_FEDERATED,
284
+            '@nextclouders',
285
+            AccountManager::SCOPE_FEDERATED,
286
+            '2020-01-01',
287
+            AccountManager::SCOPE_FEDERATED,
288
+            'they/them',
289
+            AccountManager::SCOPE_FEDERATED,
290
+        );
291
+
292
+        $this->assertSame($expectedStatus, $result->getStatus());
293
+    }
294
+
295
+    public static function dataTestSetUserSettings(): array {
296
+        return [
297
+            ['', true, Http::STATUS_OK],
298
+            ['', false, Http::STATUS_OK],
299
+            ['example.com', false, Http::STATUS_UNPROCESSABLE_ENTITY],
300
+            ['[email protected]', true, Http::STATUS_OK],
301
+        ];
302
+    }
303
+
304
+    public function testSetUserSettingsWhenUserDisplayNameChangeNotAllowed(): void {
305
+        $controller = $this->getController(false, ['saveUserSettings']);
306
+
307
+        $avatarScope = IAccountManager::SCOPE_PUBLISHED;
308
+        $displayName = 'Display name';
309
+        $displayNameScope = IAccountManager::SCOPE_PUBLISHED;
310
+        $phone = '47658468';
311
+        $phoneScope = IAccountManager::SCOPE_PUBLISHED;
312
+        $email = '[email protected]';
313
+        $emailScope = IAccountManager::SCOPE_PUBLISHED;
314
+        $website = 'nextcloud.com';
315
+        $websiteScope = IAccountManager::SCOPE_PUBLISHED;
316
+        $address = 'street and city';
317
+        $addressScope = IAccountManager::SCOPE_PUBLISHED;
318
+        $twitter = '@nextclouders';
319
+        $twitterScope = IAccountManager::SCOPE_PUBLISHED;
320
+        $fediverse = '@[email protected]';
321
+        $fediverseScope = IAccountManager::SCOPE_PUBLISHED;
322
+        $birtdate = '2020-01-01';
323
+        $birthdateScope = IAccountManager::SCOPE_PUBLISHED;
324
+        $pronouns = 'she/her';
325
+        $pronounsScope = IAccountManager::SCOPE_PUBLISHED;
326
+
327
+        $user = $this->createMock(IUser::class);
328
+        $user->method('getUID')->willReturn('johndoe');
329
+
330
+        $this->userSession->method('getUser')->willReturn($user);
331
+
332
+        /** @var MockObject|IAccount $userAccount */
333
+        $userAccount = $this->getDefaultAccountMock();
334
+        $this->accountManager->expects($this->once())
335
+            ->method('getAccount')
336
+            ->with($user)
337
+            ->willReturn($userAccount);
338
+
339
+        /** @var MockObject|IAccountProperty $avatarProperty */
340
+        $avatarProperty = $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR);
341
+        $avatarProperty->expects($this->atLeastOnce())
342
+            ->method('setScope')
343
+            ->with($avatarScope)
344
+            ->willReturnSelf();
345
+
346
+        /** @var MockObject|IAccountProperty $avatarProperty */
347
+        $avatarProperty = $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS);
348
+        $avatarProperty->expects($this->atLeastOnce())
349
+            ->method('setScope')
350
+            ->with($addressScope)
351
+            ->willReturnSelf();
352
+        $avatarProperty->expects($this->atLeastOnce())
353
+            ->method('setValue')
354
+            ->with($address)
355
+            ->willReturnSelf();
356
+
357
+        /** @var MockObject|IAccountProperty $emailProperty */
358
+        $emailProperty = $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL);
359
+        $emailProperty->expects($this->never())
360
+            ->method('setValue');
361
+        $emailProperty->expects($this->never())
362
+            ->method('setScope');
363
+
364
+        /** @var MockObject|IAccountProperty $emailProperty */
365
+        $emailProperty = $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
366
+        $emailProperty->expects($this->never())
367
+            ->method('setValue');
368
+        $emailProperty->expects($this->never())
369
+            ->method('setScope');
370
+
371
+        $this->config->expects($this->once())
372
+            ->method('getSystemValueBool')
373
+            ->with('allow_user_to_change_display_name')
374
+            ->willReturn(false);
375
+
376
+        $this->appManager->expects($this->any())
377
+            ->method('isEnabledForUser')
378
+            ->with('federatedfilesharing')
379
+            ->willReturn(true);
380
+
381
+        $this->mailer->expects($this->once())->method('validateMailAddress')
382
+            ->willReturn(true);
383
+
384
+        $controller->expects($this->once())
385
+            ->method('saveUserSettings');
386
+
387
+        $controller->setUserSettings(
388
+            $avatarScope,
389
+            $displayName,
390
+            $displayNameScope,
391
+            $phone,
392
+            $phoneScope,
393
+            $email,
394
+            $emailScope,
395
+            $website,
396
+            $websiteScope,
397
+            $address,
398
+            $addressScope,
399
+            $twitter,
400
+            $twitterScope,
401
+            $fediverse,
402
+            $fediverseScope,
403
+            $birtdate,
404
+            $birthdateScope,
405
+            $pronouns,
406
+            $pronounsScope,
407
+        );
408
+    }
409
+
410
+    public function testSetUserSettingsWhenFederatedFilesharingNotEnabled(): void {
411
+        $controller = $this->getController(false, ['saveUserSettings']);
412
+        $user = $this->createMock(IUser::class);
413
+        $user->method('getUID')->willReturn('johndoe');
414
+
415
+        $this->userSession->method('getUser')->willReturn($user);
416
+
417
+        $defaultProperties = []; //$this->getDefaultAccountMock();
418
+
419
+        $userAccount = $this->getDefaultAccountMock();
420
+        $this->accountManager->expects($this->once())
421
+            ->method('getAccount')
422
+            ->with($user)
423
+            ->willReturn($userAccount);
424
+
425
+        $this->appManager->expects($this->any())
426
+            ->method('isEnabledForUser')
427
+            ->with('federatedfilesharing')
428
+            ->willReturn(false);
429
+
430
+        $avatarScope = IAccountManager::SCOPE_PUBLISHED;
431
+        $displayName = 'Display name';
432
+        $displayNameScope = IAccountManager::SCOPE_PUBLISHED;
433
+        $phone = '47658468';
434
+        $phoneScope = IAccountManager::SCOPE_PUBLISHED;
435
+        $email = '[email protected]';
436
+        $emailScope = IAccountManager::SCOPE_PUBLISHED;
437
+        $website = 'nextcloud.com';
438
+        $websiteScope = IAccountManager::SCOPE_PUBLISHED;
439
+        $address = 'street and city';
440
+        $addressScope = IAccountManager::SCOPE_PUBLISHED;
441
+        $twitter = '@nextclouders';
442
+        $twitterScope = IAccountManager::SCOPE_PUBLISHED;
443
+        $bluesky = 'nextclouders.net';
444
+        $blueskyScope = IAccountManager::SCOPE_PUBLISHED;
445
+        $fediverse = '@[email protected]';
446
+        $fediverseScope = IAccountManager::SCOPE_PUBLISHED;
447
+        $birthdate = '2020-01-01';
448
+        $birthdateScope = IAccountManager::SCOPE_PUBLISHED;
449
+        $pronouns = 'she/her';
450
+        $pronounsScope = IAccountManager::SCOPE_PUBLISHED;
451
+
452
+        // All settings are changed (in the past phone, website, address and
453
+        // twitter were not changed).
454
+        $expectedProperties = $defaultProperties;
455
+        $expectedProperties[IAccountManager::PROPERTY_AVATAR]['scope'] = $avatarScope;
456
+        $expectedProperties[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $displayName;
457
+        $expectedProperties[IAccountManager::PROPERTY_DISPLAYNAME]['scope'] = $displayNameScope;
458
+        $expectedProperties[IAccountManager::PROPERTY_EMAIL]['value'] = $email;
459
+        $expectedProperties[IAccountManager::PROPERTY_EMAIL]['scope'] = $emailScope;
460
+        $expectedProperties[IAccountManager::PROPERTY_PHONE]['value'] = $phone;
461
+        $expectedProperties[IAccountManager::PROPERTY_PHONE]['scope'] = $phoneScope;
462
+        $expectedProperties[IAccountManager::PROPERTY_WEBSITE]['value'] = $website;
463
+        $expectedProperties[IAccountManager::PROPERTY_WEBSITE]['scope'] = $websiteScope;
464
+        $expectedProperties[IAccountManager::PROPERTY_ADDRESS]['value'] = $address;
465
+        $expectedProperties[IAccountManager::PROPERTY_ADDRESS]['scope'] = $addressScope;
466
+        $expectedProperties[IAccountManager::PROPERTY_TWITTER]['value'] = $twitter;
467
+        $expectedProperties[IAccountManager::PROPERTY_TWITTER]['scope'] = $twitterScope;
468
+        $expectedProperties[IAccountManager::PROPERTY_BLUESKY]['value'] = $bluesky;
469
+        $expectedProperties[IAccountManager::PROPERTY_BLUESKY]['scope'] = $blueskyScope;
470
+        $expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['value'] = $fediverse;
471
+        $expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['scope'] = $fediverseScope;
472
+        $expectedProperties[IAccountManager::PROPERTY_BIRTHDATE]['value'] = $birthdate;
473
+        $expectedProperties[IAccountManager::PROPERTY_BIRTHDATE]['scope'] = $birthdateScope;
474
+        $expectedProperties[IAccountManager::PROPERTY_PRONOUNS]['value'] = $pronouns;
475
+        $expectedProperties[IAccountManager::PROPERTY_PRONOUNS]['scope'] = $pronounsScope;
476
+
477
+        $this->mailer->expects($this->once())->method('validateMailAddress')
478
+            ->willReturn(true);
479
+
480
+        $controller->expects($this->once())
481
+            ->method('saveUserSettings')
482
+            ->with($userAccount);
483
+
484
+        $controller->setUserSettings(
485
+            $avatarScope,
486
+            $displayName,
487
+            $displayNameScope,
488
+            $phone,
489
+            $phoneScope,
490
+            $email,
491
+            $emailScope,
492
+            $website,
493
+            $websiteScope,
494
+            $address,
495
+            $addressScope,
496
+            $twitter,
497
+            $twitterScope,
498
+            $bluesky,
499
+            $blueskyScope,
500
+            $fediverse,
501
+            $fediverseScope,
502
+            $birthdate,
503
+            $birthdateScope,
504
+            $pronouns,
505
+            $pronounsScope,
506
+        );
507
+    }
508
+
509
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSetUserSettingsSubset')]
510
+    public function testSetUserSettingsSubset(string $property, string $propertyValue): void {
511
+        $controller = $this->getController(false, ['saveUserSettings']);
512
+        $user = $this->createMock(IUser::class);
513
+        $user->method('getUID')->willReturn('johndoe');
514
+
515
+        $this->userSession->method('getUser')->willReturn($user);
516
+
517
+        /** @var IAccount&MockObject $userAccount */
518
+        $userAccount = $this->getDefaultAccountMock();
519
+
520
+        $this->accountManager->expects($this->once())
521
+            ->method('getAccount')
522
+            ->with($user)
523
+            ->willReturn($userAccount);
524
+
525
+        $avatarScope = ($property === 'avatarScope') ? $propertyValue : null;
526
+        $displayName = ($property === 'displayName') ? $propertyValue : null;
527
+        $displayNameScope = ($property === 'displayNameScope') ? $propertyValue : null;
528
+        $phone = ($property === 'phone') ? $propertyValue : null;
529
+        $phoneScope = ($property === 'phoneScope') ? $propertyValue : null;
530
+        $email = ($property === 'email') ? $propertyValue : null;
531
+        $emailScope = ($property === 'emailScope') ? $propertyValue : null;
532
+        $website = ($property === 'website') ? $propertyValue : null;
533
+        $websiteScope = ($property === 'websiteScope') ? $propertyValue : null;
534
+        $address = ($property === 'address') ? $propertyValue : null;
535
+        $addressScope = ($property === 'addressScope') ? $propertyValue : null;
536
+        $twitter = ($property === 'twitter') ? $propertyValue : null;
537
+        $twitterScope = ($property === 'twitterScope') ? $propertyValue : null;
538
+        $bluesky = ($property === 'bluesky') ? $propertyValue : null;
539
+        $blueskyScope = ($property === 'blueskyScope') ? $propertyValue : null;
540
+        $fediverse = ($property === 'fediverse') ? $propertyValue : null;
541
+        $fediverseScope = ($property === 'fediverseScope') ? $propertyValue : null;
542
+        $birthdate = ($property === 'birthdate') ? $propertyValue : null;
543
+        $birthdateScope = ($property === 'birthdateScope') ? $propertyValue : null;
544
+        $pronouns = ($property === 'pronouns') ? $propertyValue : null;
545
+        $pronounsScope = ($property === 'pronounsScope') ? $propertyValue : null;
546
+
547
+        /** @var IAccountProperty[]&MockObject[] $expectedProperties */
548
+        $expectedProperties = $userAccount->getProperties();
549
+        $isScope = strrpos($property, 'Scope') === strlen($property) - strlen('5');
550
+        switch ($property) {
551
+            case 'avatarScope':
552
+                $propertyId = IAccountManager::PROPERTY_AVATAR;
553
+                break;
554
+            case 'displayName':
555
+            case 'displayNameScope':
556
+                $propertyId = IAccountManager::PROPERTY_DISPLAYNAME;
557
+                break;
558
+            case 'phone':
559
+            case 'phoneScope':
560
+                $propertyId = IAccountManager::PROPERTY_PHONE;
561
+                break;
562
+            case 'email':
563
+            case 'emailScope':
564
+                $propertyId = IAccountManager::PROPERTY_EMAIL;
565
+                break;
566
+            case 'website':
567
+            case 'websiteScope':
568
+                $propertyId = IAccountManager::PROPERTY_WEBSITE;
569
+                break;
570
+            case 'address':
571
+            case 'addressScope':
572
+                $propertyId = IAccountManager::PROPERTY_ADDRESS;
573
+                break;
574
+            case 'twitter':
575
+            case 'twitterScope':
576
+                $propertyId = IAccountManager::PROPERTY_TWITTER;
577
+                break;
578
+            case 'bluesky':
579
+            case 'blueskyScope':
580
+                $propertyId = IAccountManager::PROPERTY_BLUESKY;
581
+                break;
582
+            case 'fediverse':
583
+            case 'fediverseScope':
584
+                $propertyId = IAccountManager::PROPERTY_FEDIVERSE;
585
+                break;
586
+            case 'birthdate':
587
+            case 'birthdateScope':
588
+                $propertyId = IAccountManager::PROPERTY_BIRTHDATE;
589
+                break;
590
+            case 'pronouns':
591
+            case 'pronounsScope':
592
+                $propertyId = IAccountManager::PROPERTY_PRONOUNS;
593
+                break;
594
+            default:
595
+                $propertyId = '404';
596
+        }
597
+        $expectedProperties[$propertyId]->expects($this->any())
598
+            ->method($isScope ? 'getScope' : 'getValue')
599
+            ->willReturn($propertyValue);
600
+
601
+        if (!empty($email)) {
602
+            $this->mailer->expects($this->once())->method('validateMailAddress')
603
+                ->willReturn(true);
604
+        }
605
+
606
+        $controller->expects($this->once())
607
+            ->method('saveUserSettings')
608
+            ->with($userAccount);
609
+
610
+        $controller->setUserSettings(
611
+            $avatarScope,
612
+            $displayName,
613
+            $displayNameScope,
614
+            $phone,
615
+            $phoneScope,
616
+            $email,
617
+            $emailScope,
618
+            $website,
619
+            $websiteScope,
620
+            $address,
621
+            $addressScope,
622
+            $twitter,
623
+            $twitterScope,
624
+            $bluesky,
625
+            $blueskyScope,
626
+            $fediverse,
627
+            $fediverseScope,
628
+            $birthdate,
629
+            $birthdateScope,
630
+            $pronouns,
631
+            $pronounsScope,
632
+        );
633
+    }
634
+
635
+    public static function dataTestSetUserSettingsSubset(): array {
636
+        return [
637
+            ['avatarScope', IAccountManager::SCOPE_PUBLISHED],
638
+            ['displayName', 'Display name'],
639
+            ['displayNameScope', IAccountManager::SCOPE_PUBLISHED],
640
+            ['phone', '47658468'],
641
+            ['phoneScope', IAccountManager::SCOPE_PUBLISHED],
642
+            ['email', '[email protected]'],
643
+            ['emailScope', IAccountManager::SCOPE_PUBLISHED],
644
+            ['website', 'nextcloud.com'],
645
+            ['websiteScope', IAccountManager::SCOPE_PUBLISHED],
646
+            ['address', 'street and city'],
647
+            ['addressScope', IAccountManager::SCOPE_PUBLISHED],
648
+            ['twitter', '@nextclouders'],
649
+            ['twitterScope', IAccountManager::SCOPE_PUBLISHED],
650
+            ['bluesky', 'nextclouders.net'],
651
+            ['blueskyScope', IAccountManager::SCOPE_PUBLISHED],
652
+            ['fediverse', '@[email protected]'],
653
+            ['fediverseScope', IAccountManager::SCOPE_PUBLISHED],
654
+            ['birthdate', '2020-01-01'],
655
+            ['birthdateScope', IAccountManager::SCOPE_PUBLISHED],
656
+            ['pronouns', 'he/him'],
657
+            ['pronounsScope', IAccountManager::SCOPE_PUBLISHED],
658
+        ];
659
+    }
660
+
661
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSaveUserSettings')]
662
+    public function testSaveUserSettings(array $data, ?string $oldEmailAddress, ?string $oldDisplayName): void {
663
+        $controller = $this->getController();
664
+        $user = $this->createMock(IUser::class);
665
+
666
+        $user->method('getDisplayName')->willReturn($oldDisplayName);
667
+        $user->method('getSystemEMailAddress')->willReturn($oldEmailAddress);
668
+        $user->method('canChangeDisplayName')->willReturn(true);
669
+
670
+        if (strtolower($data[IAccountManager::PROPERTY_EMAIL]['value']) === strtolower($oldEmailAddress ?? '')) {
671
+            $user->expects($this->never())->method('setSystemEMailAddress');
672
+        } else {
673
+            $user->expects($this->once())->method('setSystemEMailAddress')
674
+                ->with($data[IAccountManager::PROPERTY_EMAIL]['value']);
675
+        }
676
+
677
+        if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] === $oldDisplayName ?? '') {
678
+            $user->expects($this->never())->method('setDisplayName');
679
+        } else {
680
+            $user->expects($this->once())->method('setDisplayName')
681
+                ->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
682
+                ->willReturn(true);
683
+        }
684
+
685
+        $properties = [];
686
+        foreach ($data as $propertyName => $propertyData) {
687
+            $properties[$propertyName] = $this->createMock(IAccountProperty::class);
688
+            $properties[$propertyName]->expects($this->any())
689
+                ->method('getValue')
690
+                ->willReturn($propertyData['value']);
691
+        }
692
+
693
+        $account = $this->createMock(IAccount::class);
694
+        $account->expects($this->any())
695
+            ->method('getUser')
696
+            ->willReturn($user);
697
+        $account->expects($this->any())
698
+            ->method('getProperty')
699
+            ->willReturnCallback(function (string $propertyName) use ($properties) {
700
+                return $properties[$propertyName];
701
+            });
702
+
703
+        $this->accountManager->expects($this->any())
704
+            ->method('getAccount')
705
+            ->willReturn($account);
706
+
707
+        $this->accountManager->expects($this->once())
708
+            ->method('updateAccount')
709
+            ->with($account);
710
+
711
+        $this->invokePrivate($controller, 'saveUserSettings', [$account]);
712
+    }
713
+
714
+    public static function dataTestSaveUserSettings(): array {
715
+        return [
716
+            [
717
+                [
718
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
719
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
720
+                ],
721
+                '[email protected]',
722
+                'john doe'
723
+            ],
724
+            [
725
+                [
726
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
727
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
728
+                ],
729
+                '[email protected]',
730
+                'john New doe'
731
+            ],
732
+            [
733
+                [
734
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
735
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
736
+                ],
737
+                '[email protected]',
738
+                'john doe'
739
+            ],
740
+            [
741
+                [
742
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
743
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
744
+                ],
745
+                '[email protected]',
746
+                'john New doe'
747
+            ],
748
+            [
749
+                [
750
+                    IAccountManager::PROPERTY_EMAIL => ['value' => ''],
751
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
752
+                ],
753
+                null,
754
+                'john New doe'
755
+            ],
756
+            [
757
+                [
758
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
759
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
760
+                ],
761
+                '[email protected]',
762
+                null
763
+            ],
764
+            [
765
+                [
766
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
767
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
768
+                ],
769
+                '[email protected]',
770
+                null
771
+            ],
772
+
773
+        ];
774
+    }
775
+
776
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSaveUserSettingsException')]
777
+    public function testSaveUserSettingsException(
778
+        array $data,
779
+        string $oldEmailAddress,
780
+        string $oldDisplayName,
781
+        bool $setDisplayNameResult,
782
+        bool $canChangeEmail,
783
+    ): void {
784
+        $this->expectException(ForbiddenException::class);
785
+
786
+        $controller = $this->getController();
787
+        $user = $this->createMock(IUser::class);
788
+
789
+        $user->method('getDisplayName')->willReturn($oldDisplayName);
790
+        $user->method('getEMailAddress')->willReturn($oldEmailAddress);
791
+
792
+        /** @var MockObject|IAccount $userAccount */
793
+        $userAccount = $this->createMock(IAccount::class);
794
+        $userAccount->expects($this->any())
795
+            ->method('getUser')
796
+            ->willReturn($user);
797
+        $propertyMocks = [];
798
+        foreach ($data as $propertyName => $propertyData) {
799
+            /** @var MockObject|IAccountProperty $property */
800
+            $propertyMocks[$propertyName] = $this->buildPropertyMock($propertyName, $propertyData['value'], '');
801
+        }
802
+        $userAccount->expects($this->any())
803
+            ->method('getProperty')
804
+            ->willReturnCallback(function (string $propertyName) use ($propertyMocks) {
805
+                return $propertyMocks[$propertyName];
806
+            });
807
+
808
+        if ($data[IAccountManager::PROPERTY_EMAIL]['value'] !== $oldEmailAddress) {
809
+            $user->method('canChangeDisplayName')
810
+                ->willReturn($canChangeEmail);
811
+        }
812
+
813
+        if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] !== $oldDisplayName) {
814
+            $user->method('setDisplayName')
815
+                ->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
816
+                ->willReturn($setDisplayNameResult);
817
+        }
818
+
819
+        $this->invokePrivate($controller, 'saveUserSettings', [$userAccount]);
820
+    }
821
+
822
+
823
+    public static function dataTestSaveUserSettingsException(): array {
824
+        return [
825
+            [
826
+                [
827
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
828
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
829
+                ],
830
+                '[email protected]',
831
+                'john New doe',
832
+                true,
833
+                false
834
+            ],
835
+            [
836
+                [
837
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
838
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
839
+                ],
840
+                '[email protected]',
841
+                'john New doe',
842
+                false,
843
+                true
844
+            ],
845
+            [
846
+                [
847
+                    IAccountManager::PROPERTY_EMAIL => ['value' => '[email protected]'],
848
+                    IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'],
849
+                ],
850
+                '[email protected]',
851
+                'john New doe',
852
+                false,
853
+                false
854
+            ],
855
+
856
+        ];
857
+    }
858
+
859
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetVerificationCode')]
860
+    public function testGetVerificationCode(string $account, string $type, array $dataBefore, array $expectedData, bool $onlyVerificationCode): void {
861
+        $message = 'Use my Federated Cloud ID to share with me: [email protected]';
862
+        $signature = 'theSignature';
863
+
864
+        $code = $message . ' ' . $signature;
865
+        if ($type === IAccountManager::PROPERTY_TWITTER || $type === IAccountManager::PROPERTY_FEDIVERSE) {
866
+            $code = $message . ' ' . md5($signature);
867
+        }
868
+
869
+        $controller = $this->getController(false, ['signMessage', 'getCurrentTime']);
870
+
871
+        $user = $this->createMock(IUser::class);
872
+
873
+        $property = $this->buildPropertyMock($type, $dataBefore[$type]['value'], '', IAccountManager::NOT_VERIFIED);
874
+        $property->expects($this->atLeastOnce())
875
+            ->method('setVerified')
876
+            ->with(IAccountManager::VERIFICATION_IN_PROGRESS)
877
+            ->willReturnSelf();
878
+        $property->expects($this->atLeastOnce())
879
+            ->method('setVerificationData')
880
+            ->with($signature)
881
+            ->willReturnSelf();
882
+
883
+        $userAccount = $this->createMock(IAccount::class);
884
+        $userAccount->expects($this->any())
885
+            ->method('getUser')
886
+            ->willReturn($user);
887
+        $userAccount->expects($this->any())
888
+            ->method('getProperty')
889
+            ->willReturn($property);
890
+
891
+        $this->userSession->expects($this->once())->method('getUser')->willReturn($user);
892
+        $this->accountManager->expects($this->once())->method('getAccount')->with($user)->willReturn($userAccount);
893
+        $user->expects($this->any())->method('getCloudId')->willReturn('[email protected]');
894
+        $user->expects($this->any())->method('getUID')->willReturn('uid');
895
+        $controller->expects($this->once())->method('signMessage')->with($user, $message)->willReturn($signature);
896
+        $controller->expects($this->any())->method('getCurrentTime')->willReturn(1234567);
897
+
898
+        if ($onlyVerificationCode === false) {
899
+            $this->accountManager->expects($this->once())->method('updateAccount')->with($userAccount)->willReturnArgument(1);
900
+            $this->jobList->expects($this->once())->method('add')
901
+                ->with('OCA\Settings\BackgroundJobs\VerifyUserData',
902
+                    [
903
+                        'verificationCode' => $code,
904
+                        'data' => $dataBefore[$type]['value'],
905
+                        'type' => $type,
906
+                        'uid' => 'uid',
907
+                        'try' => 0,
908
+                        'lastRun' => 1234567
909
+                    ]);
910
+        }
911
+
912
+        $result = $controller->getVerificationCode($account, $onlyVerificationCode);
913
+
914
+        $data = $result->getData();
915
+        $this->assertSame(Http::STATUS_OK, $result->getStatus());
916
+        $this->assertSame($code, $data['code']);
917
+    }
918
+
919
+    public static function dataTestGetVerificationCode(): array {
920
+        $accountDataBefore = [
921
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::NOT_VERIFIED],
922
+            IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::NOT_VERIFIED, 'signature' => 'theSignature'],
923
+        ];
924
+
925
+        $accountDataAfterWebsite = [
926
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'],
927
+            IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::NOT_VERIFIED, 'signature' => 'theSignature'],
928
+        ];
929
+
930
+        $accountDataAfterTwitter = [
931
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => IAccountManager::NOT_VERIFIED],
932
+            IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'],
933
+        ];
934
+
935
+        return [
936
+            ['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, false],
937
+            ['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, false],
938
+            ['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, true],
939
+            ['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, true],
940
+        ];
941
+    }
942
+
943
+    /**
944
+     * test get verification code in case no valid user was given
945
+     */
946
+    public function testGetVerificationCodeInvalidUser(): void {
947
+        $controller = $this->getController();
948
+        $this->userSession->expects($this->once())->method('getUser')->willReturn(null);
949
+        $result = $controller->getVerificationCode('account', false);
950
+
951
+        $this->assertSame(Http::STATUS_BAD_REQUEST, $result->getStatus());
952
+    }
953
+
954
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCanAdminChangeUserPasswords')]
955
+    public function testCanAdminChangeUserPasswords(
956
+        bool $encryptionEnabled,
957
+        bool $encryptionModuleLoaded,
958
+        bool $masterKeyEnabled,
959
+        bool $expected,
960
+    ): void {
961
+        $controller = $this->getController();
962
+
963
+        $this->encryptionManager->expects($this->any())
964
+            ->method('isEnabled')
965
+            ->willReturn($encryptionEnabled);
966
+        $this->encryptionManager->expects($this->any())
967
+            ->method('getEncryptionModule')
968
+            ->willReturnCallback(function () use ($encryptionModuleLoaded) {
969
+                if ($encryptionModuleLoaded) {
970
+                    return $this->encryptionModule;
971
+                } else {
972
+                    throw new ModuleDoesNotExistsException();
973
+                }
974
+            });
975
+        $this->encryptionModule->expects($this->any())
976
+            ->method('needDetailedAccessList')
977
+            ->willReturn(!$masterKeyEnabled);
978
+
979
+        $result = $this->invokePrivate($controller, 'canAdminChangeUserPasswords', []);
980
+        $this->assertSame($expected, $result);
981
+    }
982
+
983
+    public static function dataTestCanAdminChangeUserPasswords(): array {
984
+        return [
985
+            // encryptionEnabled, encryptionModuleLoaded, masterKeyEnabled, expectedResult
986
+            [true, true, true, true],
987
+            [false, true, true, true],
988
+            [true, false, true, false],
989
+            [false, false, true, true],
990
+            [true, true, false, false],
991
+            [false, true, false, false],
992
+            [true, false, false, false],
993
+            [false, false, false, true],
994
+        ];
995
+    }
996 996
 }
Please login to merge, or discard this patch.
apps/settings/lib/Controller/UsersController.php 1 patch
Indentation   +525 added lines, -525 removed lines patch added patch discarded remove patch
@@ -56,529 +56,529 @@
 block discarded – undo
56 56
 
57 57
 #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
58 58
 class UsersController extends Controller {
59
-	/** Limit for counting users for subadmins, to avoid spending too much time */
60
-	private const COUNT_LIMIT_FOR_SUBADMINS = 999;
61
-
62
-	public function __construct(
63
-		string $appName,
64
-		IRequest $request,
65
-		private UserManager $userManager,
66
-		private IGroupManager $groupManager,
67
-		private IUserSession $userSession,
68
-		private IConfig $config,
69
-		private IL10N $l10n,
70
-		private IMailer $mailer,
71
-		private IFactory $l10nFactory,
72
-		private IAppManager $appManager,
73
-		private IAccountManager $accountManager,
74
-		private Manager $keyManager,
75
-		private IJobList $jobList,
76
-		private IManager $encryptionManager,
77
-		private KnownUserService $knownUserService,
78
-		private IEventDispatcher $dispatcher,
79
-		private IInitialState $initialState,
80
-	) {
81
-		parent::__construct($appName, $request);
82
-	}
83
-
84
-
85
-	/**
86
-	 * Display users list template
87
-	 *
88
-	 * @return TemplateResponse
89
-	 */
90
-	#[NoAdminRequired]
91
-	#[NoCSRFRequired]
92
-	public function usersListByGroup(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse {
93
-		return $this->usersList($navigationManager, $subAdmin);
94
-	}
95
-
96
-	/**
97
-	 * Display users list template
98
-	 *
99
-	 * @return TemplateResponse
100
-	 */
101
-	#[NoAdminRequired]
102
-	#[NoCSRFRequired]
103
-	public function usersList(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse {
104
-		$user = $this->userSession->getUser();
105
-		$uid = $user->getUID();
106
-		$isAdmin = $this->groupManager->isAdmin($uid);
107
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
108
-
109
-		$navigationManager->setActiveEntry('core_users');
110
-
111
-		/* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
112
-		$sortGroupsBy = MetaData::SORT_USERCOUNT;
113
-		$isLDAPUsed = false;
114
-		if ($this->config->getSystemValueBool('sort_groups_by_name', false)) {
115
-			$sortGroupsBy = MetaData::SORT_GROUPNAME;
116
-		} else {
117
-			if ($this->appManager->isEnabledForUser('user_ldap')) {
118
-				$isLDAPUsed
119
-					= $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
120
-				if ($isLDAPUsed) {
121
-					// LDAP user count can be slow, so we sort by group name here
122
-					$sortGroupsBy = MetaData::SORT_GROUPNAME;
123
-				}
124
-			}
125
-		}
126
-
127
-		$canChangePassword = $this->canAdminChangeUserPasswords();
128
-
129
-		/* GROUPS */
130
-		$groupsInfo = new MetaData(
131
-			$uid,
132
-			$isAdmin,
133
-			$isDelegatedAdmin,
134
-			$this->groupManager,
135
-			$this->userSession
136
-		);
137
-
138
-		$adminGroup = $this->groupManager->get('admin');
139
-		$adminGroupData = [
140
-			'id' => $adminGroup->getGID(),
141
-			'name' => $adminGroup->getDisplayName(),
142
-			'usercount' => $sortGroupsBy === MetaData::SORT_USERCOUNT ? $adminGroup->count() : 0,
143
-			'disabled' => $adminGroup->countDisabled(),
144
-			'canAdd' => $adminGroup->canAddUser(),
145
-			'canRemove' => $adminGroup->canRemoveUser(),
146
-		];
147
-
148
-		if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
149
-			$isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
150
-				return $ldapFound || $backend instanceof User_Proxy;
151
-			});
152
-		}
153
-
154
-		$disabledUsers = -1;
155
-		$userCount = 0;
156
-
157
-		if (!$isLDAPUsed) {
158
-			if ($isAdmin || $isDelegatedAdmin) {
159
-				$disabledUsers = $this->userManager->countDisabledUsers();
160
-				$userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) {
161
-					return $v + (int)$w;
162
-				}, 0);
163
-			} else {
164
-				// User is subadmin !
165
-				[$userCount,$disabledUsers] = $this->userManager->countUsersAndDisabledUsersOfGroups($groupsInfo->getGroups(), self::COUNT_LIMIT_FOR_SUBADMINS);
166
-			}
167
-
168
-			if ($disabledUsers > 0) {
169
-				$userCount -= $disabledUsers;
170
-			}
171
-		}
172
-
173
-		$recentUsersGroup = [
174
-			'id' => '__nc_internal_recent',
175
-			'name' => $this->l10n->t('Recently active'),
176
-			'usercount' => $this->userManager->countSeenUsers(),
177
-		];
178
-
179
-		$disabledUsersGroup = [
180
-			'id' => 'disabled',
181
-			'name' => $this->l10n->t('Disabled accounts'),
182
-			'usercount' => $disabledUsers
183
-		];
184
-
185
-		if (!$isAdmin && !$isDelegatedAdmin) {
186
-			$subAdminGroups = array_map(
187
-				fn (IGroup $group) => ['id' => $group->getGID(), 'name' => $group->getDisplayName()],
188
-				$subAdmin->getSubAdminsGroups($user),
189
-			);
190
-			$subAdminGroups = array_values($subAdminGroups);
191
-		}
192
-
193
-		/* QUOTAS PRESETS */
194
-		$quotaPreset = $this->parseQuotaPreset($this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB'));
195
-		$allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
196
-		if (!$allowUnlimitedQuota && count($quotaPreset) > 0) {
197
-			$defaultQuota = $this->config->getAppValue('files', 'default_quota', $quotaPreset[0]);
198
-		} else {
199
-			$defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
200
-		}
201
-
202
-		$event = new BeforeTemplateRenderedEvent();
203
-		$this->dispatcher->dispatch('OC\Settings\Users::loadAdditionalScripts', $event);
204
-		$this->dispatcher->dispatchTyped($event);
205
-
206
-		/* LANGUAGES */
207
-		$languages = $this->l10nFactory->getLanguages();
208
-
209
-		/** Using LDAP or admins (system config) can enfore sorting by group name, in this case the frontend setting is overwritten */
210
-		$forceSortGroupByName = $sortGroupsBy === MetaData::SORT_GROUPNAME;
211
-
212
-		/* FINAL DATA */
213
-		$serverData = [];
214
-		// groups
215
-		$serverData['systemGroups'] = [$adminGroupData, $recentUsersGroup, $disabledUsersGroup];
216
-		$serverData['subAdminGroups'] = $subAdminGroups ?? [];
217
-		// Various data
218
-		$serverData['isAdmin'] = $isAdmin;
219
-		$serverData['isDelegatedAdmin'] = $isDelegatedAdmin;
220
-		$serverData['sortGroups'] = $forceSortGroupByName
221
-			? MetaData::SORT_GROUPNAME
222
-			: (int)$this->config->getAppValue('core', 'group.sortBy', (string)MetaData::SORT_USERCOUNT);
223
-		$serverData['forceSortGroupByName'] = $forceSortGroupByName;
224
-		$serverData['quotaPreset'] = $quotaPreset;
225
-		$serverData['allowUnlimitedQuota'] = $allowUnlimitedQuota;
226
-		$serverData['userCount'] = $userCount;
227
-		$serverData['languages'] = $languages;
228
-		$serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
229
-		$serverData['forceLanguage'] = $this->config->getSystemValue('force_language', false);
230
-		// Settings
231
-		$serverData['defaultQuota'] = $defaultQuota;
232
-		$serverData['canChangePassword'] = $canChangePassword;
233
-		$serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
234
-		$serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
235
-		$serverData['newUserSendEmail'] = $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes';
236
-
237
-		$this->initialState->provideInitialState('usersSettings', $serverData);
238
-
239
-		Util::addStyle('settings', 'settings');
240
-		Util::addScript('settings', 'vue-settings-apps-users-management');
241
-
242
-		return new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]);
243
-	}
244
-
245
-	/**
246
-	 * @param string $key
247
-	 * @param string $value
248
-	 *
249
-	 * @return JSONResponse
250
-	 */
251
-	#[AuthorizedAdminSetting(settings:Users::class)]
252
-	public function setPreference(string $key, string $value): JSONResponse {
253
-		$allowed = ['newUser.sendEmail', 'group.sortBy'];
254
-		if (!in_array($key, $allowed, true)) {
255
-			return new JSONResponse([], Http::STATUS_FORBIDDEN);
256
-		}
257
-
258
-		$this->config->setAppValue('core', $key, $value);
259
-
260
-		return new JSONResponse([]);
261
-	}
262
-
263
-	/**
264
-	 * Parse the app value for quota_present
265
-	 *
266
-	 * @param string $quotaPreset
267
-	 * @return array
268
-	 */
269
-	protected function parseQuotaPreset(string $quotaPreset): array {
270
-		// 1 GB, 5 GB, 10 GB => [1 GB, 5 GB, 10 GB]
271
-		$presets = array_filter(array_map('trim', explode(',', $quotaPreset)));
272
-		// Drop default and none, Make array indexes numerically
273
-		return array_values(array_diff($presets, ['default', 'none']));
274
-	}
275
-
276
-	/**
277
-	 * check if the admin can change the users password
278
-	 *
279
-	 * The admin can change the passwords if:
280
-	 *
281
-	 *   - no encryption module is loaded and encryption is disabled
282
-	 *   - encryption module is loaded but it doesn't require per user keys
283
-	 *
284
-	 * The admin can not change the passwords if:
285
-	 *
286
-	 *   - an encryption module is loaded and it uses per-user keys
287
-	 *   - encryption is enabled but no encryption modules are loaded
288
-	 *
289
-	 * @return bool
290
-	 */
291
-	protected function canAdminChangeUserPasswords(): bool {
292
-		$isEncryptionEnabled = $this->encryptionManager->isEnabled();
293
-		try {
294
-			$noUserSpecificEncryptionKeys = !$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
295
-			$isEncryptionModuleLoaded = true;
296
-		} catch (ModuleDoesNotExistsException $e) {
297
-			$noUserSpecificEncryptionKeys = true;
298
-			$isEncryptionModuleLoaded = false;
299
-		}
300
-		$canChangePassword = ($isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys)
301
-			|| (!$isEncryptionModuleLoaded && !$isEncryptionEnabled);
302
-
303
-		return $canChangePassword;
304
-	}
305
-
306
-	/**
307
-	 * @NoSubAdminRequired
308
-	 *
309
-	 * @param string|null $avatarScope
310
-	 * @param string|null $displayname
311
-	 * @param string|null $displaynameScope
312
-	 * @param string|null $phone
313
-	 * @param string|null $phoneScope
314
-	 * @param string|null $email
315
-	 * @param string|null $emailScope
316
-	 * @param string|null $website
317
-	 * @param string|null $websiteScope
318
-	 * @param string|null $address
319
-	 * @param string|null $addressScope
320
-	 * @param string|null $twitter
321
-	 * @param string|null $twitterScope
322
-	 * @param string|null $bluesky
323
-	 * @param string|null $blueskyScope
324
-	 * @param string|null $fediverse
325
-	 * @param string|null $fediverseScope
326
-	 * @param string|null $birthdate
327
-	 * @param string|null $birthdateScope
328
-	 *
329
-	 * @return DataResponse
330
-	 */
331
-	#[NoAdminRequired]
332
-	#[PasswordConfirmationRequired]
333
-	#[UserRateLimit(limit: 5, period: 60)]
334
-	public function setUserSettings(?string $avatarScope = null,
335
-		?string $displayname = null,
336
-		?string $displaynameScope = null,
337
-		?string $phone = null,
338
-		?string $phoneScope = null,
339
-		?string $email = null,
340
-		?string $emailScope = null,
341
-		?string $website = null,
342
-		?string $websiteScope = null,
343
-		?string $address = null,
344
-		?string $addressScope = null,
345
-		?string $twitter = null,
346
-		?string $twitterScope = null,
347
-		?string $bluesky = null,
348
-		?string $blueskyScope = null,
349
-		?string $fediverse = null,
350
-		?string $fediverseScope = null,
351
-		?string $birthdate = null,
352
-		?string $birthdateScope = null,
353
-		?string $pronouns = null,
354
-		?string $pronounsScope = null,
355
-	) {
356
-		$user = $this->userSession->getUser();
357
-		if (!$user instanceof IUser) {
358
-			return new DataResponse(
359
-				[
360
-					'status' => 'error',
361
-					'data' => [
362
-						'message' => $this->l10n->t('Invalid account')
363
-					]
364
-				],
365
-				Http::STATUS_UNAUTHORIZED
366
-			);
367
-		}
368
-
369
-		$email = !is_null($email) ? strtolower($email) : $email;
370
-		if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
371
-			return new DataResponse(
372
-				[
373
-					'status' => 'error',
374
-					'data' => [
375
-						'message' => $this->l10n->t('Invalid mail address')
376
-					]
377
-				],
378
-				Http::STATUS_UNPROCESSABLE_ENTITY
379
-			);
380
-		}
381
-
382
-		$userAccount = $this->accountManager->getAccount($user);
383
-		$oldPhoneValue = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue();
384
-
385
-		$updatable = [
386
-			IAccountManager::PROPERTY_AVATAR => ['value' => null, 'scope' => $avatarScope],
387
-			IAccountManager::PROPERTY_DISPLAYNAME => ['value' => $displayname, 'scope' => $displaynameScope],
388
-			IAccountManager::PROPERTY_EMAIL => ['value' => $email, 'scope' => $emailScope],
389
-			IAccountManager::PROPERTY_WEBSITE => ['value' => $website, 'scope' => $websiteScope],
390
-			IAccountManager::PROPERTY_ADDRESS => ['value' => $address, 'scope' => $addressScope],
391
-			IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope],
392
-			IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope],
393
-			IAccountManager::PROPERTY_BLUESKY => ['value' => $bluesky, 'scope' => $blueskyScope],
394
-			IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope],
395
-			IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope],
396
-			IAccountManager::PROPERTY_PRONOUNS => ['value' => $pronouns, 'scope' => $pronounsScope],
397
-		];
398
-		$allowUserToChangeDisplayName = $this->config->getSystemValueBool('allow_user_to_change_display_name', true);
399
-		foreach ($updatable as $property => $data) {
400
-			if ($allowUserToChangeDisplayName === false
401
-				&& in_array($property, [IAccountManager::PROPERTY_DISPLAYNAME, IAccountManager::PROPERTY_EMAIL], true)) {
402
-				continue;
403
-			}
404
-			$property = $userAccount->getProperty($property);
405
-			if ($data['value'] !== null) {
406
-				$property->setValue($data['value']);
407
-			}
408
-			if ($data['scope'] !== null) {
409
-				$property->setScope($data['scope']);
410
-			}
411
-		}
412
-
413
-		try {
414
-			$this->saveUserSettings($userAccount);
415
-			if ($oldPhoneValue !== $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue()) {
416
-				$this->knownUserService->deleteByContactUserId($user->getUID());
417
-			}
418
-			return new DataResponse(
419
-				[
420
-					'status' => 'success',
421
-					'data' => [
422
-						'userId' => $user->getUID(),
423
-						'avatarScope' => $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR)->getScope(),
424
-						'displayname' => $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue(),
425
-						'displaynameScope' => $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope(),
426
-						'phone' => $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(),
427
-						'phoneScope' => $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getScope(),
428
-						'email' => $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
429
-						'emailScope' => $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
430
-						'website' => $userAccount->getProperty(IAccountManager::PROPERTY_WEBSITE)->getValue(),
431
-						'websiteScope' => $userAccount->getProperty(IAccountManager::PROPERTY_WEBSITE)->getScope(),
432
-						'address' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getValue(),
433
-						'addressScope' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getScope(),
434
-						'twitter' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getValue(),
435
-						'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(),
436
-						'bluesky' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue(),
437
-						'blueskyScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getScope(),
438
-						'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(),
439
-						'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(),
440
-						'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(),
441
-						'birthdateScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getScope(),
442
-						'pronouns' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getValue(),
443
-						'pronounsScope' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getScope(),
444
-						'message' => $this->l10n->t('Settings saved'),
445
-					],
446
-				],
447
-				Http::STATUS_OK
448
-			);
449
-		} catch (ForbiddenException|InvalidArgumentException|PropertyDoesNotExistException $e) {
450
-			return new DataResponse([
451
-				'status' => 'error',
452
-				'data' => [
453
-					'message' => $e->getMessage()
454
-				],
455
-			]);
456
-		}
457
-	}
458
-	/**
459
-	 * update account manager with new user data
460
-	 *
461
-	 * @throws ForbiddenException
462
-	 * @throws InvalidArgumentException
463
-	 */
464
-	protected function saveUserSettings(IAccount $userAccount): void {
465
-		// keep the user back-end up-to-date with the latest display name and email
466
-		// address
467
-		$oldDisplayName = $userAccount->getUser()->getDisplayName();
468
-		if ($oldDisplayName !== $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue()) {
469
-			$result = $userAccount->getUser()->setDisplayName($userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue());
470
-			if ($result === false) {
471
-				throw new ForbiddenException($this->l10n->t('Unable to change full name'));
472
-			}
473
-		}
474
-
475
-		$oldEmailAddress = $userAccount->getUser()->getSystemEMailAddress();
476
-		$oldEmailAddress = strtolower((string)$oldEmailAddress);
477
-		if ($oldEmailAddress !== strtolower($userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue())) {
478
-			// this is the only permission a backend provides and is also used
479
-			// for the permission of setting a email address
480
-			if (!$userAccount->getUser()->canChangeDisplayName()) {
481
-				throw new ForbiddenException($this->l10n->t('Unable to change email address'));
482
-			}
483
-			$userAccount->getUser()->setSystemEMailAddress($userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue());
484
-		}
485
-
486
-		try {
487
-			$this->accountManager->updateAccount($userAccount);
488
-		} catch (InvalidArgumentException $e) {
489
-			if ($e->getMessage() === IAccountManager::PROPERTY_PHONE) {
490
-				throw new InvalidArgumentException($this->l10n->t('Unable to set invalid phone number'));
491
-			}
492
-			if ($e->getMessage() === IAccountManager::PROPERTY_WEBSITE) {
493
-				throw new InvalidArgumentException($this->l10n->t('Unable to set invalid website'));
494
-			}
495
-			throw new InvalidArgumentException($this->l10n->t('Some account data was invalid'));
496
-		}
497
-	}
498
-
499
-	/**
500
-	 * Set the mail address of a user
501
-	 *
502
-	 * @NoSubAdminRequired
503
-	 *
504
-	 * @param string $account
505
-	 * @param bool $onlyVerificationCode only return verification code without updating the data
506
-	 * @return DataResponse
507
-	 */
508
-	#[NoAdminRequired]
509
-	#[PasswordConfirmationRequired]
510
-	public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
511
-		$user = $this->userSession->getUser();
512
-
513
-		if ($user === null) {
514
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
515
-		}
516
-
517
-		$userAccount = $this->accountManager->getAccount($user);
518
-		$cloudId = $user->getCloudId();
519
-		$message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
520
-		$signature = $this->signMessage($user, $message);
521
-
522
-		$code = $message . ' ' . $signature;
523
-		$codeMd5 = $message . ' ' . md5($signature);
524
-
525
-		switch ($account) {
526
-			case 'verify-twitter':
527
-				$msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
528
-				$code = $codeMd5;
529
-				$type = IAccountManager::PROPERTY_TWITTER;
530
-				break;
531
-			case 'verify-website':
532
-				$msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
533
-				$type = IAccountManager::PROPERTY_WEBSITE;
534
-				break;
535
-			default:
536
-				return new DataResponse([], Http::STATUS_BAD_REQUEST);
537
-		}
538
-
539
-		$userProperty = $userAccount->getProperty($type);
540
-		$userProperty
541
-			->setVerified(IAccountManager::VERIFICATION_IN_PROGRESS)
542
-			->setVerificationData($signature);
543
-
544
-		if ($onlyVerificationCode === false) {
545
-			$this->accountManager->updateAccount($userAccount);
546
-
547
-			$this->jobList->add(VerifyUserData::class,
548
-				[
549
-					'verificationCode' => $code,
550
-					'data' => $userProperty->getValue(),
551
-					'type' => $type,
552
-					'uid' => $user->getUID(),
553
-					'try' => 0,
554
-					'lastRun' => $this->getCurrentTime()
555
-				]
556
-			);
557
-		}
558
-
559
-		return new DataResponse(['msg' => $msg, 'code' => $code]);
560
-	}
561
-
562
-	/**
563
-	 * get current timestamp
564
-	 *
565
-	 * @return int
566
-	 */
567
-	protected function getCurrentTime(): int {
568
-		return time();
569
-	}
570
-
571
-	/**
572
-	 * sign message with users private key
573
-	 *
574
-	 * @param IUser $user
575
-	 * @param string $message
576
-	 *
577
-	 * @return string base64 encoded signature
578
-	 */
579
-	protected function signMessage(IUser $user, string $message): string {
580
-		$privateKey = $this->keyManager->getKey($user)->getPrivate();
581
-		openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
582
-		return base64_encode($signature);
583
-	}
59
+    /** Limit for counting users for subadmins, to avoid spending too much time */
60
+    private const COUNT_LIMIT_FOR_SUBADMINS = 999;
61
+
62
+    public function __construct(
63
+        string $appName,
64
+        IRequest $request,
65
+        private UserManager $userManager,
66
+        private IGroupManager $groupManager,
67
+        private IUserSession $userSession,
68
+        private IConfig $config,
69
+        private IL10N $l10n,
70
+        private IMailer $mailer,
71
+        private IFactory $l10nFactory,
72
+        private IAppManager $appManager,
73
+        private IAccountManager $accountManager,
74
+        private Manager $keyManager,
75
+        private IJobList $jobList,
76
+        private IManager $encryptionManager,
77
+        private KnownUserService $knownUserService,
78
+        private IEventDispatcher $dispatcher,
79
+        private IInitialState $initialState,
80
+    ) {
81
+        parent::__construct($appName, $request);
82
+    }
83
+
84
+
85
+    /**
86
+     * Display users list template
87
+     *
88
+     * @return TemplateResponse
89
+     */
90
+    #[NoAdminRequired]
91
+    #[NoCSRFRequired]
92
+    public function usersListByGroup(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse {
93
+        return $this->usersList($navigationManager, $subAdmin);
94
+    }
95
+
96
+    /**
97
+     * Display users list template
98
+     *
99
+     * @return TemplateResponse
100
+     */
101
+    #[NoAdminRequired]
102
+    #[NoCSRFRequired]
103
+    public function usersList(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse {
104
+        $user = $this->userSession->getUser();
105
+        $uid = $user->getUID();
106
+        $isAdmin = $this->groupManager->isAdmin($uid);
107
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
108
+
109
+        $navigationManager->setActiveEntry('core_users');
110
+
111
+        /* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
112
+        $sortGroupsBy = MetaData::SORT_USERCOUNT;
113
+        $isLDAPUsed = false;
114
+        if ($this->config->getSystemValueBool('sort_groups_by_name', false)) {
115
+            $sortGroupsBy = MetaData::SORT_GROUPNAME;
116
+        } else {
117
+            if ($this->appManager->isEnabledForUser('user_ldap')) {
118
+                $isLDAPUsed
119
+                    = $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
120
+                if ($isLDAPUsed) {
121
+                    // LDAP user count can be slow, so we sort by group name here
122
+                    $sortGroupsBy = MetaData::SORT_GROUPNAME;
123
+                }
124
+            }
125
+        }
126
+
127
+        $canChangePassword = $this->canAdminChangeUserPasswords();
128
+
129
+        /* GROUPS */
130
+        $groupsInfo = new MetaData(
131
+            $uid,
132
+            $isAdmin,
133
+            $isDelegatedAdmin,
134
+            $this->groupManager,
135
+            $this->userSession
136
+        );
137
+
138
+        $adminGroup = $this->groupManager->get('admin');
139
+        $adminGroupData = [
140
+            'id' => $adminGroup->getGID(),
141
+            'name' => $adminGroup->getDisplayName(),
142
+            'usercount' => $sortGroupsBy === MetaData::SORT_USERCOUNT ? $adminGroup->count() : 0,
143
+            'disabled' => $adminGroup->countDisabled(),
144
+            'canAdd' => $adminGroup->canAddUser(),
145
+            'canRemove' => $adminGroup->canRemoveUser(),
146
+        ];
147
+
148
+        if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
149
+            $isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
150
+                return $ldapFound || $backend instanceof User_Proxy;
151
+            });
152
+        }
153
+
154
+        $disabledUsers = -1;
155
+        $userCount = 0;
156
+
157
+        if (!$isLDAPUsed) {
158
+            if ($isAdmin || $isDelegatedAdmin) {
159
+                $disabledUsers = $this->userManager->countDisabledUsers();
160
+                $userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) {
161
+                    return $v + (int)$w;
162
+                }, 0);
163
+            } else {
164
+                // User is subadmin !
165
+                [$userCount,$disabledUsers] = $this->userManager->countUsersAndDisabledUsersOfGroups($groupsInfo->getGroups(), self::COUNT_LIMIT_FOR_SUBADMINS);
166
+            }
167
+
168
+            if ($disabledUsers > 0) {
169
+                $userCount -= $disabledUsers;
170
+            }
171
+        }
172
+
173
+        $recentUsersGroup = [
174
+            'id' => '__nc_internal_recent',
175
+            'name' => $this->l10n->t('Recently active'),
176
+            'usercount' => $this->userManager->countSeenUsers(),
177
+        ];
178
+
179
+        $disabledUsersGroup = [
180
+            'id' => 'disabled',
181
+            'name' => $this->l10n->t('Disabled accounts'),
182
+            'usercount' => $disabledUsers
183
+        ];
184
+
185
+        if (!$isAdmin && !$isDelegatedAdmin) {
186
+            $subAdminGroups = array_map(
187
+                fn (IGroup $group) => ['id' => $group->getGID(), 'name' => $group->getDisplayName()],
188
+                $subAdmin->getSubAdminsGroups($user),
189
+            );
190
+            $subAdminGroups = array_values($subAdminGroups);
191
+        }
192
+
193
+        /* QUOTAS PRESETS */
194
+        $quotaPreset = $this->parseQuotaPreset($this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB'));
195
+        $allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
196
+        if (!$allowUnlimitedQuota && count($quotaPreset) > 0) {
197
+            $defaultQuota = $this->config->getAppValue('files', 'default_quota', $quotaPreset[0]);
198
+        } else {
199
+            $defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
200
+        }
201
+
202
+        $event = new BeforeTemplateRenderedEvent();
203
+        $this->dispatcher->dispatch('OC\Settings\Users::loadAdditionalScripts', $event);
204
+        $this->dispatcher->dispatchTyped($event);
205
+
206
+        /* LANGUAGES */
207
+        $languages = $this->l10nFactory->getLanguages();
208
+
209
+        /** Using LDAP or admins (system config) can enfore sorting by group name, in this case the frontend setting is overwritten */
210
+        $forceSortGroupByName = $sortGroupsBy === MetaData::SORT_GROUPNAME;
211
+
212
+        /* FINAL DATA */
213
+        $serverData = [];
214
+        // groups
215
+        $serverData['systemGroups'] = [$adminGroupData, $recentUsersGroup, $disabledUsersGroup];
216
+        $serverData['subAdminGroups'] = $subAdminGroups ?? [];
217
+        // Various data
218
+        $serverData['isAdmin'] = $isAdmin;
219
+        $serverData['isDelegatedAdmin'] = $isDelegatedAdmin;
220
+        $serverData['sortGroups'] = $forceSortGroupByName
221
+            ? MetaData::SORT_GROUPNAME
222
+            : (int)$this->config->getAppValue('core', 'group.sortBy', (string)MetaData::SORT_USERCOUNT);
223
+        $serverData['forceSortGroupByName'] = $forceSortGroupByName;
224
+        $serverData['quotaPreset'] = $quotaPreset;
225
+        $serverData['allowUnlimitedQuota'] = $allowUnlimitedQuota;
226
+        $serverData['userCount'] = $userCount;
227
+        $serverData['languages'] = $languages;
228
+        $serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
229
+        $serverData['forceLanguage'] = $this->config->getSystemValue('force_language', false);
230
+        // Settings
231
+        $serverData['defaultQuota'] = $defaultQuota;
232
+        $serverData['canChangePassword'] = $canChangePassword;
233
+        $serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
234
+        $serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
235
+        $serverData['newUserSendEmail'] = $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes';
236
+
237
+        $this->initialState->provideInitialState('usersSettings', $serverData);
238
+
239
+        Util::addStyle('settings', 'settings');
240
+        Util::addScript('settings', 'vue-settings-apps-users-management');
241
+
242
+        return new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]);
243
+    }
244
+
245
+    /**
246
+     * @param string $key
247
+     * @param string $value
248
+     *
249
+     * @return JSONResponse
250
+     */
251
+    #[AuthorizedAdminSetting(settings:Users::class)]
252
+    public function setPreference(string $key, string $value): JSONResponse {
253
+        $allowed = ['newUser.sendEmail', 'group.sortBy'];
254
+        if (!in_array($key, $allowed, true)) {
255
+            return new JSONResponse([], Http::STATUS_FORBIDDEN);
256
+        }
257
+
258
+        $this->config->setAppValue('core', $key, $value);
259
+
260
+        return new JSONResponse([]);
261
+    }
262
+
263
+    /**
264
+     * Parse the app value for quota_present
265
+     *
266
+     * @param string $quotaPreset
267
+     * @return array
268
+     */
269
+    protected function parseQuotaPreset(string $quotaPreset): array {
270
+        // 1 GB, 5 GB, 10 GB => [1 GB, 5 GB, 10 GB]
271
+        $presets = array_filter(array_map('trim', explode(',', $quotaPreset)));
272
+        // Drop default and none, Make array indexes numerically
273
+        return array_values(array_diff($presets, ['default', 'none']));
274
+    }
275
+
276
+    /**
277
+     * check if the admin can change the users password
278
+     *
279
+     * The admin can change the passwords if:
280
+     *
281
+     *   - no encryption module is loaded and encryption is disabled
282
+     *   - encryption module is loaded but it doesn't require per user keys
283
+     *
284
+     * The admin can not change the passwords if:
285
+     *
286
+     *   - an encryption module is loaded and it uses per-user keys
287
+     *   - encryption is enabled but no encryption modules are loaded
288
+     *
289
+     * @return bool
290
+     */
291
+    protected function canAdminChangeUserPasswords(): bool {
292
+        $isEncryptionEnabled = $this->encryptionManager->isEnabled();
293
+        try {
294
+            $noUserSpecificEncryptionKeys = !$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
295
+            $isEncryptionModuleLoaded = true;
296
+        } catch (ModuleDoesNotExistsException $e) {
297
+            $noUserSpecificEncryptionKeys = true;
298
+            $isEncryptionModuleLoaded = false;
299
+        }
300
+        $canChangePassword = ($isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys)
301
+            || (!$isEncryptionModuleLoaded && !$isEncryptionEnabled);
302
+
303
+        return $canChangePassword;
304
+    }
305
+
306
+    /**
307
+     * @NoSubAdminRequired
308
+     *
309
+     * @param string|null $avatarScope
310
+     * @param string|null $displayname
311
+     * @param string|null $displaynameScope
312
+     * @param string|null $phone
313
+     * @param string|null $phoneScope
314
+     * @param string|null $email
315
+     * @param string|null $emailScope
316
+     * @param string|null $website
317
+     * @param string|null $websiteScope
318
+     * @param string|null $address
319
+     * @param string|null $addressScope
320
+     * @param string|null $twitter
321
+     * @param string|null $twitterScope
322
+     * @param string|null $bluesky
323
+     * @param string|null $blueskyScope
324
+     * @param string|null $fediverse
325
+     * @param string|null $fediverseScope
326
+     * @param string|null $birthdate
327
+     * @param string|null $birthdateScope
328
+     *
329
+     * @return DataResponse
330
+     */
331
+    #[NoAdminRequired]
332
+    #[PasswordConfirmationRequired]
333
+    #[UserRateLimit(limit: 5, period: 60)]
334
+    public function setUserSettings(?string $avatarScope = null,
335
+        ?string $displayname = null,
336
+        ?string $displaynameScope = null,
337
+        ?string $phone = null,
338
+        ?string $phoneScope = null,
339
+        ?string $email = null,
340
+        ?string $emailScope = null,
341
+        ?string $website = null,
342
+        ?string $websiteScope = null,
343
+        ?string $address = null,
344
+        ?string $addressScope = null,
345
+        ?string $twitter = null,
346
+        ?string $twitterScope = null,
347
+        ?string $bluesky = null,
348
+        ?string $blueskyScope = null,
349
+        ?string $fediverse = null,
350
+        ?string $fediverseScope = null,
351
+        ?string $birthdate = null,
352
+        ?string $birthdateScope = null,
353
+        ?string $pronouns = null,
354
+        ?string $pronounsScope = null,
355
+    ) {
356
+        $user = $this->userSession->getUser();
357
+        if (!$user instanceof IUser) {
358
+            return new DataResponse(
359
+                [
360
+                    'status' => 'error',
361
+                    'data' => [
362
+                        'message' => $this->l10n->t('Invalid account')
363
+                    ]
364
+                ],
365
+                Http::STATUS_UNAUTHORIZED
366
+            );
367
+        }
368
+
369
+        $email = !is_null($email) ? strtolower($email) : $email;
370
+        if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
371
+            return new DataResponse(
372
+                [
373
+                    'status' => 'error',
374
+                    'data' => [
375
+                        'message' => $this->l10n->t('Invalid mail address')
376
+                    ]
377
+                ],
378
+                Http::STATUS_UNPROCESSABLE_ENTITY
379
+            );
380
+        }
381
+
382
+        $userAccount = $this->accountManager->getAccount($user);
383
+        $oldPhoneValue = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue();
384
+
385
+        $updatable = [
386
+            IAccountManager::PROPERTY_AVATAR => ['value' => null, 'scope' => $avatarScope],
387
+            IAccountManager::PROPERTY_DISPLAYNAME => ['value' => $displayname, 'scope' => $displaynameScope],
388
+            IAccountManager::PROPERTY_EMAIL => ['value' => $email, 'scope' => $emailScope],
389
+            IAccountManager::PROPERTY_WEBSITE => ['value' => $website, 'scope' => $websiteScope],
390
+            IAccountManager::PROPERTY_ADDRESS => ['value' => $address, 'scope' => $addressScope],
391
+            IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope],
392
+            IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope],
393
+            IAccountManager::PROPERTY_BLUESKY => ['value' => $bluesky, 'scope' => $blueskyScope],
394
+            IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope],
395
+            IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope],
396
+            IAccountManager::PROPERTY_PRONOUNS => ['value' => $pronouns, 'scope' => $pronounsScope],
397
+        ];
398
+        $allowUserToChangeDisplayName = $this->config->getSystemValueBool('allow_user_to_change_display_name', true);
399
+        foreach ($updatable as $property => $data) {
400
+            if ($allowUserToChangeDisplayName === false
401
+                && in_array($property, [IAccountManager::PROPERTY_DISPLAYNAME, IAccountManager::PROPERTY_EMAIL], true)) {
402
+                continue;
403
+            }
404
+            $property = $userAccount->getProperty($property);
405
+            if ($data['value'] !== null) {
406
+                $property->setValue($data['value']);
407
+            }
408
+            if ($data['scope'] !== null) {
409
+                $property->setScope($data['scope']);
410
+            }
411
+        }
412
+
413
+        try {
414
+            $this->saveUserSettings($userAccount);
415
+            if ($oldPhoneValue !== $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue()) {
416
+                $this->knownUserService->deleteByContactUserId($user->getUID());
417
+            }
418
+            return new DataResponse(
419
+                [
420
+                    'status' => 'success',
421
+                    'data' => [
422
+                        'userId' => $user->getUID(),
423
+                        'avatarScope' => $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR)->getScope(),
424
+                        'displayname' => $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue(),
425
+                        'displaynameScope' => $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope(),
426
+                        'phone' => $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(),
427
+                        'phoneScope' => $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getScope(),
428
+                        'email' => $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
429
+                        'emailScope' => $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
430
+                        'website' => $userAccount->getProperty(IAccountManager::PROPERTY_WEBSITE)->getValue(),
431
+                        'websiteScope' => $userAccount->getProperty(IAccountManager::PROPERTY_WEBSITE)->getScope(),
432
+                        'address' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getValue(),
433
+                        'addressScope' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getScope(),
434
+                        'twitter' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getValue(),
435
+                        'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(),
436
+                        'bluesky' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue(),
437
+                        'blueskyScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getScope(),
438
+                        'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(),
439
+                        'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(),
440
+                        'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(),
441
+                        'birthdateScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getScope(),
442
+                        'pronouns' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getValue(),
443
+                        'pronounsScope' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getScope(),
444
+                        'message' => $this->l10n->t('Settings saved'),
445
+                    ],
446
+                ],
447
+                Http::STATUS_OK
448
+            );
449
+        } catch (ForbiddenException|InvalidArgumentException|PropertyDoesNotExistException $e) {
450
+            return new DataResponse([
451
+                'status' => 'error',
452
+                'data' => [
453
+                    'message' => $e->getMessage()
454
+                ],
455
+            ]);
456
+        }
457
+    }
458
+    /**
459
+     * update account manager with new user data
460
+     *
461
+     * @throws ForbiddenException
462
+     * @throws InvalidArgumentException
463
+     */
464
+    protected function saveUserSettings(IAccount $userAccount): void {
465
+        // keep the user back-end up-to-date with the latest display name and email
466
+        // address
467
+        $oldDisplayName = $userAccount->getUser()->getDisplayName();
468
+        if ($oldDisplayName !== $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue()) {
469
+            $result = $userAccount->getUser()->setDisplayName($userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue());
470
+            if ($result === false) {
471
+                throw new ForbiddenException($this->l10n->t('Unable to change full name'));
472
+            }
473
+        }
474
+
475
+        $oldEmailAddress = $userAccount->getUser()->getSystemEMailAddress();
476
+        $oldEmailAddress = strtolower((string)$oldEmailAddress);
477
+        if ($oldEmailAddress !== strtolower($userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue())) {
478
+            // this is the only permission a backend provides and is also used
479
+            // for the permission of setting a email address
480
+            if (!$userAccount->getUser()->canChangeDisplayName()) {
481
+                throw new ForbiddenException($this->l10n->t('Unable to change email address'));
482
+            }
483
+            $userAccount->getUser()->setSystemEMailAddress($userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue());
484
+        }
485
+
486
+        try {
487
+            $this->accountManager->updateAccount($userAccount);
488
+        } catch (InvalidArgumentException $e) {
489
+            if ($e->getMessage() === IAccountManager::PROPERTY_PHONE) {
490
+                throw new InvalidArgumentException($this->l10n->t('Unable to set invalid phone number'));
491
+            }
492
+            if ($e->getMessage() === IAccountManager::PROPERTY_WEBSITE) {
493
+                throw new InvalidArgumentException($this->l10n->t('Unable to set invalid website'));
494
+            }
495
+            throw new InvalidArgumentException($this->l10n->t('Some account data was invalid'));
496
+        }
497
+    }
498
+
499
+    /**
500
+     * Set the mail address of a user
501
+     *
502
+     * @NoSubAdminRequired
503
+     *
504
+     * @param string $account
505
+     * @param bool $onlyVerificationCode only return verification code without updating the data
506
+     * @return DataResponse
507
+     */
508
+    #[NoAdminRequired]
509
+    #[PasswordConfirmationRequired]
510
+    public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
511
+        $user = $this->userSession->getUser();
512
+
513
+        if ($user === null) {
514
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
515
+        }
516
+
517
+        $userAccount = $this->accountManager->getAccount($user);
518
+        $cloudId = $user->getCloudId();
519
+        $message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
520
+        $signature = $this->signMessage($user, $message);
521
+
522
+        $code = $message . ' ' . $signature;
523
+        $codeMd5 = $message . ' ' . md5($signature);
524
+
525
+        switch ($account) {
526
+            case 'verify-twitter':
527
+                $msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
528
+                $code = $codeMd5;
529
+                $type = IAccountManager::PROPERTY_TWITTER;
530
+                break;
531
+            case 'verify-website':
532
+                $msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
533
+                $type = IAccountManager::PROPERTY_WEBSITE;
534
+                break;
535
+            default:
536
+                return new DataResponse([], Http::STATUS_BAD_REQUEST);
537
+        }
538
+
539
+        $userProperty = $userAccount->getProperty($type);
540
+        $userProperty
541
+            ->setVerified(IAccountManager::VERIFICATION_IN_PROGRESS)
542
+            ->setVerificationData($signature);
543
+
544
+        if ($onlyVerificationCode === false) {
545
+            $this->accountManager->updateAccount($userAccount);
546
+
547
+            $this->jobList->add(VerifyUserData::class,
548
+                [
549
+                    'verificationCode' => $code,
550
+                    'data' => $userProperty->getValue(),
551
+                    'type' => $type,
552
+                    'uid' => $user->getUID(),
553
+                    'try' => 0,
554
+                    'lastRun' => $this->getCurrentTime()
555
+                ]
556
+            );
557
+        }
558
+
559
+        return new DataResponse(['msg' => $msg, 'code' => $code]);
560
+    }
561
+
562
+    /**
563
+     * get current timestamp
564
+     *
565
+     * @return int
566
+     */
567
+    protected function getCurrentTime(): int {
568
+        return time();
569
+    }
570
+
571
+    /**
572
+     * sign message with users private key
573
+     *
574
+     * @param IUser $user
575
+     * @param string $message
576
+     *
577
+     * @return string base64 encoded signature
578
+     */
579
+    protected function signMessage(IUser $user, string $message): string {
580
+        $privateKey = $this->keyManager->getKey($user)->getPrivate();
581
+        openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
582
+        return base64_encode($signature);
583
+    }
584 584
 }
Please login to merge, or discard this patch.
apps/settings/lib/Settings/Personal/PersonalInfo.php 1 patch
Indentation   +284 added lines, -284 removed lines patch added patch discarded remove patch
@@ -33,288 +33,288 @@
 block discarded – undo
33 33
 
34 34
 class PersonalInfo implements ISettings {
35 35
 
36
-	/** @var ProfileManager */
37
-	private $profileManager;
38
-
39
-	public function __construct(
40
-		private IConfig $config,
41
-		private IUserManager $userManager,
42
-		private IGroupManager $groupManager,
43
-		private IAccountManager $accountManager,
44
-		ProfileManager $profileManager,
45
-		private IAppManager $appManager,
46
-		private IFactory $l10nFactory,
47
-		private IL10N $l,
48
-		private IInitialState $initialStateService,
49
-		private IManager $manager,
50
-	) {
51
-		$this->profileManager = $profileManager;
52
-	}
53
-
54
-	public function getForm(): TemplateResponse {
55
-		$federationEnabled = $this->appManager->isEnabledForUser('federation');
56
-		$federatedFileSharingEnabled = $this->appManager->isEnabledForUser('federatedfilesharing');
57
-		$lookupServerUploadEnabled = false;
58
-		if ($federatedFileSharingEnabled) {
59
-			/** @var FederatedShareProvider $shareProvider */
60
-			$shareProvider = Server::get(FederatedShareProvider::class);
61
-			$lookupServerUploadEnabled = $shareProvider->isLookupServerUploadEnabled();
62
-		}
63
-
64
-		$uid = \OC_User::getUser();
65
-		$user = $this->userManager->get($uid);
66
-		$account = $this->accountManager->getAccount($user);
67
-
68
-		// make sure FS is setup before querying storage related stuff...
69
-		\OC_Util::setupFS($user->getUID());
70
-
71
-		$storageInfo = \OC_Helper::getStorageInfo('/');
72
-		if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
73
-			$totalSpace = $this->l->t('Unlimited');
74
-		} else {
75
-			$totalSpace = Util::humanFileSize($storageInfo['total']);
76
-		}
77
-
78
-		$messageParameters = $this->getMessageParameters($account);
79
-
80
-		$parameters = [
81
-			'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
82
-			'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(),
83
-			'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),
84
-		] + $messageParameters;
85
-
86
-		$personalInfoParameters = [
87
-			'userId' => $uid,
88
-			'avatar' => $this->getProperty($account, IAccountManager::PROPERTY_AVATAR),
89
-			'groups' => $this->getGroups($user),
90
-			'quota' => $storageInfo['quota'],
91
-			'totalSpace' => $totalSpace,
92
-			'usage' => Util::humanFileSize($storageInfo['used']),
93
-			'usageRelative' => round($storageInfo['relative']),
94
-			'displayName' => $this->getProperty($account, IAccountManager::PROPERTY_DISPLAYNAME),
95
-			'emailMap' => $this->getEmailMap($account),
96
-			'phone' => $this->getProperty($account, IAccountManager::PROPERTY_PHONE),
97
-			'defaultPhoneRegion' => $this->config->getSystemValueString('default_phone_region'),
98
-			'location' => $this->getProperty($account, IAccountManager::PROPERTY_ADDRESS),
99
-			'website' => $this->getProperty($account, IAccountManager::PROPERTY_WEBSITE),
100
-			'twitter' => $this->getProperty($account, IAccountManager::PROPERTY_TWITTER),
101
-			'bluesky' => $this->getProperty($account, IAccountManager::PROPERTY_BLUESKY),
102
-			'fediverse' => $this->getProperty($account, IAccountManager::PROPERTY_FEDIVERSE),
103
-			'languageMap' => $this->getLanguageMap($user),
104
-			'localeMap' => $this->getLocaleMap($user),
105
-			'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),
106
-			'profileEnabled' => $this->profileManager->isProfileEnabled($user),
107
-			'organisation' => $this->getProperty($account, IAccountManager::PROPERTY_ORGANISATION),
108
-			'role' => $this->getProperty($account, IAccountManager::PROPERTY_ROLE),
109
-			'headline' => $this->getProperty($account, IAccountManager::PROPERTY_HEADLINE),
110
-			'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY),
111
-			'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE),
112
-			'firstDayOfWeek' => $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK),
113
-			'pronouns' => $this->getProperty($account, IAccountManager::PROPERTY_PRONOUNS),
114
-		];
115
-
116
-		$accountParameters = [
117
-			'avatarChangeSupported' => $user->canChangeAvatar(),
118
-			'displayNameChangeSupported' => $user->canChangeDisplayName(),
119
-			'emailChangeSupported' => $user->canChangeEmail(),
120
-			'federationEnabled' => $federationEnabled,
121
-			'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
122
-		];
123
-
124
-		$profileParameters = [
125
-			'profileConfig' => $this->profileManager->getProfileConfigWithMetadata($user, $user),
126
-		];
127
-
128
-		$this->initialStateService->provideInitialState('profileEnabledGlobally', $this->profileManager->isProfileEnabled());
129
-		$this->initialStateService->provideInitialState('personalInfoParameters', $personalInfoParameters);
130
-		$this->initialStateService->provideInitialState('accountParameters', $accountParameters);
131
-		$this->initialStateService->provideInitialState('profileParameters', $profileParameters);
132
-
133
-		return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, '');
134
-	}
135
-
136
-	/**
137
-	 * Check if is fair use of free push service
138
-	 * @return boolean
139
-	 */
140
-	private function isFairUseOfFreePushService(): bool {
141
-		return $this->manager->isFairUseOfFreePushService();
142
-	}
143
-
144
-	/**
145
-	 * returns the property data in an
146
-	 * associative array
147
-	 */
148
-	private function getProperty(IAccount $account, string $property): array {
149
-		$property = [
150
-			'name' => $account->getProperty($property)->getName(),
151
-			'value' => $account->getProperty($property)->getValue(),
152
-			'scope' => $account->getProperty($property)->getScope(),
153
-			'verified' => $account->getProperty($property)->getVerified(),
154
-		];
155
-
156
-		return $property;
157
-	}
158
-
159
-	/**
160
-	 * returns the section ID string, e.g. 'sharing'
161
-	 * @since 9.1
162
-	 */
163
-	public function getSection(): string {
164
-		return 'personal-info';
165
-	}
166
-
167
-	/**
168
-	 * @return int whether the form should be rather on the top or bottom of
169
-	 *             the admin section. The forms are arranged in ascending order of the
170
-	 *             priority values. It is required to return a value between 0 and 100.
171
-	 *
172
-	 * E.g.: 70
173
-	 * @since 9.1
174
-	 */
175
-	public function getPriority(): int {
176
-		return 10;
177
-	}
178
-
179
-	/**
180
-	 * returns a sorted list of the user's group GIDs
181
-	 */
182
-	private function getGroups(IUser $user): array {
183
-		$groups = array_map(
184
-			static function (IGroup $group) {
185
-				return $group->getDisplayName();
186
-			},
187
-			$this->groupManager->getUserGroups($user)
188
-		);
189
-		sort($groups);
190
-
191
-		return $groups;
192
-	}
193
-
194
-	/**
195
-	 * returns the primary email and additional emails in an
196
-	 * associative array
197
-	 */
198
-	private function getEmailMap(IAccount $account): array {
199
-		$systemEmail = [
200
-			'name' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getName(),
201
-			'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
202
-			'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
203
-			'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
204
-		];
205
-
206
-		$additionalEmails = array_map(
207
-			function (IAccountProperty $property) {
208
-				return [
209
-					'name' => $property->getName(),
210
-					'value' => $property->getValue(),
211
-					'scope' => $property->getScope(),
212
-					'verified' => $property->getVerified(),
213
-					'locallyVerified' => $property->getLocallyVerified(),
214
-				];
215
-			},
216
-			$account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties(),
217
-		);
218
-
219
-		$emailMap = [
220
-			'primaryEmail' => $systemEmail,
221
-			'additionalEmails' => $additionalEmails,
222
-			'notificationEmail' => (string)$account->getUser()->getPrimaryEMailAddress(),
223
-		];
224
-
225
-		return $emailMap;
226
-	}
227
-
228
-	/**
229
-	 * returns the user's active language, common languages, and other languages in an
230
-	 * associative array
231
-	 */
232
-	private function getLanguageMap(IUser $user): array {
233
-		$forceLanguage = $this->config->getSystemValue('force_language', false);
234
-		if ($forceLanguage !== false) {
235
-			return [];
236
-		}
237
-
238
-		$uid = $user->getUID();
239
-
240
-		$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
241
-		$languages = $this->l10nFactory->getLanguages();
242
-
243
-		// associate the user language with the proper array
244
-		$userLangIndex = array_search($userConfLang, array_column($languages['commonLanguages'], 'code'));
245
-		$userLang = $languages['commonLanguages'][$userLangIndex];
246
-		// search in the other languages
247
-		if ($userLangIndex === false) {
248
-			$userLangIndex = array_search($userConfLang, array_column($languages['otherLanguages'], 'code'));
249
-			$userLang = $languages['otherLanguages'][$userLangIndex];
250
-		}
251
-		// if user language is not available but set somehow: show the actual code as name
252
-		if (!is_array($userLang)) {
253
-			$userLang = [
254
-				'code' => $userConfLang,
255
-				'name' => $userConfLang,
256
-			];
257
-		}
258
-
259
-		return array_merge(
260
-			['activeLanguage' => $userLang],
261
-			$languages
262
-		);
263
-	}
264
-
265
-	private function getLocaleMap(IUser $user): array {
266
-		$forceLanguage = $this->config->getSystemValue('force_locale', false);
267
-		if ($forceLanguage !== false) {
268
-			return [];
269
-		}
270
-
271
-		$uid = $user->getUID();
272
-		$userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
273
-		$userLocaleString = $this->config->getUserValue($uid, 'core', 'locale', $this->l10nFactory->findLocale($userLang));
274
-		$localeCodes = $this->l10nFactory->findAvailableLocales();
275
-		$userLocale = array_filter($localeCodes, fn ($value) => $userLocaleString === $value['code']);
276
-
277
-		if (!empty($userLocale)) {
278
-			$userLocale = reset($userLocale);
279
-		}
280
-
281
-		$localesForLanguage = array_values(array_filter($localeCodes, fn ($localeCode) => str_starts_with($localeCode['code'], $userLang)));
282
-		$otherLocales = array_values(array_filter($localeCodes, fn ($localeCode) => !str_starts_with($localeCode['code'], $userLang)));
283
-
284
-		if (!$userLocale) {
285
-			$userLocale = [
286
-				'code' => 'en',
287
-				'name' => 'English'
288
-			];
289
-		}
290
-
291
-		return [
292
-			'activeLocaleLang' => $userLocaleString,
293
-			'activeLocale' => $userLocale,
294
-			'localesForLanguage' => $localesForLanguage,
295
-			'otherLocales' => $otherLocales,
296
-		];
297
-	}
298
-
299
-	/**
300
-	 * returns the message parameters
301
-	 */
302
-	private function getMessageParameters(IAccount $account): array {
303
-		$needVerifyMessage = [IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER];
304
-		$messageParameters = [];
305
-		foreach ($needVerifyMessage as $property) {
306
-			switch ($account->getProperty($property)->getVerified()) {
307
-				case IAccountManager::VERIFIED:
308
-					$message = $this->l->t('Verifying');
309
-					break;
310
-				case IAccountManager::VERIFICATION_IN_PROGRESS:
311
-					$message = $this->l->t('Verifying …');
312
-					break;
313
-				default:
314
-					$message = $this->l->t('Verify');
315
-			}
316
-			$messageParameters[$property . 'Message'] = $message;
317
-		}
318
-		return $messageParameters;
319
-	}
36
+    /** @var ProfileManager */
37
+    private $profileManager;
38
+
39
+    public function __construct(
40
+        private IConfig $config,
41
+        private IUserManager $userManager,
42
+        private IGroupManager $groupManager,
43
+        private IAccountManager $accountManager,
44
+        ProfileManager $profileManager,
45
+        private IAppManager $appManager,
46
+        private IFactory $l10nFactory,
47
+        private IL10N $l,
48
+        private IInitialState $initialStateService,
49
+        private IManager $manager,
50
+    ) {
51
+        $this->profileManager = $profileManager;
52
+    }
53
+
54
+    public function getForm(): TemplateResponse {
55
+        $federationEnabled = $this->appManager->isEnabledForUser('federation');
56
+        $federatedFileSharingEnabled = $this->appManager->isEnabledForUser('federatedfilesharing');
57
+        $lookupServerUploadEnabled = false;
58
+        if ($federatedFileSharingEnabled) {
59
+            /** @var FederatedShareProvider $shareProvider */
60
+            $shareProvider = Server::get(FederatedShareProvider::class);
61
+            $lookupServerUploadEnabled = $shareProvider->isLookupServerUploadEnabled();
62
+        }
63
+
64
+        $uid = \OC_User::getUser();
65
+        $user = $this->userManager->get($uid);
66
+        $account = $this->accountManager->getAccount($user);
67
+
68
+        // make sure FS is setup before querying storage related stuff...
69
+        \OC_Util::setupFS($user->getUID());
70
+
71
+        $storageInfo = \OC_Helper::getStorageInfo('/');
72
+        if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
73
+            $totalSpace = $this->l->t('Unlimited');
74
+        } else {
75
+            $totalSpace = Util::humanFileSize($storageInfo['total']);
76
+        }
77
+
78
+        $messageParameters = $this->getMessageParameters($account);
79
+
80
+        $parameters = [
81
+            'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
82
+            'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(),
83
+            'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),
84
+        ] + $messageParameters;
85
+
86
+        $personalInfoParameters = [
87
+            'userId' => $uid,
88
+            'avatar' => $this->getProperty($account, IAccountManager::PROPERTY_AVATAR),
89
+            'groups' => $this->getGroups($user),
90
+            'quota' => $storageInfo['quota'],
91
+            'totalSpace' => $totalSpace,
92
+            'usage' => Util::humanFileSize($storageInfo['used']),
93
+            'usageRelative' => round($storageInfo['relative']),
94
+            'displayName' => $this->getProperty($account, IAccountManager::PROPERTY_DISPLAYNAME),
95
+            'emailMap' => $this->getEmailMap($account),
96
+            'phone' => $this->getProperty($account, IAccountManager::PROPERTY_PHONE),
97
+            'defaultPhoneRegion' => $this->config->getSystemValueString('default_phone_region'),
98
+            'location' => $this->getProperty($account, IAccountManager::PROPERTY_ADDRESS),
99
+            'website' => $this->getProperty($account, IAccountManager::PROPERTY_WEBSITE),
100
+            'twitter' => $this->getProperty($account, IAccountManager::PROPERTY_TWITTER),
101
+            'bluesky' => $this->getProperty($account, IAccountManager::PROPERTY_BLUESKY),
102
+            'fediverse' => $this->getProperty($account, IAccountManager::PROPERTY_FEDIVERSE),
103
+            'languageMap' => $this->getLanguageMap($user),
104
+            'localeMap' => $this->getLocaleMap($user),
105
+            'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),
106
+            'profileEnabled' => $this->profileManager->isProfileEnabled($user),
107
+            'organisation' => $this->getProperty($account, IAccountManager::PROPERTY_ORGANISATION),
108
+            'role' => $this->getProperty($account, IAccountManager::PROPERTY_ROLE),
109
+            'headline' => $this->getProperty($account, IAccountManager::PROPERTY_HEADLINE),
110
+            'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY),
111
+            'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE),
112
+            'firstDayOfWeek' => $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK),
113
+            'pronouns' => $this->getProperty($account, IAccountManager::PROPERTY_PRONOUNS),
114
+        ];
115
+
116
+        $accountParameters = [
117
+            'avatarChangeSupported' => $user->canChangeAvatar(),
118
+            'displayNameChangeSupported' => $user->canChangeDisplayName(),
119
+            'emailChangeSupported' => $user->canChangeEmail(),
120
+            'federationEnabled' => $federationEnabled,
121
+            'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
122
+        ];
123
+
124
+        $profileParameters = [
125
+            'profileConfig' => $this->profileManager->getProfileConfigWithMetadata($user, $user),
126
+        ];
127
+
128
+        $this->initialStateService->provideInitialState('profileEnabledGlobally', $this->profileManager->isProfileEnabled());
129
+        $this->initialStateService->provideInitialState('personalInfoParameters', $personalInfoParameters);
130
+        $this->initialStateService->provideInitialState('accountParameters', $accountParameters);
131
+        $this->initialStateService->provideInitialState('profileParameters', $profileParameters);
132
+
133
+        return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, '');
134
+    }
135
+
136
+    /**
137
+     * Check if is fair use of free push service
138
+     * @return boolean
139
+     */
140
+    private function isFairUseOfFreePushService(): bool {
141
+        return $this->manager->isFairUseOfFreePushService();
142
+    }
143
+
144
+    /**
145
+     * returns the property data in an
146
+     * associative array
147
+     */
148
+    private function getProperty(IAccount $account, string $property): array {
149
+        $property = [
150
+            'name' => $account->getProperty($property)->getName(),
151
+            'value' => $account->getProperty($property)->getValue(),
152
+            'scope' => $account->getProperty($property)->getScope(),
153
+            'verified' => $account->getProperty($property)->getVerified(),
154
+        ];
155
+
156
+        return $property;
157
+    }
158
+
159
+    /**
160
+     * returns the section ID string, e.g. 'sharing'
161
+     * @since 9.1
162
+     */
163
+    public function getSection(): string {
164
+        return 'personal-info';
165
+    }
166
+
167
+    /**
168
+     * @return int whether the form should be rather on the top or bottom of
169
+     *             the admin section. The forms are arranged in ascending order of the
170
+     *             priority values. It is required to return a value between 0 and 100.
171
+     *
172
+     * E.g.: 70
173
+     * @since 9.1
174
+     */
175
+    public function getPriority(): int {
176
+        return 10;
177
+    }
178
+
179
+    /**
180
+     * returns a sorted list of the user's group GIDs
181
+     */
182
+    private function getGroups(IUser $user): array {
183
+        $groups = array_map(
184
+            static function (IGroup $group) {
185
+                return $group->getDisplayName();
186
+            },
187
+            $this->groupManager->getUserGroups($user)
188
+        );
189
+        sort($groups);
190
+
191
+        return $groups;
192
+    }
193
+
194
+    /**
195
+     * returns the primary email and additional emails in an
196
+     * associative array
197
+     */
198
+    private function getEmailMap(IAccount $account): array {
199
+        $systemEmail = [
200
+            'name' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getName(),
201
+            'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
202
+            'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
203
+            'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
204
+        ];
205
+
206
+        $additionalEmails = array_map(
207
+            function (IAccountProperty $property) {
208
+                return [
209
+                    'name' => $property->getName(),
210
+                    'value' => $property->getValue(),
211
+                    'scope' => $property->getScope(),
212
+                    'verified' => $property->getVerified(),
213
+                    'locallyVerified' => $property->getLocallyVerified(),
214
+                ];
215
+            },
216
+            $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties(),
217
+        );
218
+
219
+        $emailMap = [
220
+            'primaryEmail' => $systemEmail,
221
+            'additionalEmails' => $additionalEmails,
222
+            'notificationEmail' => (string)$account->getUser()->getPrimaryEMailAddress(),
223
+        ];
224
+
225
+        return $emailMap;
226
+    }
227
+
228
+    /**
229
+     * returns the user's active language, common languages, and other languages in an
230
+     * associative array
231
+     */
232
+    private function getLanguageMap(IUser $user): array {
233
+        $forceLanguage = $this->config->getSystemValue('force_language', false);
234
+        if ($forceLanguage !== false) {
235
+            return [];
236
+        }
237
+
238
+        $uid = $user->getUID();
239
+
240
+        $userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
241
+        $languages = $this->l10nFactory->getLanguages();
242
+
243
+        // associate the user language with the proper array
244
+        $userLangIndex = array_search($userConfLang, array_column($languages['commonLanguages'], 'code'));
245
+        $userLang = $languages['commonLanguages'][$userLangIndex];
246
+        // search in the other languages
247
+        if ($userLangIndex === false) {
248
+            $userLangIndex = array_search($userConfLang, array_column($languages['otherLanguages'], 'code'));
249
+            $userLang = $languages['otherLanguages'][$userLangIndex];
250
+        }
251
+        // if user language is not available but set somehow: show the actual code as name
252
+        if (!is_array($userLang)) {
253
+            $userLang = [
254
+                'code' => $userConfLang,
255
+                'name' => $userConfLang,
256
+            ];
257
+        }
258
+
259
+        return array_merge(
260
+            ['activeLanguage' => $userLang],
261
+            $languages
262
+        );
263
+    }
264
+
265
+    private function getLocaleMap(IUser $user): array {
266
+        $forceLanguage = $this->config->getSystemValue('force_locale', false);
267
+        if ($forceLanguage !== false) {
268
+            return [];
269
+        }
270
+
271
+        $uid = $user->getUID();
272
+        $userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
273
+        $userLocaleString = $this->config->getUserValue($uid, 'core', 'locale', $this->l10nFactory->findLocale($userLang));
274
+        $localeCodes = $this->l10nFactory->findAvailableLocales();
275
+        $userLocale = array_filter($localeCodes, fn ($value) => $userLocaleString === $value['code']);
276
+
277
+        if (!empty($userLocale)) {
278
+            $userLocale = reset($userLocale);
279
+        }
280
+
281
+        $localesForLanguage = array_values(array_filter($localeCodes, fn ($localeCode) => str_starts_with($localeCode['code'], $userLang)));
282
+        $otherLocales = array_values(array_filter($localeCodes, fn ($localeCode) => !str_starts_with($localeCode['code'], $userLang)));
283
+
284
+        if (!$userLocale) {
285
+            $userLocale = [
286
+                'code' => 'en',
287
+                'name' => 'English'
288
+            ];
289
+        }
290
+
291
+        return [
292
+            'activeLocaleLang' => $userLocaleString,
293
+            'activeLocale' => $userLocale,
294
+            'localesForLanguage' => $localesForLanguage,
295
+            'otherLocales' => $otherLocales,
296
+        ];
297
+    }
298
+
299
+    /**
300
+     * returns the message parameters
301
+     */
302
+    private function getMessageParameters(IAccount $account): array {
303
+        $needVerifyMessage = [IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER];
304
+        $messageParameters = [];
305
+        foreach ($needVerifyMessage as $property) {
306
+            switch ($account->getProperty($property)->getVerified()) {
307
+                case IAccountManager::VERIFIED:
308
+                    $message = $this->l->t('Verifying');
309
+                    break;
310
+                case IAccountManager::VERIFICATION_IN_PROGRESS:
311
+                    $message = $this->l->t('Verifying …');
312
+                    break;
313
+                default:
314
+                    $message = $this->l->t('Verify');
315
+            }
316
+            $messageParameters[$property . 'Message'] = $message;
317
+        }
318
+        return $messageParameters;
319
+    }
320 320
 }
Please login to merge, or discard this patch.
apps/provisioning_api/tests/Controller/UsersControllerTest.php 1 patch
Indentation   +4412 added lines, -4412 removed lines patch added patch discarded remove patch
@@ -47,4417 +47,4417 @@
 block discarded – undo
47 47
 use Test\TestCase;
48 48
 
49 49
 class UsersControllerTest extends TestCase {
50
-	protected IUserManager&MockObject $userManager;
51
-	protected IConfig&MockObject $config;
52
-	protected Manager&MockObject $groupManager;
53
-	protected IUserSession&MockObject $userSession;
54
-	protected LoggerInterface&MockObject $logger;
55
-	protected UsersController&MockObject $api;
56
-	protected IAccountManager&MockObject $accountManager;
57
-	protected ISubAdmin&MockObject $subAdminManager;
58
-	protected IURLGenerator&MockObject $urlGenerator;
59
-	protected IRequest&MockObject $request;
60
-	private IFactory&MockObject $l10nFactory;
61
-	private NewUserMailHelper&MockObject $newUserMailHelper;
62
-	private ISecureRandom&MockObject $secureRandom;
63
-	private RemoteWipe&MockObject $remoteWipe;
64
-	private KnownUserService&MockObject $knownUserService;
65
-	private IEventDispatcher&MockObject $eventDispatcher;
66
-	private IRootFolder $rootFolder;
67
-	private IPhoneNumberUtil $phoneNumberUtil;
68
-	private IAppManager $appManager;
69
-
70
-	protected function setUp(): void {
71
-		parent::setUp();
72
-
73
-		$this->userManager = $this->createMock(IUserManager::class);
74
-		$this->config = $this->createMock(IConfig::class);
75
-		$this->groupManager = $this->createMock(Manager::class);
76
-		$this->userSession = $this->createMock(IUserSession::class);
77
-		$this->logger = $this->createMock(LoggerInterface::class);
78
-		$this->request = $this->createMock(IRequest::class);
79
-		$this->accountManager = $this->createMock(IAccountManager::class);
80
-		$this->subAdminManager = $this->createMock(ISubAdmin::class);
81
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
82
-		$this->l10nFactory = $this->createMock(IFactory::class);
83
-		$this->newUserMailHelper = $this->createMock(NewUserMailHelper::class);
84
-		$this->secureRandom = $this->createMock(ISecureRandom::class);
85
-		$this->remoteWipe = $this->createMock(RemoteWipe::class);
86
-		$this->knownUserService = $this->createMock(KnownUserService::class);
87
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
88
-		$this->phoneNumberUtil = new PhoneNumberUtil();
89
-		$this->appManager = $this->createMock(IAppManager::class);
90
-		$this->rootFolder = $this->createMock(IRootFolder::class);
91
-
92
-		$l10n = $this->createMock(IL10N::class);
93
-		$l10n->method('t')->willReturnCallback(fn (string $txt, array $replacement = []) => sprintf($txt, ...$replacement));
94
-		$this->l10nFactory->method('get')->with('provisioning_api')->willReturn($l10n);
95
-
96
-		$this->api = $this->getMockBuilder(UsersController::class)
97
-			->setConstructorArgs([
98
-				'provisioning_api',
99
-				$this->request,
100
-				$this->userManager,
101
-				$this->config,
102
-				$this->groupManager,
103
-				$this->userSession,
104
-				$this->accountManager,
105
-				$this->subAdminManager,
106
-				$this->l10nFactory,
107
-				$this->rootFolder,
108
-				$this->urlGenerator,
109
-				$this->logger,
110
-				$this->newUserMailHelper,
111
-				$this->secureRandom,
112
-				$this->remoteWipe,
113
-				$this->knownUserService,
114
-				$this->eventDispatcher,
115
-				$this->phoneNumberUtil,
116
-				$this->appManager,
117
-			])
118
-			->onlyMethods(['fillStorageInfo'])
119
-			->getMock();
120
-	}
121
-
122
-	public function testGetUsersAsAdmin(): void {
123
-		$loggedInUser = $this->getMockBuilder(IUser::class)
124
-			->disableOriginalConstructor()
125
-			->getMock();
126
-		$loggedInUser
127
-			->expects($this->once())
128
-			->method('getUID')
129
-			->willReturn('admin');
130
-		$this->userSession
131
-			->expects($this->once())
132
-			->method('getUser')
133
-			->willReturn($loggedInUser);
134
-		$this->groupManager
135
-			->expects($this->once())
136
-			->method('isAdmin')
137
-			->willReturn(true);
138
-		$this->userManager
139
-			->expects($this->once())
140
-			->method('search')
141
-			->with('MyCustomSearch')
142
-			->willReturn(['Admin' => [], 'Foo' => [], 'Bar' => []]);
143
-
144
-		$expected = [
145
-			'users' => [
146
-				'Admin',
147
-				'Foo',
148
-				'Bar',
149
-			],
150
-		];
151
-		$this->assertEquals($expected, $this->api->getUsers('MyCustomSearch')->getData());
152
-	}
153
-
154
-	public function testGetUsersAsSubAdmin(): void {
155
-		$loggedInUser = $this->getMockBuilder(IUser::class)
156
-			->disableOriginalConstructor()
157
-			->getMock();
158
-		$loggedInUser
159
-			->expects($this->once())
160
-			->method('getUID')
161
-			->willReturn('subadmin');
162
-		$this->userSession
163
-			->expects($this->once())
164
-			->method('getUser')
165
-			->willReturn($loggedInUser);
166
-		$this->groupManager
167
-			->expects($this->once())
168
-			->method('isAdmin')
169
-			->willReturn(false);
170
-		$firstGroup = $this->getMockBuilder('OCP\IGroup')
171
-			->disableOriginalConstructor()
172
-			->getMock();
173
-		$firstGroup
174
-			->expects($this->once())
175
-			->method('getGID')
176
-			->willReturn('FirstGroup');
177
-		$secondGroup = $this->getMockBuilder('OCP\IGroup')
178
-			->disableOriginalConstructor()
179
-			->getMock();
180
-		$secondGroup
181
-			->expects($this->once())
182
-			->method('getGID')
183
-			->willReturn('SecondGroup');
184
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
185
-			->disableOriginalConstructor()->getMock();
186
-		$subAdminManager
187
-			->expects($this->once())
188
-			->method('isSubAdmin')
189
-			->with($loggedInUser)
190
-			->willReturn(true);
191
-		$subAdminManager
192
-			->expects($this->once())
193
-			->method('getSubAdminsGroups')
194
-			->with($loggedInUser)
195
-			->willReturn([$firstGroup, $secondGroup]);
196
-		$this->groupManager
197
-			->expects($this->once())
198
-			->method('getSubAdmin')
199
-			->willReturn($subAdminManager);
200
-		$this->groupManager
201
-			->expects($this->any())
202
-			->method('displayNamesInGroup')->willReturnOnConsecutiveCalls(['AnotherUserInTheFirstGroup' => []], ['UserInTheSecondGroup' => []]);
203
-
204
-		$expected = [
205
-			'users' => [
206
-				'AnotherUserInTheFirstGroup',
207
-				'UserInTheSecondGroup',
208
-			],
209
-		];
210
-		$this->assertEquals($expected, $this->api->getUsers('MyCustomSearch')->getData());
211
-	}
212
-
213
-	private function createUserMock(string $uid, bool $enabled): MockObject&IUser {
214
-		$mockUser = $this->getMockBuilder(IUser::class)
215
-			->disableOriginalConstructor()
216
-			->getMock();
217
-		$mockUser
218
-			->method('getUID')
219
-			->willReturn($uid);
220
-		$mockUser
221
-			->method('isEnabled')
222
-			->willReturn($enabled);
223
-		return $mockUser;
224
-	}
225
-
226
-	public function testGetDisabledUsersAsAdmin(): void {
227
-		$loggedInUser = $this->getMockBuilder(IUser::class)
228
-			->disableOriginalConstructor()
229
-			->getMock();
230
-		$loggedInUser
231
-			->expects($this->once())
232
-			->method('getUID')
233
-			->willReturn('admin');
234
-		$this->userSession
235
-			->expects($this->atLeastOnce())
236
-			->method('getUser')
237
-			->willReturn($loggedInUser);
238
-		$this->groupManager
239
-			->expects($this->once())
240
-			->method('isAdmin')
241
-			->willReturn(true);
242
-		$this->userManager
243
-			->expects($this->once())
244
-			->method('getDisabledUsers')
245
-			->with(3, 0, 'MyCustomSearch')
246
-			->willReturn([
247
-				$this->createUserMock('admin', false),
248
-				$this->createUserMock('foo', false),
249
-				$this->createUserMock('bar', false),
250
-			]);
251
-
252
-		$expected = [
253
-			'users' => [
254
-				'admin' => ['id' => 'admin'],
255
-				'foo' => ['id' => 'foo'],
256
-				'bar' => ['id' => 'bar'],
257
-			],
258
-		];
259
-		$this->assertEquals($expected, $this->api->getDisabledUsersDetails('MyCustomSearch', 3)->getData());
260
-	}
261
-
262
-	public function testGetDisabledUsersAsSubAdmin(): void {
263
-		$loggedInUser = $this->getMockBuilder(IUser::class)
264
-			->disableOriginalConstructor()
265
-			->getMock();
266
-		$loggedInUser
267
-			->expects($this->once())
268
-			->method('getUID')
269
-			->willReturn('subadmin');
270
-		$this->userSession
271
-			->expects($this->atLeastOnce())
272
-			->method('getUser')
273
-			->willReturn($loggedInUser);
274
-		$this->groupManager
275
-			->expects($this->once())
276
-			->method('isAdmin')
277
-			->willReturn(false);
278
-		$firstGroup = $this->getMockBuilder('OCP\IGroup')
279
-			->disableOriginalConstructor()
280
-			->getMock();
281
-		$secondGroup = $this->getMockBuilder('OCP\IGroup')
282
-			->disableOriginalConstructor()
283
-			->getMock();
284
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
285
-			->disableOriginalConstructor()->getMock();
286
-		$subAdminManager
287
-			->expects($this->once())
288
-			->method('isSubAdmin')
289
-			->with($loggedInUser)
290
-			->willReturn(true);
291
-		$subAdminManager
292
-			->expects($this->once())
293
-			->method('getSubAdminsGroups')
294
-			->with($loggedInUser)
295
-			->willReturn([$firstGroup, $secondGroup]);
296
-		$this->groupManager
297
-			->expects($this->once())
298
-			->method('getSubAdmin')
299
-			->willReturn($subAdminManager);
300
-		$this->groupManager
301
-			->expects($this->never())
302
-			->method('displayNamesInGroup');
303
-
304
-		$firstGroup
305
-			->expects($this->once())
306
-			->method('searchUsers')
307
-			->with('MyCustomSearch')
308
-			->willReturn([
309
-				$this->createUserMock('user1', false),
310
-				$this->createUserMock('bob', true),
311
-				$this->createUserMock('user2', false),
312
-				$this->createUserMock('alice', true),
313
-			]);
314
-
315
-		$secondGroup
316
-			->expects($this->once())
317
-			->method('searchUsers')
318
-			->with('MyCustomSearch')
319
-			->willReturn([
320
-				$this->createUserMock('user2', false),
321
-				$this->createUserMock('joe', true),
322
-				$this->createUserMock('user3', false),
323
-				$this->createUserMock('jim', true),
324
-				$this->createUserMock('john', true),
325
-			]);
326
-
327
-
328
-		$expected = [
329
-			'users' => [
330
-				'user1' => ['id' => 'user1'],
331
-				'user2' => ['id' => 'user2'],
332
-				'user3' => ['id' => 'user3'],
333
-			],
334
-		];
335
-		$this->assertEquals($expected, $this->api->getDisabledUsersDetails('MyCustomSearch', 3)->getData());
336
-	}
337
-
338
-
339
-	public function testAddUserAlreadyExisting(): void {
340
-		$this->expectException(OCSException::class);
341
-		$this->expectExceptionCode(102);
342
-
343
-		$this->userManager
344
-			->expects($this->once())
345
-			->method('userExists')
346
-			->with('AlreadyExistingUser')
347
-			->willReturn(true);
348
-		$this->logger
349
-			->expects($this->once())
350
-			->method('error')
351
-			->with('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
352
-		$loggedInUser = $this->getMockBuilder(IUser::class)
353
-			->disableOriginalConstructor()
354
-			->getMock();
355
-		$loggedInUser
356
-			->expects($this->exactly(2))
357
-			->method('getUID')
358
-			->willReturn('adminUser');
359
-		$this->userSession
360
-			->expects($this->once())
361
-			->method('getUser')
362
-			->willReturn($loggedInUser);
363
-		$this->groupManager
364
-			->expects($this->once())
365
-			->method('isAdmin')
366
-			->with('adminUser')
367
-			->willReturn(true);
368
-
369
-		$this->api->addUser('AlreadyExistingUser', 'password', '', '', []);
370
-	}
371
-
372
-
373
-	public function testAddUserNonExistingGroup(): void {
374
-		$this->expectException(OCSException::class);
375
-		$this->expectExceptionMessage('Group NonExistingGroup does not exist');
376
-		$this->expectExceptionCode(104);
377
-
378
-		$this->userManager
379
-			->expects($this->once())
380
-			->method('userExists')
381
-			->with('NewUser')
382
-			->willReturn(false);
383
-		$loggedInUser = $this->getMockBuilder(IUser::class)
384
-			->disableOriginalConstructor()
385
-			->getMock();
386
-		$loggedInUser
387
-			->expects($this->exactly(2))
388
-			->method('getUID')
389
-			->willReturn('adminUser');
390
-		$this->userSession
391
-			->expects($this->once())
392
-			->method('getUser')
393
-			->willReturn($loggedInUser);
394
-		$this->groupManager
395
-			->expects($this->once())
396
-			->method('isAdmin')
397
-			->with('adminUser')
398
-			->willReturn(true);
399
-		$this->groupManager
400
-			->expects($this->once())
401
-			->method('groupExists')
402
-			->with('NonExistingGroup')
403
-			->willReturn(false);
404
-
405
-		$this->api->addUser('NewUser', 'pass', '', '', ['NonExistingGroup']);
406
-	}
407
-
408
-
409
-	public function testAddUserExistingGroupNonExistingGroup(): void {
410
-		$this->expectException(OCSException::class);
411
-		$this->expectExceptionMessage('Group NonExistingGroup does not exist');
412
-		$this->expectExceptionCode(104);
413
-
414
-		$this->userManager
415
-			->expects($this->once())
416
-			->method('userExists')
417
-			->with('NewUser')
418
-			->willReturn(false);
419
-		$loggedInUser = $this->getMockBuilder(IUser::class)
420
-			->disableOriginalConstructor()
421
-			->getMock();
422
-		$loggedInUser
423
-			->expects($this->exactly(2))
424
-			->method('getUID')
425
-			->willReturn('adminUser');
426
-		$this->userSession
427
-			->expects($this->once())
428
-			->method('getUser')
429
-			->willReturn($loggedInUser);
430
-		$this->groupManager
431
-			->expects($this->once())
432
-			->method('isAdmin')
433
-			->with('adminUser')
434
-			->willReturn(true);
435
-		$this->groupManager
436
-			->expects($this->exactly(2))
437
-			->method('groupExists')
438
-			->willReturnMap([
439
-				['ExistingGroup', true],
440
-				['NonExistingGroup', false]
441
-			]);
442
-
443
-		$this->api->addUser('NewUser', 'pass', '', '', ['ExistingGroup', 'NonExistingGroup']);
444
-	}
445
-
446
-	public function testAddUserSuccessful(): void {
447
-		$this->userManager
448
-			->expects($this->once())
449
-			->method('userExists')
450
-			->with('NewUser')
451
-			->willReturn(false);
452
-		$this->userManager
453
-			->expects($this->once())
454
-			->method('createUser')
455
-			->with('NewUser', 'PasswordOfTheNewUser')
456
-			->willReturn($this->createMock(IUser::class));
457
-		$this->logger
458
-			->expects($this->once())
459
-			->method('info')
460
-			->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
461
-		$loggedInUser = $this->getMockBuilder(IUser::class)
462
-			->disableOriginalConstructor()
463
-			->getMock();
464
-		$loggedInUser
465
-			->expects($this->exactly(2))
466
-			->method('getUID')
467
-			->willReturn('adminUser');
468
-		$this->userSession
469
-			->expects($this->once())
470
-			->method('getUser')
471
-			->willReturn($loggedInUser);
472
-		$this->groupManager
473
-			->expects($this->once())
474
-			->method('isAdmin')
475
-			->with('adminUser')
476
-			->willReturn(true);
477
-
478
-		$this->assertTrue(key_exists(
479
-			'id',
480
-			$this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
481
-		));
482
-	}
483
-
484
-	public function testAddUserSuccessfulWithDisplayName(): void {
485
-		/**
486
-		 * @var UserController
487
-		 */
488
-		$api = $this->getMockBuilder(UsersController::class)
489
-			->setConstructorArgs([
490
-				'provisioning_api',
491
-				$this->request,
492
-				$this->userManager,
493
-				$this->config,
494
-				$this->groupManager,
495
-				$this->userSession,
496
-				$this->accountManager,
497
-				$this->subAdminManager,
498
-				$this->l10nFactory,
499
-				$this->rootFolder,
500
-				$this->urlGenerator,
501
-				$this->logger,
502
-				$this->newUserMailHelper,
503
-				$this->secureRandom,
504
-				$this->remoteWipe,
505
-				$this->knownUserService,
506
-				$this->eventDispatcher,
507
-				$this->phoneNumberUtil,
508
-				$this->appManager,
509
-			])
510
-			->onlyMethods(['editUser'])
511
-			->getMock();
512
-
513
-		$this->userManager
514
-			->expects($this->once())
515
-			->method('userExists')
516
-			->with('NewUser')
517
-			->willReturn(false);
518
-		$this->userManager
519
-			->expects($this->once())
520
-			->method('createUser')
521
-			->with('NewUser', 'PasswordOfTheNewUser')
522
-			->willReturn($this->createMock(IUser::class));
523
-		$this->logger
524
-			->expects($this->once())
525
-			->method('info')
526
-			->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
527
-		$loggedInUser = $this->getMockBuilder(IUser::class)
528
-			->disableOriginalConstructor()
529
-			->getMock();
530
-		$loggedInUser
531
-			->expects($this->any())
532
-			->method('getUID')
533
-			->willReturn('adminUser');
534
-		$this->userSession
535
-			->expects($this->any())
536
-			->method('getUser')
537
-			->willReturn($loggedInUser);
538
-		$this->groupManager
539
-			->expects($this->once())
540
-			->method('isAdmin')
541
-			->with('adminUser')
542
-			->willReturn(true);
543
-		$api
544
-			->expects($this->once())
545
-			->method('editUser')
546
-			->with('NewUser', 'display', 'DisplayNameOfTheNewUser');
547
-
548
-		$this->assertTrue(key_exists(
549
-			'id',
550
-			$api->addUser('NewUser', 'PasswordOfTheNewUser', 'DisplayNameOfTheNewUser')->getData()
551
-		));
552
-	}
553
-
554
-	public function testAddUserSuccessfulGenerateUserID(): void {
555
-		$this->config
556
-			->expects($this->any())
557
-			->method('getAppValue')
558
-			->willReturnCallback(function ($appid, $key, $default) {
559
-				if ($key === 'newUser.generateUserID') {
560
-					return 'yes';
561
-				}
562
-				return null;
563
-			});
564
-		$this->userManager
565
-			->expects($this->any())
566
-			->method('userExists')
567
-			->with($this->anything())
568
-			->willReturn(false);
569
-		$this->userManager
570
-			->expects($this->once())
571
-			->method('createUser')
572
-			->with($this->anything(), 'PasswordOfTheNewUser')
573
-			->willReturn($this->createMock(IUser::class));
574
-		$this->logger
575
-			->expects($this->once())
576
-			->method('info')
577
-			->with($this->stringStartsWith('Successful addUser call with userid: '), ['app' => 'ocs_api']);
578
-		$loggedInUser = $this->getMockBuilder(IUser::class)
579
-			->disableOriginalConstructor()
580
-			->getMock();
581
-		$loggedInUser
582
-			->expects($this->exactly(2))
583
-			->method('getUID')
584
-			->willReturn('adminUser');
585
-		$this->userSession
586
-			->expects($this->once())
587
-			->method('getUser')
588
-			->willReturn($loggedInUser);
589
-		$this->groupManager
590
-			->expects($this->once())
591
-			->method('isAdmin')
592
-			->with('adminUser')
593
-			->willReturn(true);
594
-		$this->secureRandom->expects($this->any())
595
-			->method('generate')
596
-			->with(10)
597
-			->willReturnCallback(function () {
598
-				return (string)rand(100000000, 999999999);
599
-			});
600
-
601
-		$this->assertTrue(key_exists(
602
-			'id',
603
-			$this->api->addUser('', 'PasswordOfTheNewUser')->getData()
604
-		));
605
-	}
606
-
607
-	public function testAddUserSuccessfulGeneratePassword(): void {
608
-		$this->userManager
609
-			->expects($this->once())
610
-			->method('userExists')
611
-			->with('NewUser')
612
-			->willReturn(false);
613
-		$newUser = $this->createMock(IUser::class);
614
-		$newUser->expects($this->once())
615
-			->method('setSystemEMailAddress');
616
-		$this->userManager
617
-			->expects($this->once())
618
-			->method('createUser')
619
-			->willReturn($newUser);
620
-		$this->logger
621
-			->expects($this->once())
622
-			->method('info')
623
-			->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
624
-		$loggedInUser = $this->getMockBuilder(IUser::class)
625
-			->disableOriginalConstructor()
626
-			->getMock();
627
-		$loggedInUser
628
-			->expects($this->exactly(2))
629
-			->method('getUID')
630
-			->willReturn('adminUser');
631
-		$this->userSession
632
-			->expects($this->once())
633
-			->method('getUser')
634
-			->willReturn($loggedInUser);
635
-		$this->groupManager
636
-			->expects($this->once())
637
-			->method('isAdmin')
638
-			->with('adminUser')
639
-			->willReturn(true);
640
-		$this->eventDispatcher
641
-			->expects($this->once())
642
-			->method('dispatchTyped')
643
-			->with(new GenerateSecurePasswordEvent());
644
-
645
-		$this->assertTrue(key_exists(
646
-			'id',
647
-			$this->api->addUser('NewUser', '', '', 'foo@bar')->getData()
648
-		));
649
-	}
650
-
651
-	public function testAddUserSuccessfulLowercaseEmail(): void {
652
-		$this->userManager
653
-			->expects($this->once())
654
-			->method('userExists')
655
-			->with('NewUser')
656
-			->willReturn(false);
657
-		$newUser = $this->createMock(IUser::class);
658
-		$newUser->expects($this->once())
659
-			->method('setSystemEMailAddress')
660
-			->with('[email protected]');
661
-		$this->userManager
662
-			->expects($this->once())
663
-			->method('createUser')
664
-			->willReturn($newUser);
665
-		$this->logger
666
-			->expects($this->once())
667
-			->method('info')
668
-			->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
669
-		$loggedInUser = $this->getMockBuilder(IUser::class)
670
-			->disableOriginalConstructor()
671
-			->getMock();
672
-		$loggedInUser
673
-			->expects($this->exactly(2))
674
-			->method('getUID')
675
-			->willReturn('adminUser');
676
-		$this->userSession
677
-			->expects($this->once())
678
-			->method('getUser')
679
-			->willReturn($loggedInUser);
680
-		$this->groupManager
681
-			->expects($this->once())
682
-			->method('isAdmin')
683
-			->with('adminUser')
684
-			->willReturn(true);
685
-		$this->eventDispatcher
686
-			->expects($this->once())
687
-			->method('dispatchTyped')
688
-			->with(new GenerateSecurePasswordEvent());
689
-
690
-		$this->assertTrue(key_exists(
691
-			'id',
692
-			$this->api->addUser('NewUser', '', '', '[email protected]')->getData()
693
-		));
694
-	}
695
-
696
-
697
-	public function testAddUserFailedToGenerateUserID(): void {
698
-		$this->expectException(OCSException::class);
699
-		$this->expectExceptionMessage('Could not create non-existing user ID');
700
-		$this->expectExceptionCode(111);
701
-
702
-		$this->config
703
-			->expects($this->any())
704
-			->method('getAppValue')
705
-			->willReturnCallback(function ($appid, $key, $default) {
706
-				if ($key === 'newUser.generateUserID') {
707
-					return 'yes';
708
-				}
709
-				return null;
710
-			});
711
-		$this->userManager
712
-			->expects($this->any())
713
-			->method('userExists')
714
-			->with($this->anything())
715
-			->willReturn(true);
716
-		$this->userManager
717
-			->expects($this->never())
718
-			->method('createUser');
719
-		$loggedInUser = $this->getMockBuilder(IUser::class)
720
-			->disableOriginalConstructor()
721
-			->getMock();
722
-		$loggedInUser
723
-			->expects($this->exactly(2))
724
-			->method('getUID')
725
-			->willReturn('adminUser');
726
-		$this->userSession
727
-			->expects($this->once())
728
-			->method('getUser')
729
-			->willReturn($loggedInUser);
730
-		$this->groupManager
731
-			->expects($this->once())
732
-			->method('isAdmin')
733
-			->with('adminUser')
734
-			->willReturn(true);
735
-
736
-		$this->api->addUser('', 'PasswordOfTheNewUser')->getData();
737
-	}
738
-
739
-
740
-	public function testAddUserEmailRequired(): void {
741
-		$this->expectException(OCSException::class);
742
-		$this->expectExceptionMessage('Required email address was not provided');
743
-		$this->expectExceptionCode(110);
744
-
745
-		$this->config
746
-			->expects($this->any())
747
-			->method('getAppValue')
748
-			->willReturnCallback(function ($appid, $key, $default) {
749
-				if ($key === 'newUser.requireEmail') {
750
-					return 'yes';
751
-				}
752
-				return null;
753
-			});
754
-		$this->userManager
755
-			->expects($this->once())
756
-			->method('userExists')
757
-			->with('NewUser')
758
-			->willReturn(false);
759
-		$this->userManager
760
-			->expects($this->never())
761
-			->method('createUser');
762
-		$loggedInUser = $this->getMockBuilder(IUser::class)
763
-			->disableOriginalConstructor()
764
-			->getMock();
765
-		$loggedInUser
766
-			->expects($this->exactly(2))
767
-			->method('getUID')
768
-			->willReturn('adminUser');
769
-		$this->userSession
770
-			->expects($this->once())
771
-			->method('getUser')
772
-			->willReturn($loggedInUser);
773
-		$this->groupManager
774
-			->expects($this->once())
775
-			->method('isAdmin')
776
-			->with('adminUser')
777
-			->willReturn(true);
778
-
779
-		$this->assertTrue(key_exists(
780
-			'id',
781
-			$this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
782
-		));
783
-	}
784
-
785
-	public function testAddUserExistingGroup(): void {
786
-		$this->userManager
787
-			->expects($this->once())
788
-			->method('userExists')
789
-			->with('NewUser')
790
-			->willReturn(false);
791
-		$loggedInUser = $this->getMockBuilder(IUser::class)
792
-			->disableOriginalConstructor()
793
-			->getMock();
794
-		$loggedInUser
795
-			->expects($this->exactly(2))
796
-			->method('getUID')
797
-			->willReturn('adminUser');
798
-		$this->userSession
799
-			->expects($this->once())
800
-			->method('getUser')
801
-			->willReturn($loggedInUser);
802
-		$this->groupManager
803
-			->expects($this->once())
804
-			->method('isAdmin')
805
-			->with('adminUser')
806
-			->willReturn(true);
807
-		$this->groupManager
808
-			->expects($this->once())
809
-			->method('groupExists')
810
-			->with('ExistingGroup')
811
-			->willReturn(true);
812
-		$user = $this->getMockBuilder(IUser::class)
813
-			->disableOriginalConstructor()
814
-			->getMock();
815
-		$this->userManager
816
-			->expects($this->once())
817
-			->method('createUser')
818
-			->with('NewUser', 'PasswordOfTheNewUser')
819
-			->willReturn($user);
820
-		$group = $this->getMockBuilder('OCP\IGroup')
821
-			->disableOriginalConstructor()
822
-			->getMock();
823
-		$group
824
-			->expects($this->once())
825
-			->method('addUser')
826
-			->with($user);
827
-		$this->groupManager
828
-			->expects($this->once())
829
-			->method('get')
830
-			->with('ExistingGroup')
831
-			->willReturn($group);
832
-
833
-		$calls = [
834
-			['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']],
835
-			['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']],
836
-		];
837
-		$this->logger
838
-			->expects($this->exactly(2))
839
-			->method('info')
840
-			->willReturnCallback(function () use (&$calls): void {
841
-				$expected = array_shift($calls);
842
-				$this->assertEquals($expected, func_get_args());
843
-			});
844
-
845
-		$this->assertArrayHasKey('id', $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData());
846
-	}
847
-
848
-
849
-	public function testAddUserUnsuccessful(): void {
850
-		$this->expectException(OCSException::class);
851
-		$this->expectExceptionMessage('Bad request');
852
-		$this->expectExceptionCode(101);
853
-
854
-		$exception = new Exception('User backend not found.');
855
-		$this->userManager
856
-			->expects($this->once())
857
-			->method('userExists')
858
-			->with('NewUser')
859
-			->willReturn(false);
860
-		$this->userManager
861
-			->expects($this->once())
862
-			->method('createUser')
863
-			->with('NewUser', 'PasswordOfTheNewUser')
864
-			->willThrowException($exception);
865
-		$this->logger
866
-			->expects($this->once())
867
-			->method('error')
868
-			->with(
869
-				'Failed addUser attempt with exception.',
870
-				[
871
-					'app' => 'ocs_api',
872
-					'exception' => $exception
873
-				]
874
-			);
875
-		$loggedInUser = $this->getMockBuilder(IUser::class)
876
-			->disableOriginalConstructor()
877
-			->getMock();
878
-		$loggedInUser
879
-			->expects($this->exactly(2))
880
-			->method('getUID')
881
-			->willReturn('adminUser');
882
-		$this->userSession
883
-			->expects($this->once())
884
-			->method('getUser')
885
-			->willReturn($loggedInUser);
886
-		$this->groupManager
887
-			->expects($this->once())
888
-			->method('isAdmin')
889
-			->with('adminUser')
890
-			->willReturn(true);
891
-
892
-		$this->api->addUser('NewUser', 'PasswordOfTheNewUser');
893
-	}
894
-
895
-
896
-	public function testAddUserAsSubAdminNoGroup(): void {
897
-		$this->expectException(OCSException::class);
898
-		$this->expectExceptionMessage('No group specified (required for sub-admins)');
899
-		$this->expectExceptionCode(106);
900
-
901
-		$loggedInUser = $this->getMockBuilder(IUser::class)
902
-			->disableOriginalConstructor()
903
-			->getMock();
904
-		$loggedInUser
905
-			->expects($this->exactly(2))
906
-			->method('getUID')
907
-			->willReturn('regularUser');
908
-		$this->userSession
909
-			->expects($this->once())
910
-			->method('getUser')
911
-			->willReturn($loggedInUser);
912
-		$this->groupManager
913
-			->expects($this->once())
914
-			->method('isAdmin')
915
-			->with('regularUser')
916
-			->willReturn(false);
917
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
918
-			->disableOriginalConstructor()->getMock();
919
-		$this->groupManager
920
-			->expects($this->once())
921
-			->method('getSubAdmin')
922
-			->with()
923
-			->willReturn($subAdminManager);
924
-
925
-		$this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', []);
926
-	}
927
-
928
-
929
-	public function testAddUserAsSubAdminValidGroupNotSubAdmin(): void {
930
-		$this->expectException(OCSException::class);
931
-		$this->expectExceptionMessage('Insufficient privileges for group ExistingGroup');
932
-		$this->expectExceptionCode(105);
933
-
934
-		$loggedInUser = $this->getMockBuilder(IUser::class)
935
-			->disableOriginalConstructor()
936
-			->getMock();
937
-		$loggedInUser
938
-			->expects($this->exactly(2))
939
-			->method('getUID')
940
-			->willReturn('regularUser');
941
-		$this->userSession
942
-			->expects($this->once())
943
-			->method('getUser')
944
-			->willReturn($loggedInUser);
945
-		$this->groupManager
946
-			->expects($this->once())
947
-			->method('isAdmin')
948
-			->with('regularUser')
949
-			->willReturn(false);
950
-		$existingGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
951
-		$this->groupManager
952
-			->expects($this->once())
953
-			->method('get')
954
-			->with('ExistingGroup')
955
-			->willReturn($existingGroup);
956
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
957
-			->disableOriginalConstructor()->getMock();
958
-		$subAdminManager
959
-			->expects($this->once())
960
-			->method('isSubAdminOfGroup')
961
-			->with($loggedInUser, $existingGroup)
962
-			->willReturn(false);
963
-		$this->groupManager
964
-			->expects($this->once())
965
-			->method('getSubAdmin')
966
-			->with()
967
-			->willReturn($subAdminManager);
968
-		$this->groupManager
969
-			->expects($this->once())
970
-			->method('groupExists')
971
-			->with('ExistingGroup')
972
-			->willReturn(true);
973
-
974
-		$this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData();
975
-	}
976
-
977
-	public function testAddUserAsSubAdminExistingGroups(): void {
978
-		$this->userManager
979
-			->expects($this->once())
980
-			->method('userExists')
981
-			->with('NewUser')
982
-			->willReturn(false);
983
-		$loggedInUser = $this->getMockBuilder(IUser::class)
984
-			->disableOriginalConstructor()
985
-			->getMock();
986
-		$loggedInUser
987
-			->expects($this->exactly(2))
988
-			->method('getUID')
989
-			->willReturn('subAdminUser');
990
-		$this->userSession
991
-			->expects($this->once())
992
-			->method('getUser')
993
-			->willReturn($loggedInUser);
994
-		$this->groupManager
995
-			->expects($this->once())
996
-			->method('isAdmin')
997
-			->with('subAdminUser')
998
-			->willReturn(false);
999
-		$this->groupManager
1000
-			->expects($this->exactly(2))
1001
-			->method('groupExists')
1002
-			->willReturnMap([
1003
-				['ExistingGroup1', true],
1004
-				['ExistingGroup2', true]
1005
-			]);
1006
-		$user = $this->getMockBuilder(IUser::class)
1007
-			->disableOriginalConstructor()
1008
-			->getMock();
1009
-		$this->userManager
1010
-			->expects($this->once())
1011
-			->method('createUser')
1012
-			->with('NewUser', 'PasswordOfTheNewUser')
1013
-			->willReturn($user);
1014
-		$existingGroup1 = $this->getMockBuilder('OCP\IGroup')
1015
-			->disableOriginalConstructor()
1016
-			->getMock();
1017
-		$existingGroup2 = $this->getMockBuilder('OCP\IGroup')
1018
-			->disableOriginalConstructor()
1019
-			->getMock();
1020
-		$existingGroup1
1021
-			->expects($this->once())
1022
-			->method('addUser')
1023
-			->with($user);
1024
-		$existingGroup2
1025
-			->expects($this->once())
1026
-			->method('addUser')
1027
-			->with($user);
1028
-		$this->groupManager
1029
-			->expects($this->exactly(4))
1030
-			->method('get')
1031
-			->willReturnMap([
1032
-				['ExistingGroup1', $existingGroup1],
1033
-				['ExistingGroup2', $existingGroup2]
1034
-			]);
1035
-
1036
-		$calls = [
1037
-			['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']],
1038
-			['Added userid NewUser to group ExistingGroup1', ['app' => 'ocs_api']],
1039
-			['Added userid NewUser to group ExistingGroup2', ['app' => 'ocs_api']],
1040
-		];
1041
-		$this->logger
1042
-			->expects($this->exactly(3))
1043
-			->method('info')
1044
-			->willReturnCallback(function () use (&$calls): void {
1045
-				$expected = array_shift($calls);
1046
-				$this->assertEquals($expected, func_get_args());
1047
-			});
1048
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1049
-			->disableOriginalConstructor()->getMock();
1050
-		$this->groupManager
1051
-			->expects($this->once())
1052
-			->method('getSubAdmin')
1053
-			->willReturn($subAdminManager);
1054
-		$subAdminManager
1055
-			->expects($this->exactly(2))
1056
-			->method('isSubAdminOfGroup')
1057
-			->willReturnMap([
1058
-				[$loggedInUser, $existingGroup1, true],
1059
-				[$loggedInUser, $existingGroup2, true],
1060
-			]);
1061
-
1062
-		$this->assertArrayHasKey('id', $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup1', 'ExistingGroup2'])->getData());
1063
-	}
1064
-
1065
-
1066
-	public function testGetUserTargetDoesNotExist(): void {
1067
-		$this->expectException(OCSException::class);
1068
-		$this->expectExceptionMessage('User does not exist');
1069
-		$this->expectExceptionCode(404);
1070
-
1071
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1072
-			->disableOriginalConstructor()
1073
-			->getMock();
1074
-		$this->userSession
1075
-			->method('getUser')
1076
-			->willReturn($loggedInUser);
1077
-		$this->userManager
1078
-			->expects($this->once())
1079
-			->method('get')
1080
-			->with('UserToGet')
1081
-			->willReturn(null);
1082
-
1083
-		$this->api->getUser('UserToGet');
1084
-	}
1085
-
1086
-	public function testGetUserDataAsAdmin(): void {
1087
-		$group0 = $this->createMock(IGroup::class);
1088
-		$group1 = $this->createMock(IGroup::class);
1089
-		$group2 = $this->createMock(IGroup::class);
1090
-		$group3 = $this->createMock(IGroup::class);
1091
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1092
-			->disableOriginalConstructor()
1093
-			->getMock();
1094
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1095
-			->disableOriginalConstructor()
1096
-			->getMock();
1097
-		$loggedInUser
1098
-			->method('getUID')
1099
-			->willReturn('admin');
1100
-		$targetUser = $this->getMockBuilder(IUser::class)
1101
-			->disableOriginalConstructor()
1102
-			->getMock();
1103
-		$targetUser->expects($this->once())
1104
-			->method('getSystemEMailAddress')
1105
-			->willReturn('[email protected]');
1106
-		$this->userSession
1107
-			->method('getUser')
1108
-			->willReturn($loggedInUser);
1109
-		$this->userManager
1110
-			->method('get')
1111
-			->with('UID')
1112
-			->willReturn($targetUser);
1113
-		$this->groupManager
1114
-			->method('isAdmin')
1115
-			->with('admin')
1116
-			->willReturn(true);
1117
-		$this->groupManager
1118
-			->expects($this->any())
1119
-			->method('getUserGroups')
1120
-			->willReturn([$group0, $group1, $group2]);
1121
-		$this->groupManager
1122
-			->expects($this->once())
1123
-			->method('getSubAdmin')
1124
-			->willReturn($subAdminManager);
1125
-		$subAdminManager
1126
-			->expects($this->once())
1127
-			->method('getSubAdminsGroups')
1128
-			->willReturn([$group3]);
1129
-		$group0->expects($this->once())
1130
-			->method('getGID')
1131
-			->willReturn('group0');
1132
-		$group1->expects($this->once())
1133
-			->method('getGID')
1134
-			->willReturn('group1');
1135
-		$group2->expects($this->once())
1136
-			->method('getGID')
1137
-			->willReturn('group2');
1138
-		$group3->expects($this->once())
1139
-			->method('getGID')
1140
-			->willReturn('group3');
1141
-
1142
-		$this->mockAccount($targetUser, [
1143
-			IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1144
-			IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1145
-			IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1146
-			IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1147
-			IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1148
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1149
-			IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1150
-			IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1151
-			IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1152
-			IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1153
-			IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1154
-			IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1155
-		]);
1156
-		$this->config
1157
-			->method('getUserValue')
1158
-			->willReturnMap([
1159
-				['UID', 'core', 'enabled', 'true', 'true'],
1160
-			]);
1161
-		$this->api
1162
-			->expects($this->once())
1163
-			->method('fillStorageInfo')
1164
-			->with($targetUser)
1165
-			->willReturn(['DummyValue']);
1166
-
1167
-		$backend = $this->createMock(UserInterface::class);
1168
-		$backend->expects($this->any())
1169
-			->method('implementsActions')
1170
-			->willReturn(true);
1171
-
1172
-		$targetUser
1173
-			->expects($this->once())
1174
-			->method('getDisplayName')
1175
-			->willReturn('Demo User');
1176
-		$targetUser
1177
-			->expects($this->once())
1178
-			->method('getHome')
1179
-			->willReturn('/var/www/newtcloud/data/UID');
1180
-		$targetUser
1181
-			->expects($this->exactly(2))
1182
-			->method('getLastLogin')
1183
-			->willReturn(1521191471);
1184
-		$targetUser
1185
-			->expects($this->once())
1186
-			->method('getFirstLogin')
1187
-			->willReturn(1511191471);
1188
-		$targetUser
1189
-			->expects($this->once())
1190
-			->method('getBackendClassName')
1191
-			->willReturn('Database');
1192
-		$targetUser
1193
-			->expects($this->once())
1194
-			->method('getBackend')
1195
-			->willReturn($backend);
1196
-		$targetUser
1197
-			->method('getUID')
1198
-			->willReturn('UID');
1199
-
1200
-		$this->l10nFactory
1201
-			->expects($this->once())
1202
-			->method('getUserLanguage')
1203
-			->with($targetUser)
1204
-			->willReturn('de');
1205
-
1206
-		$expected = [
1207
-			'id' => 'UID',
1208
-			'enabled' => true,
1209
-			'storageLocation' => '/var/www/newtcloud/data/UID',
1210
-			'firstLoginTimestamp' => 1511191471,
1211
-			'lastLoginTimestamp' => 1521191471,
1212
-			'lastLogin' => 1521191471000,
1213
-			'backend' => 'Database',
1214
-			'subadmin' => ['group3'],
1215
-			'quota' => ['DummyValue'],
1216
-			'email' => '[email protected]',
1217
-			'displayname' => 'Demo User',
1218
-			'display-name' => 'Demo User',
1219
-			'phone' => 'phone',
1220
-			'address' => 'address',
1221
-			'website' => 'website',
1222
-			'twitter' => 'twitter',
1223
-			'bluesky' => 'bluesky',
1224
-			'fediverse' => 'fediverse',
1225
-			'groups' => ['group0', 'group1', 'group2'],
1226
-			'language' => 'de',
1227
-			'locale' => null,
1228
-			'backendCapabilities' => [
1229
-				'setDisplayName' => true,
1230
-				'setPassword' => true,
1231
-			],
1232
-			'additional_mail' => [],
1233
-			'organisation' => 'organisation',
1234
-			'role' => 'role',
1235
-			'headline' => 'headline',
1236
-			'biography' => 'biography',
1237
-			'profile_enabled' => '1',
1238
-			'notify_email' => null,
1239
-			'manager' => '',
1240
-			'pronouns' => 'they/them',
1241
-		];
1242
-		$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1243
-	}
1244
-
1245
-	public function testGetUserDataAsSubAdminAndUserIsAccessible(): void {
1246
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1247
-			->disableOriginalConstructor()
1248
-			->getMock();
1249
-		$loggedInUser
1250
-			->method('getUID')
1251
-			->willReturn('subadmin');
1252
-		$targetUser = $this->getMockBuilder(IUser::class)
1253
-			->disableOriginalConstructor()
1254
-			->getMock();
1255
-		$targetUser
1256
-			->expects($this->once())
1257
-			->method('getSystemEMailAddress')
1258
-			->willReturn('[email protected]');
1259
-		$this->userSession
1260
-			->method('getUser')
1261
-			->willReturn($loggedInUser);
1262
-		$this->userManager
1263
-			->method('get')
1264
-			->with('UID')
1265
-			->willReturn($targetUser);
1266
-		$this->groupManager
1267
-			->method('isAdmin')
1268
-			->with('subadmin')
1269
-			->willReturn(false);
1270
-		$this->groupManager
1271
-			->expects($this->any())
1272
-			->method('getUserGroups')
1273
-			->willReturn([]);
1274
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1275
-			->disableOriginalConstructor()
1276
-			->getMock();
1277
-		$subAdminManager
1278
-			->expects($this->once())
1279
-			->method('isUserAccessible')
1280
-			->with($loggedInUser, $targetUser)
1281
-			->willReturn(true);
1282
-		$subAdminManager
1283
-			->expects($this->once())
1284
-			->method('getSubAdminsGroups')
1285
-			->willReturn([]);
1286
-		$this->groupManager
1287
-			->expects($this->exactly(2))
1288
-			->method('getSubAdmin')
1289
-			->willReturn($subAdminManager);
1290
-		$this->config
1291
-			->method('getUserValue')
1292
-			->willReturnMap([
1293
-				['UID', 'core', 'enabled', 'true', 'true'],
1294
-			]);
1295
-		$this->api
1296
-			->expects($this->once())
1297
-			->method('fillStorageInfo')
1298
-			->with($targetUser)
1299
-			->willReturn(['DummyValue']);
1300
-
1301
-		$backend = $this->createMock(UserInterface::class);
1302
-		$backend->expects($this->any())
1303
-			->method('implementsActions')
1304
-			->willReturn(true);
1305
-
1306
-		$targetUser
1307
-			->expects($this->once())
1308
-			->method('getDisplayName')
1309
-			->willReturn('Demo User');
1310
-		$targetUser
1311
-			->expects($this->never())
1312
-			->method('getHome');
1313
-		$targetUser
1314
-			->expects($this->exactly(2))
1315
-			->method('getLastLogin')
1316
-			->willReturn(1521191471);
1317
-		$targetUser
1318
-			->expects($this->once())
1319
-			->method('getFirstLogin')
1320
-			->willReturn(1511191471);
1321
-		$targetUser
1322
-			->expects($this->once())
1323
-			->method('getBackendClassName')
1324
-			->willReturn('Database');
1325
-		$targetUser
1326
-			->expects($this->once())
1327
-			->method('getBackend')
1328
-			->willReturn($backend);
1329
-		$targetUser
1330
-			->method('getUID')
1331
-			->willReturn('UID');
1332
-
1333
-		$this->mockAccount($targetUser, [
1334
-			IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1335
-			IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1336
-			IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1337
-			IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1338
-			IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1339
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1340
-			IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1341
-			IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1342
-			IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1343
-			IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1344
-			IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1345
-			IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1346
-		]);
1347
-
1348
-		$this->l10nFactory
1349
-			->expects($this->once())
1350
-			->method('getUserLanguage')
1351
-			->with($targetUser)
1352
-			->willReturn('da');
1353
-
1354
-		$expected = [
1355
-			'id' => 'UID',
1356
-			'enabled' => true,
1357
-			'firstLoginTimestamp' => 1511191471,
1358
-			'lastLoginTimestamp' => 1521191471,
1359
-			'lastLogin' => 1521191471000,
1360
-			'backend' => 'Database',
1361
-			'subadmin' => [],
1362
-			'quota' => ['DummyValue'],
1363
-			'email' => '[email protected]',
1364
-			'displayname' => 'Demo User',
1365
-			'display-name' => 'Demo User',
1366
-			'phone' => 'phone',
1367
-			'address' => 'address',
1368
-			'website' => 'website',
1369
-			'twitter' => 'twitter',
1370
-			'bluesky' => 'bluesky',
1371
-			'fediverse' => 'fediverse',
1372
-			'groups' => [],
1373
-			'language' => 'da',
1374
-			'locale' => null,
1375
-			'backendCapabilities' => [
1376
-				'setDisplayName' => true,
1377
-				'setPassword' => true,
1378
-			],
1379
-			'additional_mail' => [],
1380
-			'organisation' => 'organisation',
1381
-			'role' => 'role',
1382
-			'headline' => 'headline',
1383
-			'biography' => 'biography',
1384
-			'profile_enabled' => '1',
1385
-			'notify_email' => null,
1386
-			'manager' => '',
1387
-			'pronouns' => 'they/them',
1388
-		];
1389
-		$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1390
-	}
1391
-
1392
-
1393
-
1394
-	public function testGetUserDataAsSubAdminAndUserIsNotAccessible(): void {
1395
-		$this->expectException(OCSException::class);
1396
-		$this->expectExceptionCode(998);
1397
-
1398
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1399
-			->disableOriginalConstructor()
1400
-			->getMock();
1401
-		$loggedInUser
1402
-			->expects($this->exactly(4))
1403
-			->method('getUID')
1404
-			->willReturn('subadmin');
1405
-		$targetUser = $this->getMockBuilder(IUser::class)
1406
-			->disableOriginalConstructor()
1407
-			->getMock();
1408
-		$this->userSession
1409
-			->method('getUser')
1410
-			->willReturn($loggedInUser);
1411
-		$this->userManager
1412
-			->expects($this->once())
1413
-			->method('get')
1414
-			->with('UserToGet')
1415
-			->willReturn($targetUser);
1416
-		$this->groupManager
1417
-			->expects($this->once())
1418
-			->method('isAdmin')
1419
-			->with('subadmin')
1420
-			->willReturn(false);
1421
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1422
-			->disableOriginalConstructor()
1423
-			->getMock();
1424
-		$subAdminManager
1425
-			->expects($this->once())
1426
-			->method('isUserAccessible')
1427
-			->with($loggedInUser, $targetUser)
1428
-			->willReturn(false);
1429
-		$this->groupManager
1430
-			->expects($this->once())
1431
-			->method('getSubAdmin')
1432
-			->willReturn($subAdminManager);
1433
-
1434
-		$this->invokePrivate($this->api, 'getUser', ['UserToGet']);
1435
-	}
1436
-
1437
-	public function testGetUserDataAsSubAdminSelfLookup(): void {
1438
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1439
-			->disableOriginalConstructor()
1440
-			->getMock();
1441
-		$loggedInUser
1442
-			->method('getUID')
1443
-			->willReturn('UID');
1444
-		$targetUser = $this->getMockBuilder(IUser::class)
1445
-			->disableOriginalConstructor()
1446
-			->getMock();
1447
-		$this->userSession
1448
-			->method('getUser')
1449
-			->willReturn($loggedInUser);
1450
-		$this->userManager
1451
-			->method('get')
1452
-			->with('UID')
1453
-			->willReturn($targetUser);
1454
-		$this->groupManager
1455
-			->method('isAdmin')
1456
-			->with('UID')
1457
-			->willReturn(false);
1458
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1459
-			->disableOriginalConstructor()
1460
-			->getMock();
1461
-		$subAdminManager
1462
-			->expects($this->once())
1463
-			->method('isUserAccessible')
1464
-			->with($loggedInUser, $targetUser)
1465
-			->willReturn(false);
1466
-		$subAdminManager
1467
-			->expects($this->once())
1468
-			->method('getSubAdminsGroups')
1469
-			->willReturn([]);
1470
-		$this->groupManager
1471
-			->expects($this->exactly(2))
1472
-			->method('getSubAdmin')
1473
-			->willReturn($subAdminManager);
1474
-		$this->groupManager
1475
-			->expects($this->any())
1476
-			->method('getUserGroups')
1477
-			->willReturn([]);
1478
-		$this->api
1479
-			->expects($this->once())
1480
-			->method('fillStorageInfo')
1481
-			->with($targetUser)
1482
-			->willReturn(['DummyValue']);
1483
-
1484
-		$backend = $this->createMock(UserInterface::class);
1485
-		$backend->expects($this->atLeastOnce())
1486
-			->method('implementsActions')
1487
-			->willReturn(false);
1488
-
1489
-		$targetUser
1490
-			->expects($this->once())
1491
-			->method('getDisplayName')
1492
-			->willReturn('Subadmin User');
1493
-		$targetUser
1494
-			->expects($this->once())
1495
-			->method('getSystemEMailAddress')
1496
-			->willReturn('[email protected]');
1497
-		$targetUser
1498
-			->method('getUID')
1499
-			->willReturn('UID');
1500
-		$targetUser
1501
-			->expects($this->never())
1502
-			->method('getHome');
1503
-		$targetUser
1504
-			->expects($this->exactly(2))
1505
-			->method('getLastLogin')
1506
-			->willReturn(1521191471);
1507
-		$targetUser
1508
-			->expects($this->once())
1509
-			->method('getFirstLogin')
1510
-			->willReturn(1511191471);
1511
-		$targetUser
1512
-			->expects($this->once())
1513
-			->method('getBackendClassName')
1514
-			->willReturn('Database');
1515
-		$targetUser
1516
-			->expects($this->once())
1517
-			->method('getBackend')
1518
-			->willReturn($backend);
1519
-		$this->mockAccount($targetUser, [
1520
-			IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1521
-			IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1522
-			IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1523
-			IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1524
-			IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1525
-			IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1526
-			IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1527
-			IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1528
-			IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1529
-			IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1530
-			IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1531
-			IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1532
-		]);
1533
-
1534
-		$this->l10nFactory
1535
-			->expects($this->once())
1536
-			->method('getUserLanguage')
1537
-			->with($targetUser)
1538
-			->willReturn('ru');
1539
-
1540
-		$expected = [
1541
-			'id' => 'UID',
1542
-			'firstLoginTimestamp' => 1511191471,
1543
-			'lastLoginTimestamp' => 1521191471,
1544
-			'lastLogin' => 1521191471000,
1545
-			'backend' => 'Database',
1546
-			'subadmin' => [],
1547
-			'quota' => ['DummyValue'],
1548
-			'email' => '[email protected]',
1549
-			'displayname' => 'Subadmin User',
1550
-			'display-name' => 'Subadmin User',
1551
-			'phone' => 'phone',
1552
-			'address' => 'address',
1553
-			'website' => 'website',
1554
-			'twitter' => 'twitter',
1555
-			'bluesky' => 'bluesky',
1556
-			'fediverse' => 'fediverse',
1557
-			'groups' => [],
1558
-			'language' => 'ru',
1559
-			'locale' => null,
1560
-			'backendCapabilities' => [
1561
-				'setDisplayName' => false,
1562
-				'setPassword' => false,
1563
-			],
1564
-			'additional_mail' => [],
1565
-			'organisation' => 'organisation',
1566
-			'role' => 'role',
1567
-			'headline' => 'headline',
1568
-			'biography' => 'biography',
1569
-			'profile_enabled' => '1',
1570
-			'notify_email' => null,
1571
-			'manager' => '',
1572
-			'pronouns' => 'they/them',
1573
-		];
1574
-		$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1575
-	}
1576
-
1577
-	public static function dataSearchByPhoneNumbers(): array {
1578
-		return [
1579
-			'Invalid country' => ['Not a country code', ['12345' => ['NaN']], 400, null, null, []],
1580
-			'No number to search' => ['DE', ['12345' => ['NaN']], 200, null, null, []],
1581
-			'Valid number but no match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []],
1582
-			'Invalid number' => ['FR', ['12345' => ['0711 / 25 24 28-90']], 200, null, null, []],
1583
-			'Invalid and valid number' => ['DE', ['12345' => ['NaN', '0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []],
1584
-			'Valid and invalid number' => ['DE', ['12345' => ['0711 / 25 24 28-90', 'NaN']], 200, ['+4971125242890'], [], []],
1585
-			'Valid number and a match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['12345' => 'admin@localhost']],
1586
-			'Same number twice, later hits' => ['DE', ['12345' => ['0711 / 25 24 28-90'], '23456' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['23456' => 'admin@localhost']],
1587
-		];
1588
-	}
1589
-
1590
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSearchByPhoneNumbers')]
1591
-	public function testSearchByPhoneNumbers(string $location, array $search, int $status, ?array $searchUsers, ?array $userMatches, array $expected): void {
1592
-		$knownTo = 'knownTo';
1593
-		$user = $this->createMock(IUser::class);
1594
-		$user->method('getUID')
1595
-			->willReturn($knownTo);
1596
-		$this->userSession->method('getUser')
1597
-			->willReturn($user);
1598
-
1599
-		if ($searchUsers === null) {
1600
-			$this->accountManager->expects($this->never())
1601
-				->method('searchUsers');
1602
-		} else {
1603
-			$this->accountManager->expects($this->once())
1604
-				->method('searchUsers')
1605
-				->with(IAccountManager::PROPERTY_PHONE, $searchUsers)
1606
-				->willReturn($userMatches);
1607
-
1608
-			$this->knownUserService->expects($this->once())
1609
-				->method('deleteKnownTo')
1610
-				->with($knownTo);
1611
-
1612
-			$this->knownUserService->expects($this->exactly(count($expected)))
1613
-				->method('storeIsKnownToUser')
1614
-				->with($knownTo, $this->anything());
1615
-		}
1616
-
1617
-		$this->urlGenerator->method('getAbsoluteURL')
1618
-			->with('/')
1619
-			->willReturn('https://localhost/');
1620
-
1621
-		$response = $this->api->searchByPhoneNumbers($location, $search);
1622
-
1623
-		self::assertEquals($status, $response->getStatus());
1624
-		self::assertEquals($expected, $response->getData());
1625
-	}
1626
-
1627
-	public function testEditUserRegularUserSelfEditChangeDisplayName(): void {
1628
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1629
-			->disableOriginalConstructor()
1630
-			->getMock();
1631
-		$loggedInUser
1632
-			->expects($this->any())
1633
-			->method('getUID')
1634
-			->willReturn('UID');
1635
-		$targetUser = $this->getMockBuilder(IUser::class)
1636
-			->disableOriginalConstructor()
1637
-			->getMock();
1638
-		$this->userSession
1639
-			->expects($this->once())
1640
-			->method('getUser')
1641
-			->willReturn($loggedInUser);
1642
-		$this->userManager
1643
-			->expects($this->once())
1644
-			->method('get')
1645
-			->with('UserToEdit')
1646
-			->willReturn($targetUser);
1647
-		$targetUser
1648
-			->expects($this->once())
1649
-			->method('getBackend')
1650
-			->willReturn($this->createMock(ISetDisplayNameBackend::class));
1651
-		$targetUser
1652
-			->expects($this->once())
1653
-			->method('setDisplayName')
1654
-			->with('NewDisplayName')
1655
-			->willReturn(true);
1656
-		$targetUser
1657
-			->expects($this->any())
1658
-			->method('getUID')
1659
-			->willReturn('UID');
1660
-
1661
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'display', 'NewDisplayName')->getData());
1662
-	}
1663
-
1664
-	public function testEditUserRegularUserSelfEditChangeEmailValid(): void {
1665
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1666
-			->disableOriginalConstructor()
1667
-			->getMock();
1668
-		$loggedInUser
1669
-			->expects($this->any())
1670
-			->method('getUID')
1671
-			->willReturn('UID');
1672
-		$targetUser = $this->getMockBuilder(IUser::class)
1673
-			->disableOriginalConstructor()
1674
-			->getMock();
1675
-		$this->userSession
1676
-			->expects($this->once())
1677
-			->method('getUser')
1678
-			->willReturn($loggedInUser);
1679
-		$this->userManager
1680
-			->expects($this->once())
1681
-			->method('get')
1682
-			->with('UserToEdit')
1683
-			->willReturn($targetUser);
1684
-		$targetUser
1685
-			->expects($this->once())
1686
-			->method('setSystemEMailAddress')
1687
-			->with('[email protected]');
1688
-		$targetUser
1689
-			->expects($this->any())
1690
-			->method('getUID')
1691
-			->willReturn('UID');
1692
-
1693
-		$backend = $this->createMock(UserInterface::class);
1694
-		$targetUser
1695
-			->expects($this->any())
1696
-			->method('getBackend')
1697
-			->willReturn($backend);
1698
-
1699
-		$this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => $default);
1700
-
1701
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'email', '[email protected]')->getData());
1702
-	}
1703
-
1704
-	public function testEditUserRegularUserSelfEditAddAdditionalEmailValid(): void {
1705
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1706
-			->disableOriginalConstructor()
1707
-			->getMock();
1708
-		$loggedInUser
1709
-			->expects($this->any())
1710
-			->method('getUID')
1711
-			->willReturn('UID');
1712
-		$targetUser = $this->getMockBuilder(IUser::class)
1713
-			->disableOriginalConstructor()
1714
-			->getMock();
1715
-		$this->userSession
1716
-			->expects($this->once())
1717
-			->method('getUser')
1718
-			->willReturn($loggedInUser);
1719
-		$this->userManager
1720
-			->expects($this->once())
1721
-			->method('get')
1722
-			->with('UserToEdit')
1723
-			->willReturn($targetUser);
1724
-		$targetUser
1725
-			->expects($this->any())
1726
-			->method('getUID')
1727
-			->willReturn('UID');
1728
-
1729
-		$backend = $this->createMock(UserInterface::class);
1730
-		$targetUser
1731
-			->expects($this->any())
1732
-			->method('getBackend')
1733
-			->willReturn($backend);
1734
-
1735
-		$userAccount = $this->createMock(IAccount::class);
1736
-
1737
-		$this->accountManager
1738
-			->expects($this->once())
1739
-			->method('getAccount')
1740
-			->with($targetUser)
1741
-			->willReturn($userAccount);
1742
-		$this->accountManager
1743
-			->expects($this->once())
1744
-			->method('updateAccount')
1745
-			->with($userAccount);
1746
-
1747
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData());
1748
-	}
1749
-
1750
-	public function testEditUserRegularUserSelfEditAddAdditionalEmailMainAddress(): void {
1751
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1752
-			->disableOriginalConstructor()
1753
-			->getMock();
1754
-		$loggedInUser
1755
-			->expects($this->any())
1756
-			->method('getUID')
1757
-			->willReturn('UID');
1758
-		$targetUser = $this->getMockBuilder(IUser::class)
1759
-			->disableOriginalConstructor()
1760
-			->getMock();
1761
-		$this->userSession
1762
-			->expects($this->once())
1763
-			->method('getUser')
1764
-			->willReturn($loggedInUser);
1765
-		$this->userManager
1766
-			->expects($this->once())
1767
-			->method('get')
1768
-			->with('UserToEdit')
1769
-			->willReturn($targetUser);
1770
-		$targetUser
1771
-			->expects($this->any())
1772
-			->method('getUID')
1773
-			->willReturn('UID');
1774
-
1775
-		$backend = $this->createMock(UserInterface::class);
1776
-		$targetUser
1777
-			->expects($this->any())
1778
-			->method('getBackend')
1779
-			->willReturn($backend);
1780
-		$targetUser
1781
-			->expects($this->any())
1782
-			->method('getSystemEMailAddress')
1783
-			->willReturn('[email protected]');
1784
-
1785
-		$userAccount = $this->createMock(IAccount::class);
1786
-
1787
-		$this->accountManager
1788
-			->expects($this->never())
1789
-			->method('getAccount')
1790
-			->with($targetUser)
1791
-			->willReturn($userAccount);
1792
-		$this->accountManager
1793
-			->expects($this->never())
1794
-			->method('updateAccount')
1795
-			->with($userAccount);
1796
-
1797
-		$this->expectException(OCSException::class);
1798
-		$this->expectExceptionCode(101);
1799
-		$this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData();
1800
-	}
1801
-
1802
-	public function testEditUserRegularUserSelfEditAddAdditionalEmailDuplicate(): void {
1803
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1804
-			->disableOriginalConstructor()
1805
-			->getMock();
1806
-		$loggedInUser
1807
-			->expects($this->any())
1808
-			->method('getUID')
1809
-			->willReturn('UID');
1810
-		$targetUser = $this->getMockBuilder(IUser::class)
1811
-			->disableOriginalConstructor()
1812
-			->getMock();
1813
-		$this->userSession
1814
-			->expects($this->once())
1815
-			->method('getUser')
1816
-			->willReturn($loggedInUser);
1817
-		$this->userManager
1818
-			->expects($this->once())
1819
-			->method('get')
1820
-			->with('UserToEdit')
1821
-			->willReturn($targetUser);
1822
-		$targetUser
1823
-			->expects($this->any())
1824
-			->method('getUID')
1825
-			->willReturn('UID');
1826
-
1827
-		$backend = $this->createMock(UserInterface::class);
1828
-		$targetUser
1829
-			->expects($this->any())
1830
-			->method('getBackend')
1831
-			->willReturn($backend);
1832
-
1833
-		$property = $this->createMock(IAccountProperty::class);
1834
-		$property->method('getValue')
1835
-			->willReturn('[email protected]');
1836
-		$collection = $this->createMock(IAccountPropertyCollection::class);
1837
-		$collection->method('getPropertyByValue')
1838
-			->with('[email protected]')
1839
-			->willReturn($property);
1840
-
1841
-		$userAccount = $this->createMock(IAccount::class);
1842
-		$userAccount->method('getPropertyCollection')
1843
-			->with(IAccountManager::COLLECTION_EMAIL)
1844
-			->willReturn($collection);
1845
-
1846
-		$this->accountManager
1847
-			->expects($this->once())
1848
-			->method('getAccount')
1849
-			->with($targetUser)
1850
-			->willReturn($userAccount);
1851
-		$this->accountManager
1852
-			->expects($this->never())
1853
-			->method('updateAccount')
1854
-			->with($userAccount);
1855
-
1856
-		$this->expectException(OCSException::class);
1857
-		$this->expectExceptionCode(101);
1858
-		$this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData();
1859
-	}
1860
-
1861
-	public function testEditUserRegularUserSelfEditChangeEmailInvalid(): void {
1862
-		$this->expectException(OCSException::class);
1863
-		$this->expectExceptionCode(101);
1864
-
1865
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1866
-			->disableOriginalConstructor()
1867
-			->getMock();
1868
-		$loggedInUser
1869
-			->expects($this->any())
1870
-			->method('getUID')
1871
-			->willReturn('UID');
1872
-		$targetUser = $this->getMockBuilder(IUser::class)
1873
-			->disableOriginalConstructor()
1874
-			->getMock();
1875
-		$this->userSession
1876
-			->expects($this->once())
1877
-			->method('getUser')
1878
-			->willReturn($loggedInUser);
1879
-		$this->userManager
1880
-			->expects($this->once())
1881
-			->method('get')
1882
-			->with('UserToEdit')
1883
-			->willReturn($targetUser);
1884
-		$targetUser
1885
-			->expects($this->any())
1886
-			->method('getUID')
1887
-			->willReturn('UID');
1888
-
1889
-		$backend = $this->createMock(UserInterface::class);
1890
-		$targetUser
1891
-			->expects($this->any())
1892
-			->method('getBackend')
1893
-			->willReturn($backend);
1894
-
1895
-		$this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => $default);
1896
-
1897
-		$this->api->editUser('UserToEdit', 'email', 'demo.org');
1898
-	}
1899
-
1900
-	public static function selfEditChangePropertyProvider(): array {
1901
-		return [
1902
-			[IAccountManager::PROPERTY_TWITTER, '@oldtwitter', '@newtwitter'],
1903
-			[IAccountManager::PROPERTY_BLUESKY, 'old.bluesky', 'new.bluesky'],
1904
-			[IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', '@[email protected]'],
1905
-			[IAccountManager::PROPERTY_PHONE, '1234', '12345'],
1906
-			[IAccountManager::PROPERTY_ADDRESS, 'Something street 2', 'Another street 3'],
1907
-			[IAccountManager::PROPERTY_WEBSITE, 'https://examplesite1', 'https://examplesite2'],
1908
-			[IAccountManager::PROPERTY_ORGANISATION, 'Organisation A', 'Organisation B'],
1909
-			[IAccountManager::PROPERTY_ROLE, 'Human', 'Alien'],
1910
-			[IAccountManager::PROPERTY_HEADLINE, 'Hi', 'Hello'],
1911
-			[IAccountManager::PROPERTY_BIOGRAPHY, 'A biography', 'Another biography'],
1912
-			[IAccountManager::PROPERTY_PROFILE_ENABLED, '1', '0'],
1913
-			[IAccountManager::PROPERTY_PRONOUNS, 'they/them', 'he/him'],
1914
-		];
1915
-	}
1916
-
1917
-	#[\PHPUnit\Framework\Attributes\DataProvider('selfEditChangePropertyProvider')]
1918
-	public function testEditUserRegularUserSelfEditChangeProperty($propertyName, $oldValue, $newValue): void {
1919
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1920
-			->disableOriginalConstructor()
1921
-			->getMock();
1922
-		$loggedInUser
1923
-			->expects($this->any())
1924
-			->method('getUID')
1925
-			->willReturn('UID');
1926
-		$this->userSession
1927
-			->expects($this->once())
1928
-			->method('getUser')
1929
-			->willReturn($loggedInUser);
1930
-		$this->userManager
1931
-			->expects($this->once())
1932
-			->method('get')
1933
-			->with('UserToEdit')
1934
-			->willReturn($loggedInUser);
1935
-
1936
-		$backend = $this->createMock(UserInterface::class);
1937
-		$loggedInUser
1938
-			->expects($this->any())
1939
-			->method('getBackend')
1940
-			->willReturn($backend);
1941
-
1942
-		$propertyMock = $this->createMock(IAccountProperty::class);
1943
-		$propertyMock->expects($this->any())
1944
-			->method('getName')
1945
-			->willReturn($propertyName);
1946
-		$propertyMock->expects($this->any())
1947
-			->method('getValue')
1948
-			->willReturn($oldValue);
1949
-		$propertyMock->expects($this->once())
1950
-			->method('setValue')
1951
-			->with($newValue)
1952
-			->willReturnSelf();
1953
-		$propertyMock->expects($this->any())
1954
-			->method('getScope')
1955
-			->willReturn(IAccountManager::SCOPE_LOCAL);
1956
-
1957
-		$accountMock = $this->createMock(IAccount::class);
1958
-		$accountMock->expects($this->any())
1959
-			->method('getProperty')
1960
-			->with($propertyName)
1961
-			->willReturn($propertyMock);
1962
-
1963
-		$this->accountManager->expects($this->atLeastOnce())
1964
-			->method('getAccount')
1965
-			->with($loggedInUser)
1966
-			->willReturn($accountMock);
1967
-		$this->accountManager->expects($this->once())
1968
-			->method('updateAccount')
1969
-			->with($accountMock);
1970
-
1971
-		$this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName, $newValue)->getData());
1972
-	}
1973
-
1974
-	public function selfEditChangePropertyScopeProvider() {
1975
-		return [
1976
-			[IAccountManager::PROPERTY_AVATAR, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1977
-			[IAccountManager::PROPERTY_DISPLAYNAME, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1978
-			[IAccountManager::PROPERTY_EMAIL, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1979
-			[IAccountManager::PROPERTY_TWITTER, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1980
-			[IAccountManager::PROPERTY_BLUESKY, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1981
-			[IAccountManager::PROPERTY_FEDIVERSE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1982
-			[IAccountManager::PROPERTY_PHONE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1983
-			[IAccountManager::PROPERTY_ADDRESS, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1984
-			[IAccountManager::PROPERTY_WEBSITE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1985
-			[IAccountManager::PROPERTY_ORGANISATION, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1986
-			[IAccountManager::PROPERTY_ROLE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1987
-			[IAccountManager::PROPERTY_HEADLINE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1988
-			[IAccountManager::PROPERTY_BIOGRAPHY, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1989
-			[IAccountManager::PROPERTY_PROFILE_ENABLED, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1990
-			[IAccountManager::PROPERTY_PRONOUNS, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1991
-		];
1992
-	}
1993
-
1994
-	#[\PHPUnit\Framework\Attributes\DataProvider('selfEditChangePropertyProvider')]
1995
-	public function testEditUserRegularUserSelfEditChangePropertyScope($propertyName, $oldScope, $newScope): void {
1996
-		$loggedInUser = $this->getMockBuilder(IUser::class)
1997
-			->disableOriginalConstructor()
1998
-			->getMock();
1999
-		$loggedInUser
2000
-			->expects($this->any())
2001
-			->method('getUID')
2002
-			->willReturn('UID');
2003
-		$this->userSession
2004
-			->expects($this->once())
2005
-			->method('getUser')
2006
-			->willReturn($loggedInUser);
2007
-		$this->userManager
2008
-			->expects($this->once())
2009
-			->method('get')
2010
-			->with('UserToEdit')
2011
-			->willReturn($loggedInUser);
2012
-
2013
-		$backend = $this->createMock(UserInterface::class);
2014
-		$loggedInUser
2015
-			->expects($this->any())
2016
-			->method('getBackend')
2017
-			->willReturn($backend);
2018
-
2019
-		$propertyMock = $this->createMock(IAccountProperty::class);
2020
-		$propertyMock->expects($this->any())
2021
-			->method('getName')
2022
-			->willReturn($propertyName);
2023
-		$propertyMock->expects($this->any())
2024
-			->method('getValue')
2025
-			->willReturn('somevalue');
2026
-		$propertyMock->expects($this->any())
2027
-			->method('getScope')
2028
-			->willReturn($oldScope);
2029
-		$propertyMock->expects($this->atLeastOnce())
2030
-			->method('setScope')
2031
-			->with($newScope)
2032
-			->willReturnSelf();
2033
-
2034
-		$accountMock = $this->createMock(IAccount::class);
2035
-		$accountMock->expects($this->any())
2036
-			->method('getProperty')
2037
-			->with($propertyName)
2038
-			->willReturn($propertyMock);
2039
-
2040
-		$this->accountManager->expects($this->atLeastOnce())
2041
-			->method('getAccount')
2042
-			->with($loggedInUser)
2043
-			->willReturn($accountMock);
2044
-		$this->accountManager->expects($this->once())
2045
-			->method('updateAccount')
2046
-			->with($accountMock);
2047
-
2048
-		$this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName . 'Scope', $newScope)->getData());
2049
-	}
2050
-
2051
-	public function testEditUserRegularUserSelfEditChangePassword(): void {
2052
-		$loggedInUser = $this->getMockBuilder(IUser::class)
2053
-			->disableOriginalConstructor()
2054
-			->getMock();
2055
-		$loggedInUser
2056
-			->expects($this->any())
2057
-			->method('getUID')
2058
-			->willReturn('UID');
2059
-		$targetUser = $this->getMockBuilder(IUser::class)
2060
-			->disableOriginalConstructor()
2061
-			->getMock();
2062
-		$this->userSession
2063
-			->expects($this->once())
2064
-			->method('getUser')
2065
-			->willReturn($loggedInUser);
2066
-		$this->userManager
2067
-			->expects($this->once())
2068
-			->method('get')
2069
-			->with('UserToEdit')
2070
-			->willReturn($targetUser);
2071
-		$targetUser
2072
-			->expects($this->once())
2073
-			->method('canChangePassword')
2074
-			->willReturn(true);
2075
-		$targetUser
2076
-			->expects($this->once())
2077
-			->method('setPassword')
2078
-			->with('NewPassword');
2079
-		$targetUser
2080
-			->expects($this->any())
2081
-			->method('getUID')
2082
-			->willReturn('UID');
2083
-
2084
-		$backend = $this->createMock(UserInterface::class);
2085
-		$targetUser
2086
-			->expects($this->any())
2087
-			->method('getBackend')
2088
-			->willReturn($backend);
2089
-
2090
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'password', 'NewPassword')->getData());
2091
-	}
2092
-
2093
-
2094
-
2095
-	public function testEditUserRegularUserSelfEditChangeQuota(): void {
2096
-		$this->expectException(OCSException::class);
2097
-		$this->expectExceptionCode(113);
2098
-
2099
-		$loggedInUser = $this->getMockBuilder(IUser::class)
2100
-			->disableOriginalConstructor()
2101
-			->getMock();
2102
-		$loggedInUser
2103
-			->expects($this->any())
2104
-			->method('getUID')
2105
-			->willReturn('UID');
2106
-		$targetUser = $this->getMockBuilder(IUser::class)
2107
-			->disableOriginalConstructor()
2108
-			->getMock();
2109
-		$this->userSession
2110
-			->expects($this->once())
2111
-			->method('getUser')
2112
-			->willReturn($loggedInUser);
2113
-		$this->userManager
2114
-			->expects($this->once())
2115
-			->method('get')
2116
-			->with('UserToEdit')
2117
-			->willReturn($targetUser);
2118
-		$targetUser
2119
-			->expects($this->any())
2120
-			->method('getUID')
2121
-			->willReturn('UID');
2122
-
2123
-		$backend = $this->createMock(UserInterface::class);
2124
-		$targetUser
2125
-			->expects($this->any())
2126
-			->method('getBackend')
2127
-			->willReturn($backend);
2128
-
2129
-		$this->api->editUser('UserToEdit', 'quota', 'NewQuota');
2130
-	}
2131
-
2132
-	public function testEditUserAdminUserSelfEditChangeValidQuota(): void {
2133
-		$this->config
2134
-			->expects($this->once())
2135
-			->method('getAppValue')
2136
-			->willReturnCallback(function ($appid, $key, $default) {
2137
-				if ($key === 'max_quota') {
2138
-					return '-1';
2139
-				}
2140
-				return null;
2141
-			});
2142
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2143
-		$loggedInUser
2144
-			->expects($this->any())
2145
-			->method('getUID')
2146
-			->willReturn('UID');
2147
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2148
-		$targetUser->expects($this->once())
2149
-			->method('setQuota')
2150
-			->with('2.9 MB');
2151
-		$this->userSession
2152
-			->expects($this->once())
2153
-			->method('getUser')
2154
-			->willReturn($loggedInUser);
2155
-		$this->userManager
2156
-			->expects($this->once())
2157
-			->method('get')
2158
-			->with('UserToEdit')
2159
-			->willReturn($targetUser);
2160
-		$this->groupManager
2161
-			->expects($this->exactly(3))
2162
-			->method('isAdmin')
2163
-			->with('UID')
2164
-			->willReturn(true);
2165
-		$targetUser
2166
-			->expects($this->any())
2167
-			->method('getUID')
2168
-			->willReturn('UID');
2169
-
2170
-		$backend = $this->createMock(UserInterface::class);
2171
-		$targetUser
2172
-			->expects($this->any())
2173
-			->method('getBackend')
2174
-			->willReturn($backend);
2175
-
2176
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2177
-	}
2178
-
2179
-
2180
-
2181
-	public function testEditUserAdminUserSelfEditChangeInvalidQuota(): void {
2182
-		$this->expectException(OCSException::class);
2183
-		$this->expectExceptionMessage('Invalid quota value: ABC');
2184
-		$this->expectExceptionCode(101);
2185
-
2186
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2187
-		$loggedInUser
2188
-			->expects($this->any())
2189
-			->method('getUID')
2190
-			->willReturn('UID');
2191
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2192
-		$this->userSession
2193
-			->expects($this->once())
2194
-			->method('getUser')
2195
-			->willReturn($loggedInUser);
2196
-		$this->userManager
2197
-			->expects($this->once())
2198
-			->method('get')
2199
-			->with('UserToEdit')
2200
-			->willReturn($targetUser);
2201
-		$this->groupManager
2202
-			->expects($this->exactly(3))
2203
-			->method('isAdmin')
2204
-			->with('UID')
2205
-			->willReturn(true);
2206
-		$targetUser
2207
-			->expects($this->any())
2208
-			->method('getUID')
2209
-			->willReturn('UID');
2210
-
2211
-		$backend = $this->createMock(UserInterface::class);
2212
-		$targetUser
2213
-			->expects($this->any())
2214
-			->method('getBackend')
2215
-			->willReturn($backend);
2216
-
2217
-		$this->api->editUser('UserToEdit', 'quota', 'ABC');
2218
-	}
2219
-
2220
-	public function testEditUserAdminUserEditChangeValidQuota(): void {
2221
-		$this->config
2222
-			->expects($this->once())
2223
-			->method('getAppValue')
2224
-			->willReturnCallback(function ($appid, $key, $default) {
2225
-				if ($key === 'max_quota') {
2226
-					return '-1';
2227
-				}
2228
-				return null;
2229
-			});
2230
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2231
-		$loggedInUser
2232
-			->expects($this->any())
2233
-			->method('getUID')
2234
-			->willReturn('admin');
2235
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2236
-		$targetUser->expects($this->once())
2237
-			->method('setQuota')
2238
-			->with('2.9 MB');
2239
-		$this->userSession
2240
-			->expects($this->once())
2241
-			->method('getUser')
2242
-			->willReturn($loggedInUser);
2243
-		$this->userManager
2244
-			->expects($this->once())
2245
-			->method('get')
2246
-			->with('UserToEdit')
2247
-			->willReturn($targetUser);
2248
-		$this->groupManager
2249
-			->expects($this->once())
2250
-			->method('isAdmin')
2251
-			->with('admin')
2252
-			->willReturn(true);
2253
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2254
-			->disableOriginalConstructor()
2255
-			->getMock();
2256
-		$this->groupManager
2257
-			->expects($this->once())
2258
-			->method('getSubAdmin')
2259
-			->willReturn($subAdminManager);
2260
-		$targetUser
2261
-			->expects($this->any())
2262
-			->method('getUID')
2263
-			->willReturn('UID');
2264
-
2265
-		$backend = $this->createMock(UserInterface::class);
2266
-		$targetUser
2267
-			->expects($this->any())
2268
-			->method('getBackend')
2269
-			->willReturn($backend);
2270
-
2271
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2272
-	}
2273
-
2274
-	public function testEditUserSelfEditChangeLanguage(): void {
2275
-		$this->l10nFactory->expects($this->once())
2276
-			->method('findAvailableLanguages')
2277
-			->willReturn(['en', 'de', 'sv']);
2278
-		$this->config->expects($this->any())
2279
-			->method('getSystemValue')
2280
-			->willReturnMap([
2281
-				['allow_user_to_change_display_name', true, true],
2282
-				['force_language', false, false],
2283
-			]);
2284
-
2285
-		$loggedInUser = $this->createMock(IUser::class);
2286
-		$loggedInUser
2287
-			->expects($this->any())
2288
-			->method('getUID')
2289
-			->willReturn('UserToEdit');
2290
-		$targetUser = $this->createMock(IUser::class);
2291
-		$this->config->expects($this->once())
2292
-			->method('setUserValue')
2293
-			->with('UserToEdit', 'core', 'lang', 'de');
2294
-		$this->userSession
2295
-			->expects($this->once())
2296
-			->method('getUser')
2297
-			->willReturn($loggedInUser);
2298
-		$this->userManager
2299
-			->expects($this->once())
2300
-			->method('get')
2301
-			->with('UserToEdit')
2302
-			->willReturn($targetUser);
2303
-		$this->groupManager
2304
-			->expects($this->atLeastOnce())
2305
-			->method('isAdmin')
2306
-			->with('UserToEdit')
2307
-			->willReturn(false);
2308
-		$targetUser
2309
-			->expects($this->any())
2310
-			->method('getUID')
2311
-			->willReturn('UserToEdit');
2312
-
2313
-		$backend = $this->createMock(UserInterface::class);
2314
-		$targetUser
2315
-			->expects($this->any())
2316
-			->method('getBackend')
2317
-			->willReturn($backend);
2318
-
2319
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2320
-	}
2321
-
2322
-	public static function dataEditUserSelfEditChangeLanguageButForced(): array {
2323
-		return [
2324
-			['de'],
2325
-			[true],
2326
-		];
2327
-	}
2328
-
2329
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataEditUserSelfEditChangeLanguageButForced')]
2330
-	public function testEditUserSelfEditChangeLanguageButForced($forced): void {
2331
-		$this->expectException(OCSException::class);
2332
-
2333
-		$this->config->expects($this->any())
2334
-			->method('getSystemValue')
2335
-			->willReturnMap([
2336
-				['allow_user_to_change_display_name', true, true],
2337
-				['force_language', false, $forced],
2338
-			]);
2339
-
2340
-		$loggedInUser = $this->createMock(IUser::class);
2341
-		$loggedInUser
2342
-			->expects($this->any())
2343
-			->method('getUID')
2344
-			->willReturn('UserToEdit');
2345
-		$targetUser = $this->createMock(IUser::class);
2346
-		$this->config->expects($this->never())
2347
-			->method('setUserValue');
2348
-		$this->userSession
2349
-			->expects($this->once())
2350
-			->method('getUser')
2351
-			->willReturn($loggedInUser);
2352
-		$this->userManager
2353
-			->expects($this->once())
2354
-			->method('get')
2355
-			->with('UserToEdit')
2356
-			->willReturn($targetUser);
2357
-		$this->groupManager
2358
-			->expects($this->atLeastOnce())
2359
-			->method('isAdmin')
2360
-			->with('UserToEdit')
2361
-			->willReturn(false);
2362
-		$targetUser
2363
-			->expects($this->any())
2364
-			->method('getUID')
2365
-			->willReturn('UserToEdit');
2366
-
2367
-		$backend = $this->createMock(UserInterface::class);
2368
-		$targetUser
2369
-			->expects($this->any())
2370
-			->method('getBackend')
2371
-			->willReturn($backend);
2372
-
2373
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2374
-	}
2375
-
2376
-	public function testEditUserAdminEditChangeLanguage(): void {
2377
-		$this->l10nFactory->expects($this->once())
2378
-			->method('findAvailableLanguages')
2379
-			->willReturn(['en', 'de', 'sv']);
2380
-
2381
-		$loggedInUser = $this->createMock(IUser::class);
2382
-		$loggedInUser
2383
-			->expects($this->any())
2384
-			->method('getUID')
2385
-			->willReturn('admin');
2386
-		$targetUser = $this->createMock(IUser::class);
2387
-		$this->config->expects($this->once())
2388
-			->method('setUserValue')
2389
-			->with('UserToEdit', 'core', 'lang', 'de');
2390
-		$this->userSession
2391
-			->expects($this->once())
2392
-			->method('getUser')
2393
-			->willReturn($loggedInUser);
2394
-		$this->userManager
2395
-			->expects($this->once())
2396
-			->method('get')
2397
-			->with('UserToEdit')
2398
-			->willReturn($targetUser);
2399
-		$this->groupManager
2400
-			->expects($this->once())
2401
-			->method('isAdmin')
2402
-			->with('admin')
2403
-			->willReturn(true);
2404
-		$subAdminManager = $this->createMock(SubAdmin::class);
2405
-		$this->groupManager
2406
-			->expects($this->once())
2407
-			->method('getSubAdmin')
2408
-			->willReturn($subAdminManager);
2409
-		$targetUser
2410
-			->expects($this->any())
2411
-			->method('getUID')
2412
-			->willReturn('UserToEdit');
2413
-
2414
-		$backend = $this->createMock(UserInterface::class);
2415
-		$targetUser
2416
-			->expects($this->any())
2417
-			->method('getBackend')
2418
-			->willReturn($backend);
2419
-
2420
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2421
-	}
2422
-
2423
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataEditUserSelfEditChangeLanguageButForced')]
2424
-	public function testEditUserAdminEditChangeLanguageInvalidLanguage(): void {
2425
-		$this->expectException(OCSException::class);
2426
-
2427
-
2428
-		$this->l10nFactory->expects($this->once())
2429
-			->method('findAvailableLanguages')
2430
-			->willReturn(['en', 'de', 'sv']);
2431
-
2432
-		$loggedInUser = $this->createMock(IUser::class);
2433
-		$loggedInUser
2434
-			->expects($this->any())
2435
-			->method('getUID')
2436
-			->willReturn('admin');
2437
-		$targetUser = $this->createMock(IUser::class);
2438
-		$this->config->expects($this->never())
2439
-			->method('setUserValue');
2440
-		$this->userSession
2441
-			->expects($this->once())
2442
-			->method('getUser')
2443
-			->willReturn($loggedInUser);
2444
-		$this->userManager
2445
-			->expects($this->once())
2446
-			->method('get')
2447
-			->with('UserToEdit')
2448
-			->willReturn($targetUser);
2449
-		$this->groupManager
2450
-			->expects($this->once())
2451
-			->method('isAdmin')
2452
-			->with('admin')
2453
-			->willReturn(true);
2454
-		$subAdminManager = $this->createMock(SubAdmin::class);
2455
-		$this->groupManager
2456
-			->expects($this->once())
2457
-			->method('getSubAdmin')
2458
-			->willReturn($subAdminManager);
2459
-		$targetUser
2460
-			->expects($this->any())
2461
-			->method('getUID')
2462
-			->willReturn('UserToEdit');
2463
-
2464
-		$backend = $this->createMock(UserInterface::class);
2465
-		$targetUser
2466
-			->expects($this->any())
2467
-			->method('getBackend')
2468
-			->willReturn($backend);
2469
-
2470
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'ru')->getData());
2471
-	}
2472
-
2473
-	public function testEditUserSubadminUserAccessible(): void {
2474
-		$this->config
2475
-			->expects($this->once())
2476
-			->method('getAppValue')
2477
-			->willReturnCallback(function ($appid, $key, $default) {
2478
-				if ($key === 'max_quota') {
2479
-					return '-1';
2480
-				}
2481
-				return null;
2482
-			});
2483
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2484
-		$loggedInUser
2485
-			->expects($this->any())
2486
-			->method('getUID')
2487
-			->willReturn('subadmin');
2488
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2489
-		$targetUser->expects($this->once())
2490
-			->method('setQuota')
2491
-			->with('2.9 MB');
2492
-		$this->userSession
2493
-			->expects($this->once())
2494
-			->method('getUser')
2495
-			->willReturn($loggedInUser);
2496
-		$this->userManager
2497
-			->expects($this->once())
2498
-			->method('get')
2499
-			->with('UserToEdit')
2500
-			->willReturn($targetUser);
2501
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2502
-			->disableOriginalConstructor()
2503
-			->getMock();
2504
-		$subAdminManager
2505
-			->expects($this->once())
2506
-			->method('isUserAccessible')
2507
-			->with($loggedInUser, $targetUser)
2508
-			->willReturn(true);
2509
-		$this->groupManager
2510
-			->expects($this->once())
2511
-			->method('getSubAdmin')
2512
-			->willReturn($subAdminManager);
2513
-		$targetUser
2514
-			->expects($this->any())
2515
-			->method('getUID')
2516
-			->willReturn('UID');
2517
-
2518
-		$backend = $this->createMock(UserInterface::class);
2519
-		$targetUser
2520
-			->expects($this->any())
2521
-			->method('getBackend')
2522
-			->willReturn($backend);
2523
-
2524
-		$this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2525
-	}
2526
-
2527
-
2528
-	public function testEditUserSubadminUserInaccessible(): void {
2529
-		$this->expectException(OCSException::class);
2530
-		$this->expectExceptionCode(998);
2531
-
2532
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2533
-		$loggedInUser
2534
-			->expects($this->any())
2535
-			->method('getUID')
2536
-			->willReturn('subadmin');
2537
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2538
-		$this->userSession
2539
-			->expects($this->once())
2540
-			->method('getUser')
2541
-			->willReturn($loggedInUser);
2542
-		$this->userManager
2543
-			->expects($this->once())
2544
-			->method('get')
2545
-			->with('UserToEdit')
2546
-			->willReturn($targetUser);
2547
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2548
-			->disableOriginalConstructor()
2549
-			->getMock();
2550
-		$subAdminManager
2551
-			->expects($this->once())
2552
-			->method('isUserAccessible')
2553
-			->with($loggedInUser, $targetUser)
2554
-			->willReturn(false);
2555
-		$this->groupManager
2556
-			->expects($this->once())
2557
-			->method('getSubAdmin')
2558
-			->willReturn($subAdminManager);
2559
-		$targetUser
2560
-			->expects($this->any())
2561
-			->method('getUID')
2562
-			->willReturn('UID');
2563
-
2564
-		$this->api->editUser('UserToEdit', 'quota', 'value');
2565
-	}
2566
-
2567
-
2568
-	public function testDeleteUserNotExistingUser(): void {
2569
-		$this->expectException(OCSException::class);
2570
-		$this->expectExceptionCode(998);
2571
-
2572
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2573
-		$loggedInUser
2574
-			->expects($this->any())
2575
-			->method('getUID')
2576
-			->willReturn('UserToEdit');
2577
-		$this->userSession
2578
-			->expects($this->once())
2579
-			->method('getUser')
2580
-			->willReturn($loggedInUser);
2581
-		$this->userManager
2582
-			->expects($this->once())
2583
-			->method('get')
2584
-			->with('UserToDelete')
2585
-			->willReturn(null);
2586
-
2587
-		$this->api->deleteUser('UserToDelete');
2588
-	}
2589
-
2590
-
2591
-	public function testDeleteUserSelf(): void {
2592
-		$this->expectException(OCSException::class);
2593
-		$this->expectExceptionCode(101);
2594
-
2595
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2596
-		$loggedInUser
2597
-			->expects($this->any())
2598
-			->method('getUID')
2599
-			->willReturn('UID');
2600
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2601
-		$targetUser
2602
-			->expects($this->once())
2603
-			->method('getUID')
2604
-			->willReturn('UID');
2605
-		$this->userSession
2606
-			->expects($this->once())
2607
-			->method('getUser')
2608
-			->willReturn($loggedInUser);
2609
-		$this->userManager
2610
-			->expects($this->once())
2611
-			->method('get')
2612
-			->with('UserToDelete')
2613
-			->willReturn($targetUser);
2614
-
2615
-		$this->api->deleteUser('UserToDelete');
2616
-	}
2617
-
2618
-	public function testDeleteSuccessfulUserAsAdmin(): void {
2619
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2620
-		$loggedInUser
2621
-			->expects($this->any())
2622
-			->method('getUID')
2623
-			->willReturn('admin');
2624
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2625
-		$targetUser
2626
-			->expects($this->once())
2627
-			->method('getUID')
2628
-			->willReturn('UID');
2629
-		$this->userSession
2630
-			->expects($this->once())
2631
-			->method('getUser')
2632
-			->willReturn($loggedInUser);
2633
-		$this->userManager
2634
-			->expects($this->once())
2635
-			->method('get')
2636
-			->with('UserToDelete')
2637
-			->willReturn($targetUser);
2638
-		$this->groupManager
2639
-			->expects($this->once())
2640
-			->method('isAdmin')
2641
-			->with('admin')
2642
-			->willReturn(true);
2643
-		$targetUser
2644
-			->expects($this->once())
2645
-			->method('delete')
2646
-			->willReturn(true);
2647
-
2648
-		$this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData());
2649
-	}
2650
-
2651
-
2652
-	public function testDeleteUnsuccessfulUserAsAdmin(): void {
2653
-		$this->expectException(OCSException::class);
2654
-		$this->expectExceptionCode(101);
2655
-
2656
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2657
-		$loggedInUser
2658
-			->expects($this->any())
2659
-			->method('getUID')
2660
-			->willReturn('admin');
2661
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2662
-		$targetUser
2663
-			->expects($this->once())
2664
-			->method('getUID')
2665
-			->willReturn('UID');
2666
-		$this->userSession
2667
-			->expects($this->once())
2668
-			->method('getUser')
2669
-			->willReturn($loggedInUser);
2670
-		$this->userManager
2671
-			->expects($this->once())
2672
-			->method('get')
2673
-			->with('UserToDelete')
2674
-			->willReturn($targetUser);
2675
-		$this->groupManager
2676
-			->expects($this->once())
2677
-			->method('isAdmin')
2678
-			->with('admin')
2679
-			->willReturn(true);
2680
-		$targetUser
2681
-			->expects($this->once())
2682
-			->method('delete')
2683
-			->willReturn(false);
2684
-
2685
-		$this->api->deleteUser('UserToDelete');
2686
-	}
2687
-
2688
-	public function testDeleteSuccessfulUserAsSubadmin(): void {
2689
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2690
-		$loggedInUser
2691
-			->expects($this->any())
2692
-			->method('getUID')
2693
-			->willReturn('subadmin');
2694
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2695
-		$targetUser
2696
-			->expects($this->once())
2697
-			->method('getUID')
2698
-			->willReturn('UID');
2699
-		$this->userSession
2700
-			->expects($this->once())
2701
-			->method('getUser')
2702
-			->willReturn($loggedInUser);
2703
-		$this->userManager
2704
-			->expects($this->once())
2705
-			->method('get')
2706
-			->with('UserToDelete')
2707
-			->willReturn($targetUser);
2708
-		$this->groupManager
2709
-			->expects($this->once())
2710
-			->method('isAdmin')
2711
-			->with('subadmin')
2712
-			->willReturn(false);
2713
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2714
-			->disableOriginalConstructor()->getMock();
2715
-		$subAdminManager
2716
-			->expects($this->once())
2717
-			->method('isUserAccessible')
2718
-			->with($loggedInUser, $targetUser)
2719
-			->willReturn(true);
2720
-		$this->groupManager
2721
-			->expects($this->once())
2722
-			->method('getSubAdmin')
2723
-			->willReturn($subAdminManager);
2724
-		$targetUser
2725
-			->expects($this->once())
2726
-			->method('delete')
2727
-			->willReturn(true);
2728
-
2729
-		$this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData());
2730
-	}
2731
-
2732
-
2733
-	public function testDeleteUnsuccessfulUserAsSubadmin(): void {
2734
-		$this->expectException(OCSException::class);
2735
-		$this->expectExceptionCode(101);
2736
-
2737
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2738
-		$loggedInUser
2739
-			->expects($this->any())
2740
-			->method('getUID')
2741
-			->willReturn('subadmin');
2742
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2743
-		$targetUser
2744
-			->expects($this->once())
2745
-			->method('getUID')
2746
-			->willReturn('UID');
2747
-		$this->userSession
2748
-			->expects($this->once())
2749
-			->method('getUser')
2750
-			->willReturn($loggedInUser);
2751
-		$this->userManager
2752
-			->expects($this->once())
2753
-			->method('get')
2754
-			->with('UserToDelete')
2755
-			->willReturn($targetUser);
2756
-		$this->groupManager
2757
-			->expects($this->once())
2758
-			->method('isAdmin')
2759
-			->with('subadmin')
2760
-			->willReturn(false);
2761
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2762
-			->disableOriginalConstructor()->getMock();
2763
-		$subAdminManager
2764
-			->expects($this->once())
2765
-			->method('isUserAccessible')
2766
-			->with($loggedInUser, $targetUser)
2767
-			->willReturn(true);
2768
-		$this->groupManager
2769
-			->expects($this->once())
2770
-			->method('getSubAdmin')
2771
-			->willReturn($subAdminManager);
2772
-		$targetUser
2773
-			->expects($this->once())
2774
-			->method('delete')
2775
-			->willReturn(false);
2776
-
2777
-		$this->api->deleteUser('UserToDelete');
2778
-	}
2779
-
2780
-
2781
-	public function testDeleteUserAsSubAdminAndUserIsNotAccessible(): void {
2782
-		$this->expectException(OCSException::class);
2783
-		$this->expectExceptionCode(998);
2784
-
2785
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2786
-		$loggedInUser
2787
-			->expects($this->any())
2788
-			->method('getUID')
2789
-			->willReturn('subadmin');
2790
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2791
-		$targetUser
2792
-			->expects($this->once())
2793
-			->method('getUID')
2794
-			->willReturn('UID');
2795
-		$this->userSession
2796
-			->expects($this->once())
2797
-			->method('getUser')
2798
-			->willReturn($loggedInUser);
2799
-		$this->userManager
2800
-			->expects($this->once())
2801
-			->method('get')
2802
-			->with('UserToDelete')
2803
-			->willReturn($targetUser);
2804
-		$this->groupManager
2805
-			->expects($this->once())
2806
-			->method('isAdmin')
2807
-			->with('subadmin')
2808
-			->willReturn(false);
2809
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2810
-			->disableOriginalConstructor()->getMock();
2811
-		$subAdminManager
2812
-			->expects($this->once())
2813
-			->method('isUserAccessible')
2814
-			->with($loggedInUser, $targetUser)
2815
-			->willReturn(false);
2816
-		$this->groupManager
2817
-			->expects($this->once())
2818
-			->method('getSubAdmin')
2819
-			->willReturn($subAdminManager);
2820
-
2821
-		$this->api->deleteUser('UserToDelete');
2822
-	}
2823
-
2824
-
2825
-	public function testGetUsersGroupsTargetUserNotExisting(): void {
2826
-		$this->expectException(OCSException::class);
2827
-		$this->expectExceptionCode(998);
2828
-
2829
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2830
-		$this->userSession
2831
-			->expects($this->once())
2832
-			->method('getUser')
2833
-			->willReturn($loggedInUser);
2834
-
2835
-		$this->api->getUsersGroups('UserToLookup');
2836
-	}
2837
-
2838
-	public function testGetUsersGroupsSelfTargetted(): void {
2839
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2840
-		$loggedInUser
2841
-			->expects($this->exactly(3))
2842
-			->method('getUID')
2843
-			->willReturn('UserToLookup');
2844
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2845
-		$targetUser
2846
-			->expects($this->once())
2847
-			->method('getUID')
2848
-			->willReturn('UserToLookup');
2849
-		$this->userSession
2850
-			->expects($this->once())
2851
-			->method('getUser')
2852
-			->willReturn($loggedInUser);
2853
-		$this->userManager
2854
-			->expects($this->once())
2855
-			->method('get')
2856
-			->with('UserToLookup')
2857
-			->willReturn($targetUser);
2858
-		$this->groupManager
2859
-			->expects($this->once())
2860
-			->method('getUserGroupIds')
2861
-			->with($targetUser)
2862
-			->willReturn(['DummyValue']);
2863
-
2864
-		$this->assertEquals(['groups' => ['DummyValue']], $this->api->getUsersGroups('UserToLookup')->getData());
2865
-	}
2866
-
2867
-	public function testGetUsersGroupsForAdminUser(): void {
2868
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2869
-		$loggedInUser
2870
-			->expects($this->exactly(3))
2871
-			->method('getUID')
2872
-			->willReturn('admin');
2873
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2874
-		$targetUser
2875
-			->expects($this->once())
2876
-			->method('getUID')
2877
-			->willReturn('UserToLookup');
2878
-		$this->userSession
2879
-			->expects($this->once())
2880
-			->method('getUser')
2881
-			->willReturn($loggedInUser);
2882
-		$this->userManager
2883
-			->expects($this->once())
2884
-			->method('get')
2885
-			->with('UserToLookup')
2886
-			->willReturn($targetUser);
2887
-		$this->groupManager
2888
-			->expects($this->once())
2889
-			->method('getUserGroupIds')
2890
-			->with($targetUser)
2891
-			->willReturn(['DummyValue']);
2892
-		$this->groupManager
2893
-			->expects($this->once())
2894
-			->method('isAdmin')
2895
-			->with('admin')
2896
-			->willReturn(true);
2897
-
2898
-		$this->assertEquals(['groups' => ['DummyValue']], $this->api->getUsersGroups('UserToLookup')->getData());
2899
-	}
2900
-
2901
-	public function testGetUsersGroupsForSubAdminUserAndUserIsAccessible(): void {
2902
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2903
-		$loggedInUser
2904
-			->expects($this->exactly(3))
2905
-			->method('getUID')
2906
-			->willReturn('subadmin');
2907
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2908
-		$targetUser
2909
-			->expects($this->once())
2910
-			->method('getUID')
2911
-			->willReturn('UserToLookup');
2912
-		$this->userSession
2913
-			->expects($this->once())
2914
-			->method('getUser')
2915
-			->willReturn($loggedInUser);
2916
-		$this->userManager
2917
-			->expects($this->once())
2918
-			->method('get')
2919
-			->with('UserToLookup')
2920
-			->willReturn($targetUser);
2921
-		$this->groupManager
2922
-			->expects($this->once())
2923
-			->method('isAdmin')
2924
-			->with('subadmin')
2925
-			->willReturn(false);
2926
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2927
-			->disableOriginalConstructor()->getMock();
2928
-		$subAdminManager
2929
-			->expects($this->once())
2930
-			->method('isUserAccessible')
2931
-			->with($loggedInUser, $targetUser)
2932
-			->willReturn(true);
2933
-		$this->groupManager
2934
-			->expects($this->once())
2935
-			->method('getSubAdmin')
2936
-			->willReturn($subAdminManager);
2937
-		$group1 = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
2938
-		$group1
2939
-			->expects($this->any())
2940
-			->method('getGID')
2941
-			->willReturn('Group1');
2942
-		$group2 = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
2943
-		$group2
2944
-			->expects($this->any())
2945
-			->method('getGID')
2946
-			->willReturn('Group2');
2947
-		$subAdminManager
2948
-			->expects($this->once())
2949
-			->method('getSubAdminsGroups')
2950
-			->with($loggedInUser)
2951
-			->willReturn([$group1, $group2]);
2952
-		$this->groupManager
2953
-			->expects($this->any())
2954
-			->method('getUserGroupIds')
2955
-			->with($targetUser)
2956
-			->willReturn(['Group1']);
2957
-
2958
-		$this->assertEquals(['groups' => ['Group1']], $this->api->getUsersGroups('UserToLookup')->getData());
2959
-	}
2960
-
2961
-
2962
-	public function testGetUsersGroupsForSubAdminUserAndUserIsInaccessible(): void {
2963
-		$this->expectException(OCSException::class);
2964
-		$this->expectExceptionCode(998);
2965
-
2966
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2967
-		$loggedInUser
2968
-			->expects($this->exactly(3))
2969
-			->method('getUID')
2970
-			->willReturn('subadmin');
2971
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2972
-		$targetUser
2973
-			->expects($this->once())
2974
-			->method('getUID')
2975
-			->willReturn('UserToLookup');
2976
-		$this->userSession
2977
-			->expects($this->once())
2978
-			->method('getUser')
2979
-			->willReturn($loggedInUser);
2980
-		$this->userManager
2981
-			->expects($this->once())
2982
-			->method('get')
2983
-			->with('UserToLookup')
2984
-			->willReturn($targetUser);
2985
-		$this->groupManager
2986
-			->expects($this->once())
2987
-			->method('isAdmin')
2988
-			->with('subadmin')
2989
-			->willReturn(false);
2990
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2991
-			->disableOriginalConstructor()->getMock();
2992
-		$subAdminManager
2993
-			->expects($this->once())
2994
-			->method('isUserAccessible')
2995
-			->with($loggedInUser, $targetUser)
2996
-			->willReturn(false);
2997
-		$this->groupManager
2998
-			->expects($this->once())
2999
-			->method('getSubAdmin')
3000
-			->willReturn($subAdminManager);
3001
-		$this->groupManager
3002
-			->expects($this->any())
3003
-			->method('getUserGroupIds')
3004
-			->with($targetUser)
3005
-			->willReturn(['Group1']);
3006
-
3007
-		$this->api->getUsersGroups('UserToLookup');
3008
-	}
3009
-
3010
-
3011
-	public function testAddToGroupWithTargetGroupNotExisting(): void {
3012
-		$this->expectException(OCSException::class);
3013
-		$this->expectExceptionCode(102);
3014
-
3015
-		$this->groupManager->expects($this->once())
3016
-			->method('get')
3017
-			->with('GroupToAddTo')
3018
-			->willReturn(null);
3019
-
3020
-		$this->api->addToGroup('TargetUser', 'GroupToAddTo');
3021
-	}
3022
-
3023
-
3024
-	public function testAddToGroupWithNoGroupSpecified(): void {
3025
-		$this->expectException(OCSException::class);
3026
-		$this->expectExceptionCode(101);
3027
-
3028
-		$this->api->addToGroup('TargetUser');
3029
-	}
3030
-
3031
-
3032
-	public function testAddToGroupWithTargetUserNotExisting(): void {
3033
-		$this->expectException(OCSException::class);
3034
-		$this->expectExceptionCode(103);
3035
-
3036
-		$targetGroup = $this->createMock(IGroup::class);
3037
-		$this->groupManager->expects($this->once())
3038
-			->method('get')
3039
-			->with('GroupToAddTo')
3040
-			->willReturn($targetGroup);
3041
-
3042
-		$this->api->addToGroup('TargetUser', 'GroupToAddTo');
3043
-	}
3044
-
3045
-
3046
-	public function testAddToGroupNoSubadmin(): void {
3047
-		$this->expectException(OCSException::class);
3048
-		$this->expectExceptionCode(104);
3049
-
3050
-		$targetUser = $this->createMock(IUser::class);
3051
-		$loggedInUser = $this->createMock(IUser::class);
3052
-		$loggedInUser->expects($this->exactly(2))
3053
-			->method('getUID')
3054
-			->willReturn('subadmin');
3055
-
3056
-		$targetGroup = $this->createMock(IGroup::class);
3057
-		$targetGroup->expects($this->never())
3058
-			->method('addUser')
3059
-			->with($targetUser);
3060
-
3061
-		$this->groupManager->expects($this->once())
3062
-			->method('get')
3063
-			->with('GroupToAddTo')
3064
-			->willReturn($targetGroup);
3065
-
3066
-
3067
-		$subAdminManager = $this->createMock(SubAdmin::class);
3068
-		$subAdminManager->expects($this->once())
3069
-			->method('isSubAdminOfGroup')
3070
-			->with($loggedInUser, $targetGroup)
3071
-			->willReturn(false);
3072
-
3073
-		$this->groupManager->expects($this->once())
3074
-			->method('getSubAdmin')
3075
-			->willReturn($subAdminManager);
3076
-		$this->groupManager->expects($this->once())
3077
-			->method('isAdmin')
3078
-			->with('subadmin')
3079
-			->willReturn(false);
3080
-
3081
-		$this->userManager->expects($this->once())
3082
-			->method('get')
3083
-			->with('TargetUser')
3084
-			->willReturn($targetUser);
3085
-
3086
-		$this->userSession->expects($this->once())
3087
-			->method('getUser')
3088
-			->willReturn($loggedInUser);
3089
-
3090
-		$this->api->addToGroup('TargetUser', 'GroupToAddTo');
3091
-	}
3092
-
3093
-	public function testAddToGroupSuccessAsSubadmin(): void {
3094
-		$targetUser = $this->createMock(IUser::class);
3095
-		$loggedInUser = $this->createMock(IUser::class);
3096
-		$loggedInUser->expects($this->exactly(2))
3097
-			->method('getUID')
3098
-			->willReturn('subadmin');
3099
-
3100
-		$targetGroup = $this->createMock(IGroup::class);
3101
-		$targetGroup->expects($this->once())
3102
-			->method('addUser')
3103
-			->with($targetUser);
3104
-
3105
-		$this->groupManager->expects($this->once())
3106
-			->method('get')
3107
-			->with('GroupToAddTo')
3108
-			->willReturn($targetGroup);
50
+    protected IUserManager&MockObject $userManager;
51
+    protected IConfig&MockObject $config;
52
+    protected Manager&MockObject $groupManager;
53
+    protected IUserSession&MockObject $userSession;
54
+    protected LoggerInterface&MockObject $logger;
55
+    protected UsersController&MockObject $api;
56
+    protected IAccountManager&MockObject $accountManager;
57
+    protected ISubAdmin&MockObject $subAdminManager;
58
+    protected IURLGenerator&MockObject $urlGenerator;
59
+    protected IRequest&MockObject $request;
60
+    private IFactory&MockObject $l10nFactory;
61
+    private NewUserMailHelper&MockObject $newUserMailHelper;
62
+    private ISecureRandom&MockObject $secureRandom;
63
+    private RemoteWipe&MockObject $remoteWipe;
64
+    private KnownUserService&MockObject $knownUserService;
65
+    private IEventDispatcher&MockObject $eventDispatcher;
66
+    private IRootFolder $rootFolder;
67
+    private IPhoneNumberUtil $phoneNumberUtil;
68
+    private IAppManager $appManager;
69
+
70
+    protected function setUp(): void {
71
+        parent::setUp();
72
+
73
+        $this->userManager = $this->createMock(IUserManager::class);
74
+        $this->config = $this->createMock(IConfig::class);
75
+        $this->groupManager = $this->createMock(Manager::class);
76
+        $this->userSession = $this->createMock(IUserSession::class);
77
+        $this->logger = $this->createMock(LoggerInterface::class);
78
+        $this->request = $this->createMock(IRequest::class);
79
+        $this->accountManager = $this->createMock(IAccountManager::class);
80
+        $this->subAdminManager = $this->createMock(ISubAdmin::class);
81
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
82
+        $this->l10nFactory = $this->createMock(IFactory::class);
83
+        $this->newUserMailHelper = $this->createMock(NewUserMailHelper::class);
84
+        $this->secureRandom = $this->createMock(ISecureRandom::class);
85
+        $this->remoteWipe = $this->createMock(RemoteWipe::class);
86
+        $this->knownUserService = $this->createMock(KnownUserService::class);
87
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
88
+        $this->phoneNumberUtil = new PhoneNumberUtil();
89
+        $this->appManager = $this->createMock(IAppManager::class);
90
+        $this->rootFolder = $this->createMock(IRootFolder::class);
91
+
92
+        $l10n = $this->createMock(IL10N::class);
93
+        $l10n->method('t')->willReturnCallback(fn (string $txt, array $replacement = []) => sprintf($txt, ...$replacement));
94
+        $this->l10nFactory->method('get')->with('provisioning_api')->willReturn($l10n);
95
+
96
+        $this->api = $this->getMockBuilder(UsersController::class)
97
+            ->setConstructorArgs([
98
+                'provisioning_api',
99
+                $this->request,
100
+                $this->userManager,
101
+                $this->config,
102
+                $this->groupManager,
103
+                $this->userSession,
104
+                $this->accountManager,
105
+                $this->subAdminManager,
106
+                $this->l10nFactory,
107
+                $this->rootFolder,
108
+                $this->urlGenerator,
109
+                $this->logger,
110
+                $this->newUserMailHelper,
111
+                $this->secureRandom,
112
+                $this->remoteWipe,
113
+                $this->knownUserService,
114
+                $this->eventDispatcher,
115
+                $this->phoneNumberUtil,
116
+                $this->appManager,
117
+            ])
118
+            ->onlyMethods(['fillStorageInfo'])
119
+            ->getMock();
120
+    }
121
+
122
+    public function testGetUsersAsAdmin(): void {
123
+        $loggedInUser = $this->getMockBuilder(IUser::class)
124
+            ->disableOriginalConstructor()
125
+            ->getMock();
126
+        $loggedInUser
127
+            ->expects($this->once())
128
+            ->method('getUID')
129
+            ->willReturn('admin');
130
+        $this->userSession
131
+            ->expects($this->once())
132
+            ->method('getUser')
133
+            ->willReturn($loggedInUser);
134
+        $this->groupManager
135
+            ->expects($this->once())
136
+            ->method('isAdmin')
137
+            ->willReturn(true);
138
+        $this->userManager
139
+            ->expects($this->once())
140
+            ->method('search')
141
+            ->with('MyCustomSearch')
142
+            ->willReturn(['Admin' => [], 'Foo' => [], 'Bar' => []]);
143
+
144
+        $expected = [
145
+            'users' => [
146
+                'Admin',
147
+                'Foo',
148
+                'Bar',
149
+            ],
150
+        ];
151
+        $this->assertEquals($expected, $this->api->getUsers('MyCustomSearch')->getData());
152
+    }
153
+
154
+    public function testGetUsersAsSubAdmin(): void {
155
+        $loggedInUser = $this->getMockBuilder(IUser::class)
156
+            ->disableOriginalConstructor()
157
+            ->getMock();
158
+        $loggedInUser
159
+            ->expects($this->once())
160
+            ->method('getUID')
161
+            ->willReturn('subadmin');
162
+        $this->userSession
163
+            ->expects($this->once())
164
+            ->method('getUser')
165
+            ->willReturn($loggedInUser);
166
+        $this->groupManager
167
+            ->expects($this->once())
168
+            ->method('isAdmin')
169
+            ->willReturn(false);
170
+        $firstGroup = $this->getMockBuilder('OCP\IGroup')
171
+            ->disableOriginalConstructor()
172
+            ->getMock();
173
+        $firstGroup
174
+            ->expects($this->once())
175
+            ->method('getGID')
176
+            ->willReturn('FirstGroup');
177
+        $secondGroup = $this->getMockBuilder('OCP\IGroup')
178
+            ->disableOriginalConstructor()
179
+            ->getMock();
180
+        $secondGroup
181
+            ->expects($this->once())
182
+            ->method('getGID')
183
+            ->willReturn('SecondGroup');
184
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
185
+            ->disableOriginalConstructor()->getMock();
186
+        $subAdminManager
187
+            ->expects($this->once())
188
+            ->method('isSubAdmin')
189
+            ->with($loggedInUser)
190
+            ->willReturn(true);
191
+        $subAdminManager
192
+            ->expects($this->once())
193
+            ->method('getSubAdminsGroups')
194
+            ->with($loggedInUser)
195
+            ->willReturn([$firstGroup, $secondGroup]);
196
+        $this->groupManager
197
+            ->expects($this->once())
198
+            ->method('getSubAdmin')
199
+            ->willReturn($subAdminManager);
200
+        $this->groupManager
201
+            ->expects($this->any())
202
+            ->method('displayNamesInGroup')->willReturnOnConsecutiveCalls(['AnotherUserInTheFirstGroup' => []], ['UserInTheSecondGroup' => []]);
203
+
204
+        $expected = [
205
+            'users' => [
206
+                'AnotherUserInTheFirstGroup',
207
+                'UserInTheSecondGroup',
208
+            ],
209
+        ];
210
+        $this->assertEquals($expected, $this->api->getUsers('MyCustomSearch')->getData());
211
+    }
212
+
213
+    private function createUserMock(string $uid, bool $enabled): MockObject&IUser {
214
+        $mockUser = $this->getMockBuilder(IUser::class)
215
+            ->disableOriginalConstructor()
216
+            ->getMock();
217
+        $mockUser
218
+            ->method('getUID')
219
+            ->willReturn($uid);
220
+        $mockUser
221
+            ->method('isEnabled')
222
+            ->willReturn($enabled);
223
+        return $mockUser;
224
+    }
225
+
226
+    public function testGetDisabledUsersAsAdmin(): void {
227
+        $loggedInUser = $this->getMockBuilder(IUser::class)
228
+            ->disableOriginalConstructor()
229
+            ->getMock();
230
+        $loggedInUser
231
+            ->expects($this->once())
232
+            ->method('getUID')
233
+            ->willReturn('admin');
234
+        $this->userSession
235
+            ->expects($this->atLeastOnce())
236
+            ->method('getUser')
237
+            ->willReturn($loggedInUser);
238
+        $this->groupManager
239
+            ->expects($this->once())
240
+            ->method('isAdmin')
241
+            ->willReturn(true);
242
+        $this->userManager
243
+            ->expects($this->once())
244
+            ->method('getDisabledUsers')
245
+            ->with(3, 0, 'MyCustomSearch')
246
+            ->willReturn([
247
+                $this->createUserMock('admin', false),
248
+                $this->createUserMock('foo', false),
249
+                $this->createUserMock('bar', false),
250
+            ]);
251
+
252
+        $expected = [
253
+            'users' => [
254
+                'admin' => ['id' => 'admin'],
255
+                'foo' => ['id' => 'foo'],
256
+                'bar' => ['id' => 'bar'],
257
+            ],
258
+        ];
259
+        $this->assertEquals($expected, $this->api->getDisabledUsersDetails('MyCustomSearch', 3)->getData());
260
+    }
261
+
262
+    public function testGetDisabledUsersAsSubAdmin(): void {
263
+        $loggedInUser = $this->getMockBuilder(IUser::class)
264
+            ->disableOriginalConstructor()
265
+            ->getMock();
266
+        $loggedInUser
267
+            ->expects($this->once())
268
+            ->method('getUID')
269
+            ->willReturn('subadmin');
270
+        $this->userSession
271
+            ->expects($this->atLeastOnce())
272
+            ->method('getUser')
273
+            ->willReturn($loggedInUser);
274
+        $this->groupManager
275
+            ->expects($this->once())
276
+            ->method('isAdmin')
277
+            ->willReturn(false);
278
+        $firstGroup = $this->getMockBuilder('OCP\IGroup')
279
+            ->disableOriginalConstructor()
280
+            ->getMock();
281
+        $secondGroup = $this->getMockBuilder('OCP\IGroup')
282
+            ->disableOriginalConstructor()
283
+            ->getMock();
284
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
285
+            ->disableOriginalConstructor()->getMock();
286
+        $subAdminManager
287
+            ->expects($this->once())
288
+            ->method('isSubAdmin')
289
+            ->with($loggedInUser)
290
+            ->willReturn(true);
291
+        $subAdminManager
292
+            ->expects($this->once())
293
+            ->method('getSubAdminsGroups')
294
+            ->with($loggedInUser)
295
+            ->willReturn([$firstGroup, $secondGroup]);
296
+        $this->groupManager
297
+            ->expects($this->once())
298
+            ->method('getSubAdmin')
299
+            ->willReturn($subAdminManager);
300
+        $this->groupManager
301
+            ->expects($this->never())
302
+            ->method('displayNamesInGroup');
303
+
304
+        $firstGroup
305
+            ->expects($this->once())
306
+            ->method('searchUsers')
307
+            ->with('MyCustomSearch')
308
+            ->willReturn([
309
+                $this->createUserMock('user1', false),
310
+                $this->createUserMock('bob', true),
311
+                $this->createUserMock('user2', false),
312
+                $this->createUserMock('alice', true),
313
+            ]);
314
+
315
+        $secondGroup
316
+            ->expects($this->once())
317
+            ->method('searchUsers')
318
+            ->with('MyCustomSearch')
319
+            ->willReturn([
320
+                $this->createUserMock('user2', false),
321
+                $this->createUserMock('joe', true),
322
+                $this->createUserMock('user3', false),
323
+                $this->createUserMock('jim', true),
324
+                $this->createUserMock('john', true),
325
+            ]);
326
+
327
+
328
+        $expected = [
329
+            'users' => [
330
+                'user1' => ['id' => 'user1'],
331
+                'user2' => ['id' => 'user2'],
332
+                'user3' => ['id' => 'user3'],
333
+            ],
334
+        ];
335
+        $this->assertEquals($expected, $this->api->getDisabledUsersDetails('MyCustomSearch', 3)->getData());
336
+    }
337
+
338
+
339
+    public function testAddUserAlreadyExisting(): void {
340
+        $this->expectException(OCSException::class);
341
+        $this->expectExceptionCode(102);
342
+
343
+        $this->userManager
344
+            ->expects($this->once())
345
+            ->method('userExists')
346
+            ->with('AlreadyExistingUser')
347
+            ->willReturn(true);
348
+        $this->logger
349
+            ->expects($this->once())
350
+            ->method('error')
351
+            ->with('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
352
+        $loggedInUser = $this->getMockBuilder(IUser::class)
353
+            ->disableOriginalConstructor()
354
+            ->getMock();
355
+        $loggedInUser
356
+            ->expects($this->exactly(2))
357
+            ->method('getUID')
358
+            ->willReturn('adminUser');
359
+        $this->userSession
360
+            ->expects($this->once())
361
+            ->method('getUser')
362
+            ->willReturn($loggedInUser);
363
+        $this->groupManager
364
+            ->expects($this->once())
365
+            ->method('isAdmin')
366
+            ->with('adminUser')
367
+            ->willReturn(true);
368
+
369
+        $this->api->addUser('AlreadyExistingUser', 'password', '', '', []);
370
+    }
371
+
372
+
373
+    public function testAddUserNonExistingGroup(): void {
374
+        $this->expectException(OCSException::class);
375
+        $this->expectExceptionMessage('Group NonExistingGroup does not exist');
376
+        $this->expectExceptionCode(104);
377
+
378
+        $this->userManager
379
+            ->expects($this->once())
380
+            ->method('userExists')
381
+            ->with('NewUser')
382
+            ->willReturn(false);
383
+        $loggedInUser = $this->getMockBuilder(IUser::class)
384
+            ->disableOriginalConstructor()
385
+            ->getMock();
386
+        $loggedInUser
387
+            ->expects($this->exactly(2))
388
+            ->method('getUID')
389
+            ->willReturn('adminUser');
390
+        $this->userSession
391
+            ->expects($this->once())
392
+            ->method('getUser')
393
+            ->willReturn($loggedInUser);
394
+        $this->groupManager
395
+            ->expects($this->once())
396
+            ->method('isAdmin')
397
+            ->with('adminUser')
398
+            ->willReturn(true);
399
+        $this->groupManager
400
+            ->expects($this->once())
401
+            ->method('groupExists')
402
+            ->with('NonExistingGroup')
403
+            ->willReturn(false);
404
+
405
+        $this->api->addUser('NewUser', 'pass', '', '', ['NonExistingGroup']);
406
+    }
407
+
408
+
409
+    public function testAddUserExistingGroupNonExistingGroup(): void {
410
+        $this->expectException(OCSException::class);
411
+        $this->expectExceptionMessage('Group NonExistingGroup does not exist');
412
+        $this->expectExceptionCode(104);
413
+
414
+        $this->userManager
415
+            ->expects($this->once())
416
+            ->method('userExists')
417
+            ->with('NewUser')
418
+            ->willReturn(false);
419
+        $loggedInUser = $this->getMockBuilder(IUser::class)
420
+            ->disableOriginalConstructor()
421
+            ->getMock();
422
+        $loggedInUser
423
+            ->expects($this->exactly(2))
424
+            ->method('getUID')
425
+            ->willReturn('adminUser');
426
+        $this->userSession
427
+            ->expects($this->once())
428
+            ->method('getUser')
429
+            ->willReturn($loggedInUser);
430
+        $this->groupManager
431
+            ->expects($this->once())
432
+            ->method('isAdmin')
433
+            ->with('adminUser')
434
+            ->willReturn(true);
435
+        $this->groupManager
436
+            ->expects($this->exactly(2))
437
+            ->method('groupExists')
438
+            ->willReturnMap([
439
+                ['ExistingGroup', true],
440
+                ['NonExistingGroup', false]
441
+            ]);
442
+
443
+        $this->api->addUser('NewUser', 'pass', '', '', ['ExistingGroup', 'NonExistingGroup']);
444
+    }
445
+
446
+    public function testAddUserSuccessful(): void {
447
+        $this->userManager
448
+            ->expects($this->once())
449
+            ->method('userExists')
450
+            ->with('NewUser')
451
+            ->willReturn(false);
452
+        $this->userManager
453
+            ->expects($this->once())
454
+            ->method('createUser')
455
+            ->with('NewUser', 'PasswordOfTheNewUser')
456
+            ->willReturn($this->createMock(IUser::class));
457
+        $this->logger
458
+            ->expects($this->once())
459
+            ->method('info')
460
+            ->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
461
+        $loggedInUser = $this->getMockBuilder(IUser::class)
462
+            ->disableOriginalConstructor()
463
+            ->getMock();
464
+        $loggedInUser
465
+            ->expects($this->exactly(2))
466
+            ->method('getUID')
467
+            ->willReturn('adminUser');
468
+        $this->userSession
469
+            ->expects($this->once())
470
+            ->method('getUser')
471
+            ->willReturn($loggedInUser);
472
+        $this->groupManager
473
+            ->expects($this->once())
474
+            ->method('isAdmin')
475
+            ->with('adminUser')
476
+            ->willReturn(true);
477
+
478
+        $this->assertTrue(key_exists(
479
+            'id',
480
+            $this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
481
+        ));
482
+    }
483
+
484
+    public function testAddUserSuccessfulWithDisplayName(): void {
485
+        /**
486
+         * @var UserController
487
+         */
488
+        $api = $this->getMockBuilder(UsersController::class)
489
+            ->setConstructorArgs([
490
+                'provisioning_api',
491
+                $this->request,
492
+                $this->userManager,
493
+                $this->config,
494
+                $this->groupManager,
495
+                $this->userSession,
496
+                $this->accountManager,
497
+                $this->subAdminManager,
498
+                $this->l10nFactory,
499
+                $this->rootFolder,
500
+                $this->urlGenerator,
501
+                $this->logger,
502
+                $this->newUserMailHelper,
503
+                $this->secureRandom,
504
+                $this->remoteWipe,
505
+                $this->knownUserService,
506
+                $this->eventDispatcher,
507
+                $this->phoneNumberUtil,
508
+                $this->appManager,
509
+            ])
510
+            ->onlyMethods(['editUser'])
511
+            ->getMock();
512
+
513
+        $this->userManager
514
+            ->expects($this->once())
515
+            ->method('userExists')
516
+            ->with('NewUser')
517
+            ->willReturn(false);
518
+        $this->userManager
519
+            ->expects($this->once())
520
+            ->method('createUser')
521
+            ->with('NewUser', 'PasswordOfTheNewUser')
522
+            ->willReturn($this->createMock(IUser::class));
523
+        $this->logger
524
+            ->expects($this->once())
525
+            ->method('info')
526
+            ->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
527
+        $loggedInUser = $this->getMockBuilder(IUser::class)
528
+            ->disableOriginalConstructor()
529
+            ->getMock();
530
+        $loggedInUser
531
+            ->expects($this->any())
532
+            ->method('getUID')
533
+            ->willReturn('adminUser');
534
+        $this->userSession
535
+            ->expects($this->any())
536
+            ->method('getUser')
537
+            ->willReturn($loggedInUser);
538
+        $this->groupManager
539
+            ->expects($this->once())
540
+            ->method('isAdmin')
541
+            ->with('adminUser')
542
+            ->willReturn(true);
543
+        $api
544
+            ->expects($this->once())
545
+            ->method('editUser')
546
+            ->with('NewUser', 'display', 'DisplayNameOfTheNewUser');
547
+
548
+        $this->assertTrue(key_exists(
549
+            'id',
550
+            $api->addUser('NewUser', 'PasswordOfTheNewUser', 'DisplayNameOfTheNewUser')->getData()
551
+        ));
552
+    }
553
+
554
+    public function testAddUserSuccessfulGenerateUserID(): void {
555
+        $this->config
556
+            ->expects($this->any())
557
+            ->method('getAppValue')
558
+            ->willReturnCallback(function ($appid, $key, $default) {
559
+                if ($key === 'newUser.generateUserID') {
560
+                    return 'yes';
561
+                }
562
+                return null;
563
+            });
564
+        $this->userManager
565
+            ->expects($this->any())
566
+            ->method('userExists')
567
+            ->with($this->anything())
568
+            ->willReturn(false);
569
+        $this->userManager
570
+            ->expects($this->once())
571
+            ->method('createUser')
572
+            ->with($this->anything(), 'PasswordOfTheNewUser')
573
+            ->willReturn($this->createMock(IUser::class));
574
+        $this->logger
575
+            ->expects($this->once())
576
+            ->method('info')
577
+            ->with($this->stringStartsWith('Successful addUser call with userid: '), ['app' => 'ocs_api']);
578
+        $loggedInUser = $this->getMockBuilder(IUser::class)
579
+            ->disableOriginalConstructor()
580
+            ->getMock();
581
+        $loggedInUser
582
+            ->expects($this->exactly(2))
583
+            ->method('getUID')
584
+            ->willReturn('adminUser');
585
+        $this->userSession
586
+            ->expects($this->once())
587
+            ->method('getUser')
588
+            ->willReturn($loggedInUser);
589
+        $this->groupManager
590
+            ->expects($this->once())
591
+            ->method('isAdmin')
592
+            ->with('adminUser')
593
+            ->willReturn(true);
594
+        $this->secureRandom->expects($this->any())
595
+            ->method('generate')
596
+            ->with(10)
597
+            ->willReturnCallback(function () {
598
+                return (string)rand(100000000, 999999999);
599
+            });
600
+
601
+        $this->assertTrue(key_exists(
602
+            'id',
603
+            $this->api->addUser('', 'PasswordOfTheNewUser')->getData()
604
+        ));
605
+    }
606
+
607
+    public function testAddUserSuccessfulGeneratePassword(): void {
608
+        $this->userManager
609
+            ->expects($this->once())
610
+            ->method('userExists')
611
+            ->with('NewUser')
612
+            ->willReturn(false);
613
+        $newUser = $this->createMock(IUser::class);
614
+        $newUser->expects($this->once())
615
+            ->method('setSystemEMailAddress');
616
+        $this->userManager
617
+            ->expects($this->once())
618
+            ->method('createUser')
619
+            ->willReturn($newUser);
620
+        $this->logger
621
+            ->expects($this->once())
622
+            ->method('info')
623
+            ->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
624
+        $loggedInUser = $this->getMockBuilder(IUser::class)
625
+            ->disableOriginalConstructor()
626
+            ->getMock();
627
+        $loggedInUser
628
+            ->expects($this->exactly(2))
629
+            ->method('getUID')
630
+            ->willReturn('adminUser');
631
+        $this->userSession
632
+            ->expects($this->once())
633
+            ->method('getUser')
634
+            ->willReturn($loggedInUser);
635
+        $this->groupManager
636
+            ->expects($this->once())
637
+            ->method('isAdmin')
638
+            ->with('adminUser')
639
+            ->willReturn(true);
640
+        $this->eventDispatcher
641
+            ->expects($this->once())
642
+            ->method('dispatchTyped')
643
+            ->with(new GenerateSecurePasswordEvent());
644
+
645
+        $this->assertTrue(key_exists(
646
+            'id',
647
+            $this->api->addUser('NewUser', '', '', 'foo@bar')->getData()
648
+        ));
649
+    }
650
+
651
+    public function testAddUserSuccessfulLowercaseEmail(): void {
652
+        $this->userManager
653
+            ->expects($this->once())
654
+            ->method('userExists')
655
+            ->with('NewUser')
656
+            ->willReturn(false);
657
+        $newUser = $this->createMock(IUser::class);
658
+        $newUser->expects($this->once())
659
+            ->method('setSystemEMailAddress')
660
+            ->with('[email protected]');
661
+        $this->userManager
662
+            ->expects($this->once())
663
+            ->method('createUser')
664
+            ->willReturn($newUser);
665
+        $this->logger
666
+            ->expects($this->once())
667
+            ->method('info')
668
+            ->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']);
669
+        $loggedInUser = $this->getMockBuilder(IUser::class)
670
+            ->disableOriginalConstructor()
671
+            ->getMock();
672
+        $loggedInUser
673
+            ->expects($this->exactly(2))
674
+            ->method('getUID')
675
+            ->willReturn('adminUser');
676
+        $this->userSession
677
+            ->expects($this->once())
678
+            ->method('getUser')
679
+            ->willReturn($loggedInUser);
680
+        $this->groupManager
681
+            ->expects($this->once())
682
+            ->method('isAdmin')
683
+            ->with('adminUser')
684
+            ->willReturn(true);
685
+        $this->eventDispatcher
686
+            ->expects($this->once())
687
+            ->method('dispatchTyped')
688
+            ->with(new GenerateSecurePasswordEvent());
689
+
690
+        $this->assertTrue(key_exists(
691
+            'id',
692
+            $this->api->addUser('NewUser', '', '', '[email protected]')->getData()
693
+        ));
694
+    }
695
+
696
+
697
+    public function testAddUserFailedToGenerateUserID(): void {
698
+        $this->expectException(OCSException::class);
699
+        $this->expectExceptionMessage('Could not create non-existing user ID');
700
+        $this->expectExceptionCode(111);
701
+
702
+        $this->config
703
+            ->expects($this->any())
704
+            ->method('getAppValue')
705
+            ->willReturnCallback(function ($appid, $key, $default) {
706
+                if ($key === 'newUser.generateUserID') {
707
+                    return 'yes';
708
+                }
709
+                return null;
710
+            });
711
+        $this->userManager
712
+            ->expects($this->any())
713
+            ->method('userExists')
714
+            ->with($this->anything())
715
+            ->willReturn(true);
716
+        $this->userManager
717
+            ->expects($this->never())
718
+            ->method('createUser');
719
+        $loggedInUser = $this->getMockBuilder(IUser::class)
720
+            ->disableOriginalConstructor()
721
+            ->getMock();
722
+        $loggedInUser
723
+            ->expects($this->exactly(2))
724
+            ->method('getUID')
725
+            ->willReturn('adminUser');
726
+        $this->userSession
727
+            ->expects($this->once())
728
+            ->method('getUser')
729
+            ->willReturn($loggedInUser);
730
+        $this->groupManager
731
+            ->expects($this->once())
732
+            ->method('isAdmin')
733
+            ->with('adminUser')
734
+            ->willReturn(true);
735
+
736
+        $this->api->addUser('', 'PasswordOfTheNewUser')->getData();
737
+    }
738
+
739
+
740
+    public function testAddUserEmailRequired(): void {
741
+        $this->expectException(OCSException::class);
742
+        $this->expectExceptionMessage('Required email address was not provided');
743
+        $this->expectExceptionCode(110);
744
+
745
+        $this->config
746
+            ->expects($this->any())
747
+            ->method('getAppValue')
748
+            ->willReturnCallback(function ($appid, $key, $default) {
749
+                if ($key === 'newUser.requireEmail') {
750
+                    return 'yes';
751
+                }
752
+                return null;
753
+            });
754
+        $this->userManager
755
+            ->expects($this->once())
756
+            ->method('userExists')
757
+            ->with('NewUser')
758
+            ->willReturn(false);
759
+        $this->userManager
760
+            ->expects($this->never())
761
+            ->method('createUser');
762
+        $loggedInUser = $this->getMockBuilder(IUser::class)
763
+            ->disableOriginalConstructor()
764
+            ->getMock();
765
+        $loggedInUser
766
+            ->expects($this->exactly(2))
767
+            ->method('getUID')
768
+            ->willReturn('adminUser');
769
+        $this->userSession
770
+            ->expects($this->once())
771
+            ->method('getUser')
772
+            ->willReturn($loggedInUser);
773
+        $this->groupManager
774
+            ->expects($this->once())
775
+            ->method('isAdmin')
776
+            ->with('adminUser')
777
+            ->willReturn(true);
778
+
779
+        $this->assertTrue(key_exists(
780
+            'id',
781
+            $this->api->addUser('NewUser', 'PasswordOfTheNewUser')->getData()
782
+        ));
783
+    }
784
+
785
+    public function testAddUserExistingGroup(): void {
786
+        $this->userManager
787
+            ->expects($this->once())
788
+            ->method('userExists')
789
+            ->with('NewUser')
790
+            ->willReturn(false);
791
+        $loggedInUser = $this->getMockBuilder(IUser::class)
792
+            ->disableOriginalConstructor()
793
+            ->getMock();
794
+        $loggedInUser
795
+            ->expects($this->exactly(2))
796
+            ->method('getUID')
797
+            ->willReturn('adminUser');
798
+        $this->userSession
799
+            ->expects($this->once())
800
+            ->method('getUser')
801
+            ->willReturn($loggedInUser);
802
+        $this->groupManager
803
+            ->expects($this->once())
804
+            ->method('isAdmin')
805
+            ->with('adminUser')
806
+            ->willReturn(true);
807
+        $this->groupManager
808
+            ->expects($this->once())
809
+            ->method('groupExists')
810
+            ->with('ExistingGroup')
811
+            ->willReturn(true);
812
+        $user = $this->getMockBuilder(IUser::class)
813
+            ->disableOriginalConstructor()
814
+            ->getMock();
815
+        $this->userManager
816
+            ->expects($this->once())
817
+            ->method('createUser')
818
+            ->with('NewUser', 'PasswordOfTheNewUser')
819
+            ->willReturn($user);
820
+        $group = $this->getMockBuilder('OCP\IGroup')
821
+            ->disableOriginalConstructor()
822
+            ->getMock();
823
+        $group
824
+            ->expects($this->once())
825
+            ->method('addUser')
826
+            ->with($user);
827
+        $this->groupManager
828
+            ->expects($this->once())
829
+            ->method('get')
830
+            ->with('ExistingGroup')
831
+            ->willReturn($group);
832
+
833
+        $calls = [
834
+            ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']],
835
+            ['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']],
836
+        ];
837
+        $this->logger
838
+            ->expects($this->exactly(2))
839
+            ->method('info')
840
+            ->willReturnCallback(function () use (&$calls): void {
841
+                $expected = array_shift($calls);
842
+                $this->assertEquals($expected, func_get_args());
843
+            });
844
+
845
+        $this->assertArrayHasKey('id', $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData());
846
+    }
847
+
848
+
849
+    public function testAddUserUnsuccessful(): void {
850
+        $this->expectException(OCSException::class);
851
+        $this->expectExceptionMessage('Bad request');
852
+        $this->expectExceptionCode(101);
853
+
854
+        $exception = new Exception('User backend not found.');
855
+        $this->userManager
856
+            ->expects($this->once())
857
+            ->method('userExists')
858
+            ->with('NewUser')
859
+            ->willReturn(false);
860
+        $this->userManager
861
+            ->expects($this->once())
862
+            ->method('createUser')
863
+            ->with('NewUser', 'PasswordOfTheNewUser')
864
+            ->willThrowException($exception);
865
+        $this->logger
866
+            ->expects($this->once())
867
+            ->method('error')
868
+            ->with(
869
+                'Failed addUser attempt with exception.',
870
+                [
871
+                    'app' => 'ocs_api',
872
+                    'exception' => $exception
873
+                ]
874
+            );
875
+        $loggedInUser = $this->getMockBuilder(IUser::class)
876
+            ->disableOriginalConstructor()
877
+            ->getMock();
878
+        $loggedInUser
879
+            ->expects($this->exactly(2))
880
+            ->method('getUID')
881
+            ->willReturn('adminUser');
882
+        $this->userSession
883
+            ->expects($this->once())
884
+            ->method('getUser')
885
+            ->willReturn($loggedInUser);
886
+        $this->groupManager
887
+            ->expects($this->once())
888
+            ->method('isAdmin')
889
+            ->with('adminUser')
890
+            ->willReturn(true);
891
+
892
+        $this->api->addUser('NewUser', 'PasswordOfTheNewUser');
893
+    }
894
+
895
+
896
+    public function testAddUserAsSubAdminNoGroup(): void {
897
+        $this->expectException(OCSException::class);
898
+        $this->expectExceptionMessage('No group specified (required for sub-admins)');
899
+        $this->expectExceptionCode(106);
900
+
901
+        $loggedInUser = $this->getMockBuilder(IUser::class)
902
+            ->disableOriginalConstructor()
903
+            ->getMock();
904
+        $loggedInUser
905
+            ->expects($this->exactly(2))
906
+            ->method('getUID')
907
+            ->willReturn('regularUser');
908
+        $this->userSession
909
+            ->expects($this->once())
910
+            ->method('getUser')
911
+            ->willReturn($loggedInUser);
912
+        $this->groupManager
913
+            ->expects($this->once())
914
+            ->method('isAdmin')
915
+            ->with('regularUser')
916
+            ->willReturn(false);
917
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
918
+            ->disableOriginalConstructor()->getMock();
919
+        $this->groupManager
920
+            ->expects($this->once())
921
+            ->method('getSubAdmin')
922
+            ->with()
923
+            ->willReturn($subAdminManager);
924
+
925
+        $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', []);
926
+    }
927
+
928
+
929
+    public function testAddUserAsSubAdminValidGroupNotSubAdmin(): void {
930
+        $this->expectException(OCSException::class);
931
+        $this->expectExceptionMessage('Insufficient privileges for group ExistingGroup');
932
+        $this->expectExceptionCode(105);
933
+
934
+        $loggedInUser = $this->getMockBuilder(IUser::class)
935
+            ->disableOriginalConstructor()
936
+            ->getMock();
937
+        $loggedInUser
938
+            ->expects($this->exactly(2))
939
+            ->method('getUID')
940
+            ->willReturn('regularUser');
941
+        $this->userSession
942
+            ->expects($this->once())
943
+            ->method('getUser')
944
+            ->willReturn($loggedInUser);
945
+        $this->groupManager
946
+            ->expects($this->once())
947
+            ->method('isAdmin')
948
+            ->with('regularUser')
949
+            ->willReturn(false);
950
+        $existingGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
951
+        $this->groupManager
952
+            ->expects($this->once())
953
+            ->method('get')
954
+            ->with('ExistingGroup')
955
+            ->willReturn($existingGroup);
956
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
957
+            ->disableOriginalConstructor()->getMock();
958
+        $subAdminManager
959
+            ->expects($this->once())
960
+            ->method('isSubAdminOfGroup')
961
+            ->with($loggedInUser, $existingGroup)
962
+            ->willReturn(false);
963
+        $this->groupManager
964
+            ->expects($this->once())
965
+            ->method('getSubAdmin')
966
+            ->with()
967
+            ->willReturn($subAdminManager);
968
+        $this->groupManager
969
+            ->expects($this->once())
970
+            ->method('groupExists')
971
+            ->with('ExistingGroup')
972
+            ->willReturn(true);
973
+
974
+        $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup'])->getData();
975
+    }
976
+
977
+    public function testAddUserAsSubAdminExistingGroups(): void {
978
+        $this->userManager
979
+            ->expects($this->once())
980
+            ->method('userExists')
981
+            ->with('NewUser')
982
+            ->willReturn(false);
983
+        $loggedInUser = $this->getMockBuilder(IUser::class)
984
+            ->disableOriginalConstructor()
985
+            ->getMock();
986
+        $loggedInUser
987
+            ->expects($this->exactly(2))
988
+            ->method('getUID')
989
+            ->willReturn('subAdminUser');
990
+        $this->userSession
991
+            ->expects($this->once())
992
+            ->method('getUser')
993
+            ->willReturn($loggedInUser);
994
+        $this->groupManager
995
+            ->expects($this->once())
996
+            ->method('isAdmin')
997
+            ->with('subAdminUser')
998
+            ->willReturn(false);
999
+        $this->groupManager
1000
+            ->expects($this->exactly(2))
1001
+            ->method('groupExists')
1002
+            ->willReturnMap([
1003
+                ['ExistingGroup1', true],
1004
+                ['ExistingGroup2', true]
1005
+            ]);
1006
+        $user = $this->getMockBuilder(IUser::class)
1007
+            ->disableOriginalConstructor()
1008
+            ->getMock();
1009
+        $this->userManager
1010
+            ->expects($this->once())
1011
+            ->method('createUser')
1012
+            ->with('NewUser', 'PasswordOfTheNewUser')
1013
+            ->willReturn($user);
1014
+        $existingGroup1 = $this->getMockBuilder('OCP\IGroup')
1015
+            ->disableOriginalConstructor()
1016
+            ->getMock();
1017
+        $existingGroup2 = $this->getMockBuilder('OCP\IGroup')
1018
+            ->disableOriginalConstructor()
1019
+            ->getMock();
1020
+        $existingGroup1
1021
+            ->expects($this->once())
1022
+            ->method('addUser')
1023
+            ->with($user);
1024
+        $existingGroup2
1025
+            ->expects($this->once())
1026
+            ->method('addUser')
1027
+            ->with($user);
1028
+        $this->groupManager
1029
+            ->expects($this->exactly(4))
1030
+            ->method('get')
1031
+            ->willReturnMap([
1032
+                ['ExistingGroup1', $existingGroup1],
1033
+                ['ExistingGroup2', $existingGroup2]
1034
+            ]);
1035
+
1036
+        $calls = [
1037
+            ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']],
1038
+            ['Added userid NewUser to group ExistingGroup1', ['app' => 'ocs_api']],
1039
+            ['Added userid NewUser to group ExistingGroup2', ['app' => 'ocs_api']],
1040
+        ];
1041
+        $this->logger
1042
+            ->expects($this->exactly(3))
1043
+            ->method('info')
1044
+            ->willReturnCallback(function () use (&$calls): void {
1045
+                $expected = array_shift($calls);
1046
+                $this->assertEquals($expected, func_get_args());
1047
+            });
1048
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1049
+            ->disableOriginalConstructor()->getMock();
1050
+        $this->groupManager
1051
+            ->expects($this->once())
1052
+            ->method('getSubAdmin')
1053
+            ->willReturn($subAdminManager);
1054
+        $subAdminManager
1055
+            ->expects($this->exactly(2))
1056
+            ->method('isSubAdminOfGroup')
1057
+            ->willReturnMap([
1058
+                [$loggedInUser, $existingGroup1, true],
1059
+                [$loggedInUser, $existingGroup2, true],
1060
+            ]);
1061
+
1062
+        $this->assertArrayHasKey('id', $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', '', ['ExistingGroup1', 'ExistingGroup2'])->getData());
1063
+    }
1064
+
1065
+
1066
+    public function testGetUserTargetDoesNotExist(): void {
1067
+        $this->expectException(OCSException::class);
1068
+        $this->expectExceptionMessage('User does not exist');
1069
+        $this->expectExceptionCode(404);
1070
+
1071
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1072
+            ->disableOriginalConstructor()
1073
+            ->getMock();
1074
+        $this->userSession
1075
+            ->method('getUser')
1076
+            ->willReturn($loggedInUser);
1077
+        $this->userManager
1078
+            ->expects($this->once())
1079
+            ->method('get')
1080
+            ->with('UserToGet')
1081
+            ->willReturn(null);
1082
+
1083
+        $this->api->getUser('UserToGet');
1084
+    }
1085
+
1086
+    public function testGetUserDataAsAdmin(): void {
1087
+        $group0 = $this->createMock(IGroup::class);
1088
+        $group1 = $this->createMock(IGroup::class);
1089
+        $group2 = $this->createMock(IGroup::class);
1090
+        $group3 = $this->createMock(IGroup::class);
1091
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1092
+            ->disableOriginalConstructor()
1093
+            ->getMock();
1094
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1095
+            ->disableOriginalConstructor()
1096
+            ->getMock();
1097
+        $loggedInUser
1098
+            ->method('getUID')
1099
+            ->willReturn('admin');
1100
+        $targetUser = $this->getMockBuilder(IUser::class)
1101
+            ->disableOriginalConstructor()
1102
+            ->getMock();
1103
+        $targetUser->expects($this->once())
1104
+            ->method('getSystemEMailAddress')
1105
+            ->willReturn('[email protected]');
1106
+        $this->userSession
1107
+            ->method('getUser')
1108
+            ->willReturn($loggedInUser);
1109
+        $this->userManager
1110
+            ->method('get')
1111
+            ->with('UID')
1112
+            ->willReturn($targetUser);
1113
+        $this->groupManager
1114
+            ->method('isAdmin')
1115
+            ->with('admin')
1116
+            ->willReturn(true);
1117
+        $this->groupManager
1118
+            ->expects($this->any())
1119
+            ->method('getUserGroups')
1120
+            ->willReturn([$group0, $group1, $group2]);
1121
+        $this->groupManager
1122
+            ->expects($this->once())
1123
+            ->method('getSubAdmin')
1124
+            ->willReturn($subAdminManager);
1125
+        $subAdminManager
1126
+            ->expects($this->once())
1127
+            ->method('getSubAdminsGroups')
1128
+            ->willReturn([$group3]);
1129
+        $group0->expects($this->once())
1130
+            ->method('getGID')
1131
+            ->willReturn('group0');
1132
+        $group1->expects($this->once())
1133
+            ->method('getGID')
1134
+            ->willReturn('group1');
1135
+        $group2->expects($this->once())
1136
+            ->method('getGID')
1137
+            ->willReturn('group2');
1138
+        $group3->expects($this->once())
1139
+            ->method('getGID')
1140
+            ->willReturn('group3');
1141
+
1142
+        $this->mockAccount($targetUser, [
1143
+            IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1144
+            IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1145
+            IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1146
+            IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1147
+            IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1148
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1149
+            IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1150
+            IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1151
+            IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1152
+            IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1153
+            IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1154
+            IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1155
+        ]);
1156
+        $this->config
1157
+            ->method('getUserValue')
1158
+            ->willReturnMap([
1159
+                ['UID', 'core', 'enabled', 'true', 'true'],
1160
+            ]);
1161
+        $this->api
1162
+            ->expects($this->once())
1163
+            ->method('fillStorageInfo')
1164
+            ->with($targetUser)
1165
+            ->willReturn(['DummyValue']);
1166
+
1167
+        $backend = $this->createMock(UserInterface::class);
1168
+        $backend->expects($this->any())
1169
+            ->method('implementsActions')
1170
+            ->willReturn(true);
1171
+
1172
+        $targetUser
1173
+            ->expects($this->once())
1174
+            ->method('getDisplayName')
1175
+            ->willReturn('Demo User');
1176
+        $targetUser
1177
+            ->expects($this->once())
1178
+            ->method('getHome')
1179
+            ->willReturn('/var/www/newtcloud/data/UID');
1180
+        $targetUser
1181
+            ->expects($this->exactly(2))
1182
+            ->method('getLastLogin')
1183
+            ->willReturn(1521191471);
1184
+        $targetUser
1185
+            ->expects($this->once())
1186
+            ->method('getFirstLogin')
1187
+            ->willReturn(1511191471);
1188
+        $targetUser
1189
+            ->expects($this->once())
1190
+            ->method('getBackendClassName')
1191
+            ->willReturn('Database');
1192
+        $targetUser
1193
+            ->expects($this->once())
1194
+            ->method('getBackend')
1195
+            ->willReturn($backend);
1196
+        $targetUser
1197
+            ->method('getUID')
1198
+            ->willReturn('UID');
1199
+
1200
+        $this->l10nFactory
1201
+            ->expects($this->once())
1202
+            ->method('getUserLanguage')
1203
+            ->with($targetUser)
1204
+            ->willReturn('de');
1205
+
1206
+        $expected = [
1207
+            'id' => 'UID',
1208
+            'enabled' => true,
1209
+            'storageLocation' => '/var/www/newtcloud/data/UID',
1210
+            'firstLoginTimestamp' => 1511191471,
1211
+            'lastLoginTimestamp' => 1521191471,
1212
+            'lastLogin' => 1521191471000,
1213
+            'backend' => 'Database',
1214
+            'subadmin' => ['group3'],
1215
+            'quota' => ['DummyValue'],
1216
+            'email' => '[email protected]',
1217
+            'displayname' => 'Demo User',
1218
+            'display-name' => 'Demo User',
1219
+            'phone' => 'phone',
1220
+            'address' => 'address',
1221
+            'website' => 'website',
1222
+            'twitter' => 'twitter',
1223
+            'bluesky' => 'bluesky',
1224
+            'fediverse' => 'fediverse',
1225
+            'groups' => ['group0', 'group1', 'group2'],
1226
+            'language' => 'de',
1227
+            'locale' => null,
1228
+            'backendCapabilities' => [
1229
+                'setDisplayName' => true,
1230
+                'setPassword' => true,
1231
+            ],
1232
+            'additional_mail' => [],
1233
+            'organisation' => 'organisation',
1234
+            'role' => 'role',
1235
+            'headline' => 'headline',
1236
+            'biography' => 'biography',
1237
+            'profile_enabled' => '1',
1238
+            'notify_email' => null,
1239
+            'manager' => '',
1240
+            'pronouns' => 'they/them',
1241
+        ];
1242
+        $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1243
+    }
1244
+
1245
+    public function testGetUserDataAsSubAdminAndUserIsAccessible(): void {
1246
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1247
+            ->disableOriginalConstructor()
1248
+            ->getMock();
1249
+        $loggedInUser
1250
+            ->method('getUID')
1251
+            ->willReturn('subadmin');
1252
+        $targetUser = $this->getMockBuilder(IUser::class)
1253
+            ->disableOriginalConstructor()
1254
+            ->getMock();
1255
+        $targetUser
1256
+            ->expects($this->once())
1257
+            ->method('getSystemEMailAddress')
1258
+            ->willReturn('[email protected]');
1259
+        $this->userSession
1260
+            ->method('getUser')
1261
+            ->willReturn($loggedInUser);
1262
+        $this->userManager
1263
+            ->method('get')
1264
+            ->with('UID')
1265
+            ->willReturn($targetUser);
1266
+        $this->groupManager
1267
+            ->method('isAdmin')
1268
+            ->with('subadmin')
1269
+            ->willReturn(false);
1270
+        $this->groupManager
1271
+            ->expects($this->any())
1272
+            ->method('getUserGroups')
1273
+            ->willReturn([]);
1274
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1275
+            ->disableOriginalConstructor()
1276
+            ->getMock();
1277
+        $subAdminManager
1278
+            ->expects($this->once())
1279
+            ->method('isUserAccessible')
1280
+            ->with($loggedInUser, $targetUser)
1281
+            ->willReturn(true);
1282
+        $subAdminManager
1283
+            ->expects($this->once())
1284
+            ->method('getSubAdminsGroups')
1285
+            ->willReturn([]);
1286
+        $this->groupManager
1287
+            ->expects($this->exactly(2))
1288
+            ->method('getSubAdmin')
1289
+            ->willReturn($subAdminManager);
1290
+        $this->config
1291
+            ->method('getUserValue')
1292
+            ->willReturnMap([
1293
+                ['UID', 'core', 'enabled', 'true', 'true'],
1294
+            ]);
1295
+        $this->api
1296
+            ->expects($this->once())
1297
+            ->method('fillStorageInfo')
1298
+            ->with($targetUser)
1299
+            ->willReturn(['DummyValue']);
1300
+
1301
+        $backend = $this->createMock(UserInterface::class);
1302
+        $backend->expects($this->any())
1303
+            ->method('implementsActions')
1304
+            ->willReturn(true);
1305
+
1306
+        $targetUser
1307
+            ->expects($this->once())
1308
+            ->method('getDisplayName')
1309
+            ->willReturn('Demo User');
1310
+        $targetUser
1311
+            ->expects($this->never())
1312
+            ->method('getHome');
1313
+        $targetUser
1314
+            ->expects($this->exactly(2))
1315
+            ->method('getLastLogin')
1316
+            ->willReturn(1521191471);
1317
+        $targetUser
1318
+            ->expects($this->once())
1319
+            ->method('getFirstLogin')
1320
+            ->willReturn(1511191471);
1321
+        $targetUser
1322
+            ->expects($this->once())
1323
+            ->method('getBackendClassName')
1324
+            ->willReturn('Database');
1325
+        $targetUser
1326
+            ->expects($this->once())
1327
+            ->method('getBackend')
1328
+            ->willReturn($backend);
1329
+        $targetUser
1330
+            ->method('getUID')
1331
+            ->willReturn('UID');
1332
+
1333
+        $this->mockAccount($targetUser, [
1334
+            IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1335
+            IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1336
+            IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1337
+            IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1338
+            IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1339
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1340
+            IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1341
+            IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1342
+            IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1343
+            IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1344
+            IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1345
+            IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1346
+        ]);
1347
+
1348
+        $this->l10nFactory
1349
+            ->expects($this->once())
1350
+            ->method('getUserLanguage')
1351
+            ->with($targetUser)
1352
+            ->willReturn('da');
1353
+
1354
+        $expected = [
1355
+            'id' => 'UID',
1356
+            'enabled' => true,
1357
+            'firstLoginTimestamp' => 1511191471,
1358
+            'lastLoginTimestamp' => 1521191471,
1359
+            'lastLogin' => 1521191471000,
1360
+            'backend' => 'Database',
1361
+            'subadmin' => [],
1362
+            'quota' => ['DummyValue'],
1363
+            'email' => '[email protected]',
1364
+            'displayname' => 'Demo User',
1365
+            'display-name' => 'Demo User',
1366
+            'phone' => 'phone',
1367
+            'address' => 'address',
1368
+            'website' => 'website',
1369
+            'twitter' => 'twitter',
1370
+            'bluesky' => 'bluesky',
1371
+            'fediverse' => 'fediverse',
1372
+            'groups' => [],
1373
+            'language' => 'da',
1374
+            'locale' => null,
1375
+            'backendCapabilities' => [
1376
+                'setDisplayName' => true,
1377
+                'setPassword' => true,
1378
+            ],
1379
+            'additional_mail' => [],
1380
+            'organisation' => 'organisation',
1381
+            'role' => 'role',
1382
+            'headline' => 'headline',
1383
+            'biography' => 'biography',
1384
+            'profile_enabled' => '1',
1385
+            'notify_email' => null,
1386
+            'manager' => '',
1387
+            'pronouns' => 'they/them',
1388
+        ];
1389
+        $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1390
+    }
1391
+
1392
+
1393
+
1394
+    public function testGetUserDataAsSubAdminAndUserIsNotAccessible(): void {
1395
+        $this->expectException(OCSException::class);
1396
+        $this->expectExceptionCode(998);
1397
+
1398
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1399
+            ->disableOriginalConstructor()
1400
+            ->getMock();
1401
+        $loggedInUser
1402
+            ->expects($this->exactly(4))
1403
+            ->method('getUID')
1404
+            ->willReturn('subadmin');
1405
+        $targetUser = $this->getMockBuilder(IUser::class)
1406
+            ->disableOriginalConstructor()
1407
+            ->getMock();
1408
+        $this->userSession
1409
+            ->method('getUser')
1410
+            ->willReturn($loggedInUser);
1411
+        $this->userManager
1412
+            ->expects($this->once())
1413
+            ->method('get')
1414
+            ->with('UserToGet')
1415
+            ->willReturn($targetUser);
1416
+        $this->groupManager
1417
+            ->expects($this->once())
1418
+            ->method('isAdmin')
1419
+            ->with('subadmin')
1420
+            ->willReturn(false);
1421
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1422
+            ->disableOriginalConstructor()
1423
+            ->getMock();
1424
+        $subAdminManager
1425
+            ->expects($this->once())
1426
+            ->method('isUserAccessible')
1427
+            ->with($loggedInUser, $targetUser)
1428
+            ->willReturn(false);
1429
+        $this->groupManager
1430
+            ->expects($this->once())
1431
+            ->method('getSubAdmin')
1432
+            ->willReturn($subAdminManager);
1433
+
1434
+        $this->invokePrivate($this->api, 'getUser', ['UserToGet']);
1435
+    }
1436
+
1437
+    public function testGetUserDataAsSubAdminSelfLookup(): void {
1438
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1439
+            ->disableOriginalConstructor()
1440
+            ->getMock();
1441
+        $loggedInUser
1442
+            ->method('getUID')
1443
+            ->willReturn('UID');
1444
+        $targetUser = $this->getMockBuilder(IUser::class)
1445
+            ->disableOriginalConstructor()
1446
+            ->getMock();
1447
+        $this->userSession
1448
+            ->method('getUser')
1449
+            ->willReturn($loggedInUser);
1450
+        $this->userManager
1451
+            ->method('get')
1452
+            ->with('UID')
1453
+            ->willReturn($targetUser);
1454
+        $this->groupManager
1455
+            ->method('isAdmin')
1456
+            ->with('UID')
1457
+            ->willReturn(false);
1458
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
1459
+            ->disableOriginalConstructor()
1460
+            ->getMock();
1461
+        $subAdminManager
1462
+            ->expects($this->once())
1463
+            ->method('isUserAccessible')
1464
+            ->with($loggedInUser, $targetUser)
1465
+            ->willReturn(false);
1466
+        $subAdminManager
1467
+            ->expects($this->once())
1468
+            ->method('getSubAdminsGroups')
1469
+            ->willReturn([]);
1470
+        $this->groupManager
1471
+            ->expects($this->exactly(2))
1472
+            ->method('getSubAdmin')
1473
+            ->willReturn($subAdminManager);
1474
+        $this->groupManager
1475
+            ->expects($this->any())
1476
+            ->method('getUserGroups')
1477
+            ->willReturn([]);
1478
+        $this->api
1479
+            ->expects($this->once())
1480
+            ->method('fillStorageInfo')
1481
+            ->with($targetUser)
1482
+            ->willReturn(['DummyValue']);
1483
+
1484
+        $backend = $this->createMock(UserInterface::class);
1485
+        $backend->expects($this->atLeastOnce())
1486
+            ->method('implementsActions')
1487
+            ->willReturn(false);
1488
+
1489
+        $targetUser
1490
+            ->expects($this->once())
1491
+            ->method('getDisplayName')
1492
+            ->willReturn('Subadmin User');
1493
+        $targetUser
1494
+            ->expects($this->once())
1495
+            ->method('getSystemEMailAddress')
1496
+            ->willReturn('[email protected]');
1497
+        $targetUser
1498
+            ->method('getUID')
1499
+            ->willReturn('UID');
1500
+        $targetUser
1501
+            ->expects($this->never())
1502
+            ->method('getHome');
1503
+        $targetUser
1504
+            ->expects($this->exactly(2))
1505
+            ->method('getLastLogin')
1506
+            ->willReturn(1521191471);
1507
+        $targetUser
1508
+            ->expects($this->once())
1509
+            ->method('getFirstLogin')
1510
+            ->willReturn(1511191471);
1511
+        $targetUser
1512
+            ->expects($this->once())
1513
+            ->method('getBackendClassName')
1514
+            ->willReturn('Database');
1515
+        $targetUser
1516
+            ->expects($this->once())
1517
+            ->method('getBackend')
1518
+            ->willReturn($backend);
1519
+        $this->mockAccount($targetUser, [
1520
+            IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
1521
+            IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
1522
+            IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
1523
+            IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
1524
+            IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
1525
+            IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
1526
+            IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
1527
+            IAccountManager::PROPERTY_ROLE => ['value' => 'role'],
1528
+            IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'],
1529
+            IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'],
1530
+            IAccountManager::PROPERTY_PROFILE_ENABLED => ['value' => '1'],
1531
+            IAccountManager::PROPERTY_PRONOUNS => ['value' => 'they/them'],
1532
+        ]);
1533
+
1534
+        $this->l10nFactory
1535
+            ->expects($this->once())
1536
+            ->method('getUserLanguage')
1537
+            ->with($targetUser)
1538
+            ->willReturn('ru');
1539
+
1540
+        $expected = [
1541
+            'id' => 'UID',
1542
+            'firstLoginTimestamp' => 1511191471,
1543
+            'lastLoginTimestamp' => 1521191471,
1544
+            'lastLogin' => 1521191471000,
1545
+            'backend' => 'Database',
1546
+            'subadmin' => [],
1547
+            'quota' => ['DummyValue'],
1548
+            'email' => '[email protected]',
1549
+            'displayname' => 'Subadmin User',
1550
+            'display-name' => 'Subadmin User',
1551
+            'phone' => 'phone',
1552
+            'address' => 'address',
1553
+            'website' => 'website',
1554
+            'twitter' => 'twitter',
1555
+            'bluesky' => 'bluesky',
1556
+            'fediverse' => 'fediverse',
1557
+            'groups' => [],
1558
+            'language' => 'ru',
1559
+            'locale' => null,
1560
+            'backendCapabilities' => [
1561
+                'setDisplayName' => false,
1562
+                'setPassword' => false,
1563
+            ],
1564
+            'additional_mail' => [],
1565
+            'organisation' => 'organisation',
1566
+            'role' => 'role',
1567
+            'headline' => 'headline',
1568
+            'biography' => 'biography',
1569
+            'profile_enabled' => '1',
1570
+            'notify_email' => null,
1571
+            'manager' => '',
1572
+            'pronouns' => 'they/them',
1573
+        ];
1574
+        $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
1575
+    }
1576
+
1577
+    public static function dataSearchByPhoneNumbers(): array {
1578
+        return [
1579
+            'Invalid country' => ['Not a country code', ['12345' => ['NaN']], 400, null, null, []],
1580
+            'No number to search' => ['DE', ['12345' => ['NaN']], 200, null, null, []],
1581
+            'Valid number but no match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []],
1582
+            'Invalid number' => ['FR', ['12345' => ['0711 / 25 24 28-90']], 200, null, null, []],
1583
+            'Invalid and valid number' => ['DE', ['12345' => ['NaN', '0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []],
1584
+            'Valid and invalid number' => ['DE', ['12345' => ['0711 / 25 24 28-90', 'NaN']], 200, ['+4971125242890'], [], []],
1585
+            'Valid number and a match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['12345' => 'admin@localhost']],
1586
+            'Same number twice, later hits' => ['DE', ['12345' => ['0711 / 25 24 28-90'], '23456' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['23456' => 'admin@localhost']],
1587
+        ];
1588
+    }
1589
+
1590
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchByPhoneNumbers')]
1591
+    public function testSearchByPhoneNumbers(string $location, array $search, int $status, ?array $searchUsers, ?array $userMatches, array $expected): void {
1592
+        $knownTo = 'knownTo';
1593
+        $user = $this->createMock(IUser::class);
1594
+        $user->method('getUID')
1595
+            ->willReturn($knownTo);
1596
+        $this->userSession->method('getUser')
1597
+            ->willReturn($user);
1598
+
1599
+        if ($searchUsers === null) {
1600
+            $this->accountManager->expects($this->never())
1601
+                ->method('searchUsers');
1602
+        } else {
1603
+            $this->accountManager->expects($this->once())
1604
+                ->method('searchUsers')
1605
+                ->with(IAccountManager::PROPERTY_PHONE, $searchUsers)
1606
+                ->willReturn($userMatches);
1607
+
1608
+            $this->knownUserService->expects($this->once())
1609
+                ->method('deleteKnownTo')
1610
+                ->with($knownTo);
1611
+
1612
+            $this->knownUserService->expects($this->exactly(count($expected)))
1613
+                ->method('storeIsKnownToUser')
1614
+                ->with($knownTo, $this->anything());
1615
+        }
1616
+
1617
+        $this->urlGenerator->method('getAbsoluteURL')
1618
+            ->with('/')
1619
+            ->willReturn('https://localhost/');
1620
+
1621
+        $response = $this->api->searchByPhoneNumbers($location, $search);
1622
+
1623
+        self::assertEquals($status, $response->getStatus());
1624
+        self::assertEquals($expected, $response->getData());
1625
+    }
1626
+
1627
+    public function testEditUserRegularUserSelfEditChangeDisplayName(): void {
1628
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1629
+            ->disableOriginalConstructor()
1630
+            ->getMock();
1631
+        $loggedInUser
1632
+            ->expects($this->any())
1633
+            ->method('getUID')
1634
+            ->willReturn('UID');
1635
+        $targetUser = $this->getMockBuilder(IUser::class)
1636
+            ->disableOriginalConstructor()
1637
+            ->getMock();
1638
+        $this->userSession
1639
+            ->expects($this->once())
1640
+            ->method('getUser')
1641
+            ->willReturn($loggedInUser);
1642
+        $this->userManager
1643
+            ->expects($this->once())
1644
+            ->method('get')
1645
+            ->with('UserToEdit')
1646
+            ->willReturn($targetUser);
1647
+        $targetUser
1648
+            ->expects($this->once())
1649
+            ->method('getBackend')
1650
+            ->willReturn($this->createMock(ISetDisplayNameBackend::class));
1651
+        $targetUser
1652
+            ->expects($this->once())
1653
+            ->method('setDisplayName')
1654
+            ->with('NewDisplayName')
1655
+            ->willReturn(true);
1656
+        $targetUser
1657
+            ->expects($this->any())
1658
+            ->method('getUID')
1659
+            ->willReturn('UID');
1660
+
1661
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'display', 'NewDisplayName')->getData());
1662
+    }
1663
+
1664
+    public function testEditUserRegularUserSelfEditChangeEmailValid(): void {
1665
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1666
+            ->disableOriginalConstructor()
1667
+            ->getMock();
1668
+        $loggedInUser
1669
+            ->expects($this->any())
1670
+            ->method('getUID')
1671
+            ->willReturn('UID');
1672
+        $targetUser = $this->getMockBuilder(IUser::class)
1673
+            ->disableOriginalConstructor()
1674
+            ->getMock();
1675
+        $this->userSession
1676
+            ->expects($this->once())
1677
+            ->method('getUser')
1678
+            ->willReturn($loggedInUser);
1679
+        $this->userManager
1680
+            ->expects($this->once())
1681
+            ->method('get')
1682
+            ->with('UserToEdit')
1683
+            ->willReturn($targetUser);
1684
+        $targetUser
1685
+            ->expects($this->once())
1686
+            ->method('setSystemEMailAddress')
1687
+            ->with('[email protected]');
1688
+        $targetUser
1689
+            ->expects($this->any())
1690
+            ->method('getUID')
1691
+            ->willReturn('UID');
1692
+
1693
+        $backend = $this->createMock(UserInterface::class);
1694
+        $targetUser
1695
+            ->expects($this->any())
1696
+            ->method('getBackend')
1697
+            ->willReturn($backend);
1698
+
1699
+        $this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => $default);
1700
+
1701
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'email', '[email protected]')->getData());
1702
+    }
1703
+
1704
+    public function testEditUserRegularUserSelfEditAddAdditionalEmailValid(): void {
1705
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1706
+            ->disableOriginalConstructor()
1707
+            ->getMock();
1708
+        $loggedInUser
1709
+            ->expects($this->any())
1710
+            ->method('getUID')
1711
+            ->willReturn('UID');
1712
+        $targetUser = $this->getMockBuilder(IUser::class)
1713
+            ->disableOriginalConstructor()
1714
+            ->getMock();
1715
+        $this->userSession
1716
+            ->expects($this->once())
1717
+            ->method('getUser')
1718
+            ->willReturn($loggedInUser);
1719
+        $this->userManager
1720
+            ->expects($this->once())
1721
+            ->method('get')
1722
+            ->with('UserToEdit')
1723
+            ->willReturn($targetUser);
1724
+        $targetUser
1725
+            ->expects($this->any())
1726
+            ->method('getUID')
1727
+            ->willReturn('UID');
1728
+
1729
+        $backend = $this->createMock(UserInterface::class);
1730
+        $targetUser
1731
+            ->expects($this->any())
1732
+            ->method('getBackend')
1733
+            ->willReturn($backend);
1734
+
1735
+        $userAccount = $this->createMock(IAccount::class);
1736
+
1737
+        $this->accountManager
1738
+            ->expects($this->once())
1739
+            ->method('getAccount')
1740
+            ->with($targetUser)
1741
+            ->willReturn($userAccount);
1742
+        $this->accountManager
1743
+            ->expects($this->once())
1744
+            ->method('updateAccount')
1745
+            ->with($userAccount);
1746
+
1747
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData());
1748
+    }
1749
+
1750
+    public function testEditUserRegularUserSelfEditAddAdditionalEmailMainAddress(): void {
1751
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1752
+            ->disableOriginalConstructor()
1753
+            ->getMock();
1754
+        $loggedInUser
1755
+            ->expects($this->any())
1756
+            ->method('getUID')
1757
+            ->willReturn('UID');
1758
+        $targetUser = $this->getMockBuilder(IUser::class)
1759
+            ->disableOriginalConstructor()
1760
+            ->getMock();
1761
+        $this->userSession
1762
+            ->expects($this->once())
1763
+            ->method('getUser')
1764
+            ->willReturn($loggedInUser);
1765
+        $this->userManager
1766
+            ->expects($this->once())
1767
+            ->method('get')
1768
+            ->with('UserToEdit')
1769
+            ->willReturn($targetUser);
1770
+        $targetUser
1771
+            ->expects($this->any())
1772
+            ->method('getUID')
1773
+            ->willReturn('UID');
1774
+
1775
+        $backend = $this->createMock(UserInterface::class);
1776
+        $targetUser
1777
+            ->expects($this->any())
1778
+            ->method('getBackend')
1779
+            ->willReturn($backend);
1780
+        $targetUser
1781
+            ->expects($this->any())
1782
+            ->method('getSystemEMailAddress')
1783
+            ->willReturn('[email protected]');
1784
+
1785
+        $userAccount = $this->createMock(IAccount::class);
1786
+
1787
+        $this->accountManager
1788
+            ->expects($this->never())
1789
+            ->method('getAccount')
1790
+            ->with($targetUser)
1791
+            ->willReturn($userAccount);
1792
+        $this->accountManager
1793
+            ->expects($this->never())
1794
+            ->method('updateAccount')
1795
+            ->with($userAccount);
1796
+
1797
+        $this->expectException(OCSException::class);
1798
+        $this->expectExceptionCode(101);
1799
+        $this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData();
1800
+    }
1801
+
1802
+    public function testEditUserRegularUserSelfEditAddAdditionalEmailDuplicate(): void {
1803
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1804
+            ->disableOriginalConstructor()
1805
+            ->getMock();
1806
+        $loggedInUser
1807
+            ->expects($this->any())
1808
+            ->method('getUID')
1809
+            ->willReturn('UID');
1810
+        $targetUser = $this->getMockBuilder(IUser::class)
1811
+            ->disableOriginalConstructor()
1812
+            ->getMock();
1813
+        $this->userSession
1814
+            ->expects($this->once())
1815
+            ->method('getUser')
1816
+            ->willReturn($loggedInUser);
1817
+        $this->userManager
1818
+            ->expects($this->once())
1819
+            ->method('get')
1820
+            ->with('UserToEdit')
1821
+            ->willReturn($targetUser);
1822
+        $targetUser
1823
+            ->expects($this->any())
1824
+            ->method('getUID')
1825
+            ->willReturn('UID');
1826
+
1827
+        $backend = $this->createMock(UserInterface::class);
1828
+        $targetUser
1829
+            ->expects($this->any())
1830
+            ->method('getBackend')
1831
+            ->willReturn($backend);
1832
+
1833
+        $property = $this->createMock(IAccountProperty::class);
1834
+        $property->method('getValue')
1835
+            ->willReturn('[email protected]');
1836
+        $collection = $this->createMock(IAccountPropertyCollection::class);
1837
+        $collection->method('getPropertyByValue')
1838
+            ->with('[email protected]')
1839
+            ->willReturn($property);
1840
+
1841
+        $userAccount = $this->createMock(IAccount::class);
1842
+        $userAccount->method('getPropertyCollection')
1843
+            ->with(IAccountManager::COLLECTION_EMAIL)
1844
+            ->willReturn($collection);
1845
+
1846
+        $this->accountManager
1847
+            ->expects($this->once())
1848
+            ->method('getAccount')
1849
+            ->with($targetUser)
1850
+            ->willReturn($userAccount);
1851
+        $this->accountManager
1852
+            ->expects($this->never())
1853
+            ->method('updateAccount')
1854
+            ->with($userAccount);
1855
+
1856
+        $this->expectException(OCSException::class);
1857
+        $this->expectExceptionCode(101);
1858
+        $this->api->editUser('UserToEdit', 'additional_mail', '[email protected]')->getData();
1859
+    }
1860
+
1861
+    public function testEditUserRegularUserSelfEditChangeEmailInvalid(): void {
1862
+        $this->expectException(OCSException::class);
1863
+        $this->expectExceptionCode(101);
1864
+
1865
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1866
+            ->disableOriginalConstructor()
1867
+            ->getMock();
1868
+        $loggedInUser
1869
+            ->expects($this->any())
1870
+            ->method('getUID')
1871
+            ->willReturn('UID');
1872
+        $targetUser = $this->getMockBuilder(IUser::class)
1873
+            ->disableOriginalConstructor()
1874
+            ->getMock();
1875
+        $this->userSession
1876
+            ->expects($this->once())
1877
+            ->method('getUser')
1878
+            ->willReturn($loggedInUser);
1879
+        $this->userManager
1880
+            ->expects($this->once())
1881
+            ->method('get')
1882
+            ->with('UserToEdit')
1883
+            ->willReturn($targetUser);
1884
+        $targetUser
1885
+            ->expects($this->any())
1886
+            ->method('getUID')
1887
+            ->willReturn('UID');
1888
+
1889
+        $backend = $this->createMock(UserInterface::class);
1890
+        $targetUser
1891
+            ->expects($this->any())
1892
+            ->method('getBackend')
1893
+            ->willReturn($backend);
1894
+
1895
+        $this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => $default);
1896
+
1897
+        $this->api->editUser('UserToEdit', 'email', 'demo.org');
1898
+    }
1899
+
1900
+    public static function selfEditChangePropertyProvider(): array {
1901
+        return [
1902
+            [IAccountManager::PROPERTY_TWITTER, '@oldtwitter', '@newtwitter'],
1903
+            [IAccountManager::PROPERTY_BLUESKY, 'old.bluesky', 'new.bluesky'],
1904
+            [IAccountManager::PROPERTY_FEDIVERSE, '@[email protected]', '@[email protected]'],
1905
+            [IAccountManager::PROPERTY_PHONE, '1234', '12345'],
1906
+            [IAccountManager::PROPERTY_ADDRESS, 'Something street 2', 'Another street 3'],
1907
+            [IAccountManager::PROPERTY_WEBSITE, 'https://examplesite1', 'https://examplesite2'],
1908
+            [IAccountManager::PROPERTY_ORGANISATION, 'Organisation A', 'Organisation B'],
1909
+            [IAccountManager::PROPERTY_ROLE, 'Human', 'Alien'],
1910
+            [IAccountManager::PROPERTY_HEADLINE, 'Hi', 'Hello'],
1911
+            [IAccountManager::PROPERTY_BIOGRAPHY, 'A biography', 'Another biography'],
1912
+            [IAccountManager::PROPERTY_PROFILE_ENABLED, '1', '0'],
1913
+            [IAccountManager::PROPERTY_PRONOUNS, 'they/them', 'he/him'],
1914
+        ];
1915
+    }
1916
+
1917
+    #[\PHPUnit\Framework\Attributes\DataProvider('selfEditChangePropertyProvider')]
1918
+    public function testEditUserRegularUserSelfEditChangeProperty($propertyName, $oldValue, $newValue): void {
1919
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1920
+            ->disableOriginalConstructor()
1921
+            ->getMock();
1922
+        $loggedInUser
1923
+            ->expects($this->any())
1924
+            ->method('getUID')
1925
+            ->willReturn('UID');
1926
+        $this->userSession
1927
+            ->expects($this->once())
1928
+            ->method('getUser')
1929
+            ->willReturn($loggedInUser);
1930
+        $this->userManager
1931
+            ->expects($this->once())
1932
+            ->method('get')
1933
+            ->with('UserToEdit')
1934
+            ->willReturn($loggedInUser);
1935
+
1936
+        $backend = $this->createMock(UserInterface::class);
1937
+        $loggedInUser
1938
+            ->expects($this->any())
1939
+            ->method('getBackend')
1940
+            ->willReturn($backend);
1941
+
1942
+        $propertyMock = $this->createMock(IAccountProperty::class);
1943
+        $propertyMock->expects($this->any())
1944
+            ->method('getName')
1945
+            ->willReturn($propertyName);
1946
+        $propertyMock->expects($this->any())
1947
+            ->method('getValue')
1948
+            ->willReturn($oldValue);
1949
+        $propertyMock->expects($this->once())
1950
+            ->method('setValue')
1951
+            ->with($newValue)
1952
+            ->willReturnSelf();
1953
+        $propertyMock->expects($this->any())
1954
+            ->method('getScope')
1955
+            ->willReturn(IAccountManager::SCOPE_LOCAL);
1956
+
1957
+        $accountMock = $this->createMock(IAccount::class);
1958
+        $accountMock->expects($this->any())
1959
+            ->method('getProperty')
1960
+            ->with($propertyName)
1961
+            ->willReturn($propertyMock);
1962
+
1963
+        $this->accountManager->expects($this->atLeastOnce())
1964
+            ->method('getAccount')
1965
+            ->with($loggedInUser)
1966
+            ->willReturn($accountMock);
1967
+        $this->accountManager->expects($this->once())
1968
+            ->method('updateAccount')
1969
+            ->with($accountMock);
1970
+
1971
+        $this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName, $newValue)->getData());
1972
+    }
1973
+
1974
+    public function selfEditChangePropertyScopeProvider() {
1975
+        return [
1976
+            [IAccountManager::PROPERTY_AVATAR, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1977
+            [IAccountManager::PROPERTY_DISPLAYNAME, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1978
+            [IAccountManager::PROPERTY_EMAIL, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1979
+            [IAccountManager::PROPERTY_TWITTER, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1980
+            [IAccountManager::PROPERTY_BLUESKY, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1981
+            [IAccountManager::PROPERTY_FEDIVERSE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1982
+            [IAccountManager::PROPERTY_PHONE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1983
+            [IAccountManager::PROPERTY_ADDRESS, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1984
+            [IAccountManager::PROPERTY_WEBSITE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1985
+            [IAccountManager::PROPERTY_ORGANISATION, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1986
+            [IAccountManager::PROPERTY_ROLE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1987
+            [IAccountManager::PROPERTY_HEADLINE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1988
+            [IAccountManager::PROPERTY_BIOGRAPHY, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1989
+            [IAccountManager::PROPERTY_PROFILE_ENABLED, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1990
+            [IAccountManager::PROPERTY_PRONOUNS, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
1991
+        ];
1992
+    }
1993
+
1994
+    #[\PHPUnit\Framework\Attributes\DataProvider('selfEditChangePropertyProvider')]
1995
+    public function testEditUserRegularUserSelfEditChangePropertyScope($propertyName, $oldScope, $newScope): void {
1996
+        $loggedInUser = $this->getMockBuilder(IUser::class)
1997
+            ->disableOriginalConstructor()
1998
+            ->getMock();
1999
+        $loggedInUser
2000
+            ->expects($this->any())
2001
+            ->method('getUID')
2002
+            ->willReturn('UID');
2003
+        $this->userSession
2004
+            ->expects($this->once())
2005
+            ->method('getUser')
2006
+            ->willReturn($loggedInUser);
2007
+        $this->userManager
2008
+            ->expects($this->once())
2009
+            ->method('get')
2010
+            ->with('UserToEdit')
2011
+            ->willReturn($loggedInUser);
2012
+
2013
+        $backend = $this->createMock(UserInterface::class);
2014
+        $loggedInUser
2015
+            ->expects($this->any())
2016
+            ->method('getBackend')
2017
+            ->willReturn($backend);
2018
+
2019
+        $propertyMock = $this->createMock(IAccountProperty::class);
2020
+        $propertyMock->expects($this->any())
2021
+            ->method('getName')
2022
+            ->willReturn($propertyName);
2023
+        $propertyMock->expects($this->any())
2024
+            ->method('getValue')
2025
+            ->willReturn('somevalue');
2026
+        $propertyMock->expects($this->any())
2027
+            ->method('getScope')
2028
+            ->willReturn($oldScope);
2029
+        $propertyMock->expects($this->atLeastOnce())
2030
+            ->method('setScope')
2031
+            ->with($newScope)
2032
+            ->willReturnSelf();
2033
+
2034
+        $accountMock = $this->createMock(IAccount::class);
2035
+        $accountMock->expects($this->any())
2036
+            ->method('getProperty')
2037
+            ->with($propertyName)
2038
+            ->willReturn($propertyMock);
2039
+
2040
+        $this->accountManager->expects($this->atLeastOnce())
2041
+            ->method('getAccount')
2042
+            ->with($loggedInUser)
2043
+            ->willReturn($accountMock);
2044
+        $this->accountManager->expects($this->once())
2045
+            ->method('updateAccount')
2046
+            ->with($accountMock);
2047
+
2048
+        $this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName . 'Scope', $newScope)->getData());
2049
+    }
2050
+
2051
+    public function testEditUserRegularUserSelfEditChangePassword(): void {
2052
+        $loggedInUser = $this->getMockBuilder(IUser::class)
2053
+            ->disableOriginalConstructor()
2054
+            ->getMock();
2055
+        $loggedInUser
2056
+            ->expects($this->any())
2057
+            ->method('getUID')
2058
+            ->willReturn('UID');
2059
+        $targetUser = $this->getMockBuilder(IUser::class)
2060
+            ->disableOriginalConstructor()
2061
+            ->getMock();
2062
+        $this->userSession
2063
+            ->expects($this->once())
2064
+            ->method('getUser')
2065
+            ->willReturn($loggedInUser);
2066
+        $this->userManager
2067
+            ->expects($this->once())
2068
+            ->method('get')
2069
+            ->with('UserToEdit')
2070
+            ->willReturn($targetUser);
2071
+        $targetUser
2072
+            ->expects($this->once())
2073
+            ->method('canChangePassword')
2074
+            ->willReturn(true);
2075
+        $targetUser
2076
+            ->expects($this->once())
2077
+            ->method('setPassword')
2078
+            ->with('NewPassword');
2079
+        $targetUser
2080
+            ->expects($this->any())
2081
+            ->method('getUID')
2082
+            ->willReturn('UID');
2083
+
2084
+        $backend = $this->createMock(UserInterface::class);
2085
+        $targetUser
2086
+            ->expects($this->any())
2087
+            ->method('getBackend')
2088
+            ->willReturn($backend);
2089
+
2090
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'password', 'NewPassword')->getData());
2091
+    }
2092
+
2093
+
2094
+
2095
+    public function testEditUserRegularUserSelfEditChangeQuota(): void {
2096
+        $this->expectException(OCSException::class);
2097
+        $this->expectExceptionCode(113);
2098
+
2099
+        $loggedInUser = $this->getMockBuilder(IUser::class)
2100
+            ->disableOriginalConstructor()
2101
+            ->getMock();
2102
+        $loggedInUser
2103
+            ->expects($this->any())
2104
+            ->method('getUID')
2105
+            ->willReturn('UID');
2106
+        $targetUser = $this->getMockBuilder(IUser::class)
2107
+            ->disableOriginalConstructor()
2108
+            ->getMock();
2109
+        $this->userSession
2110
+            ->expects($this->once())
2111
+            ->method('getUser')
2112
+            ->willReturn($loggedInUser);
2113
+        $this->userManager
2114
+            ->expects($this->once())
2115
+            ->method('get')
2116
+            ->with('UserToEdit')
2117
+            ->willReturn($targetUser);
2118
+        $targetUser
2119
+            ->expects($this->any())
2120
+            ->method('getUID')
2121
+            ->willReturn('UID');
2122
+
2123
+        $backend = $this->createMock(UserInterface::class);
2124
+        $targetUser
2125
+            ->expects($this->any())
2126
+            ->method('getBackend')
2127
+            ->willReturn($backend);
2128
+
2129
+        $this->api->editUser('UserToEdit', 'quota', 'NewQuota');
2130
+    }
2131
+
2132
+    public function testEditUserAdminUserSelfEditChangeValidQuota(): void {
2133
+        $this->config
2134
+            ->expects($this->once())
2135
+            ->method('getAppValue')
2136
+            ->willReturnCallback(function ($appid, $key, $default) {
2137
+                if ($key === 'max_quota') {
2138
+                    return '-1';
2139
+                }
2140
+                return null;
2141
+            });
2142
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2143
+        $loggedInUser
2144
+            ->expects($this->any())
2145
+            ->method('getUID')
2146
+            ->willReturn('UID');
2147
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2148
+        $targetUser->expects($this->once())
2149
+            ->method('setQuota')
2150
+            ->with('2.9 MB');
2151
+        $this->userSession
2152
+            ->expects($this->once())
2153
+            ->method('getUser')
2154
+            ->willReturn($loggedInUser);
2155
+        $this->userManager
2156
+            ->expects($this->once())
2157
+            ->method('get')
2158
+            ->with('UserToEdit')
2159
+            ->willReturn($targetUser);
2160
+        $this->groupManager
2161
+            ->expects($this->exactly(3))
2162
+            ->method('isAdmin')
2163
+            ->with('UID')
2164
+            ->willReturn(true);
2165
+        $targetUser
2166
+            ->expects($this->any())
2167
+            ->method('getUID')
2168
+            ->willReturn('UID');
2169
+
2170
+        $backend = $this->createMock(UserInterface::class);
2171
+        $targetUser
2172
+            ->expects($this->any())
2173
+            ->method('getBackend')
2174
+            ->willReturn($backend);
2175
+
2176
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2177
+    }
2178
+
2179
+
2180
+
2181
+    public function testEditUserAdminUserSelfEditChangeInvalidQuota(): void {
2182
+        $this->expectException(OCSException::class);
2183
+        $this->expectExceptionMessage('Invalid quota value: ABC');
2184
+        $this->expectExceptionCode(101);
2185
+
2186
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2187
+        $loggedInUser
2188
+            ->expects($this->any())
2189
+            ->method('getUID')
2190
+            ->willReturn('UID');
2191
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2192
+        $this->userSession
2193
+            ->expects($this->once())
2194
+            ->method('getUser')
2195
+            ->willReturn($loggedInUser);
2196
+        $this->userManager
2197
+            ->expects($this->once())
2198
+            ->method('get')
2199
+            ->with('UserToEdit')
2200
+            ->willReturn($targetUser);
2201
+        $this->groupManager
2202
+            ->expects($this->exactly(3))
2203
+            ->method('isAdmin')
2204
+            ->with('UID')
2205
+            ->willReturn(true);
2206
+        $targetUser
2207
+            ->expects($this->any())
2208
+            ->method('getUID')
2209
+            ->willReturn('UID');
2210
+
2211
+        $backend = $this->createMock(UserInterface::class);
2212
+        $targetUser
2213
+            ->expects($this->any())
2214
+            ->method('getBackend')
2215
+            ->willReturn($backend);
2216
+
2217
+        $this->api->editUser('UserToEdit', 'quota', 'ABC');
2218
+    }
2219
+
2220
+    public function testEditUserAdminUserEditChangeValidQuota(): void {
2221
+        $this->config
2222
+            ->expects($this->once())
2223
+            ->method('getAppValue')
2224
+            ->willReturnCallback(function ($appid, $key, $default) {
2225
+                if ($key === 'max_quota') {
2226
+                    return '-1';
2227
+                }
2228
+                return null;
2229
+            });
2230
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2231
+        $loggedInUser
2232
+            ->expects($this->any())
2233
+            ->method('getUID')
2234
+            ->willReturn('admin');
2235
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2236
+        $targetUser->expects($this->once())
2237
+            ->method('setQuota')
2238
+            ->with('2.9 MB');
2239
+        $this->userSession
2240
+            ->expects($this->once())
2241
+            ->method('getUser')
2242
+            ->willReturn($loggedInUser);
2243
+        $this->userManager
2244
+            ->expects($this->once())
2245
+            ->method('get')
2246
+            ->with('UserToEdit')
2247
+            ->willReturn($targetUser);
2248
+        $this->groupManager
2249
+            ->expects($this->once())
2250
+            ->method('isAdmin')
2251
+            ->with('admin')
2252
+            ->willReturn(true);
2253
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2254
+            ->disableOriginalConstructor()
2255
+            ->getMock();
2256
+        $this->groupManager
2257
+            ->expects($this->once())
2258
+            ->method('getSubAdmin')
2259
+            ->willReturn($subAdminManager);
2260
+        $targetUser
2261
+            ->expects($this->any())
2262
+            ->method('getUID')
2263
+            ->willReturn('UID');
2264
+
2265
+        $backend = $this->createMock(UserInterface::class);
2266
+        $targetUser
2267
+            ->expects($this->any())
2268
+            ->method('getBackend')
2269
+            ->willReturn($backend);
2270
+
2271
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2272
+    }
2273
+
2274
+    public function testEditUserSelfEditChangeLanguage(): void {
2275
+        $this->l10nFactory->expects($this->once())
2276
+            ->method('findAvailableLanguages')
2277
+            ->willReturn(['en', 'de', 'sv']);
2278
+        $this->config->expects($this->any())
2279
+            ->method('getSystemValue')
2280
+            ->willReturnMap([
2281
+                ['allow_user_to_change_display_name', true, true],
2282
+                ['force_language', false, false],
2283
+            ]);
2284
+
2285
+        $loggedInUser = $this->createMock(IUser::class);
2286
+        $loggedInUser
2287
+            ->expects($this->any())
2288
+            ->method('getUID')
2289
+            ->willReturn('UserToEdit');
2290
+        $targetUser = $this->createMock(IUser::class);
2291
+        $this->config->expects($this->once())
2292
+            ->method('setUserValue')
2293
+            ->with('UserToEdit', 'core', 'lang', 'de');
2294
+        $this->userSession
2295
+            ->expects($this->once())
2296
+            ->method('getUser')
2297
+            ->willReturn($loggedInUser);
2298
+        $this->userManager
2299
+            ->expects($this->once())
2300
+            ->method('get')
2301
+            ->with('UserToEdit')
2302
+            ->willReturn($targetUser);
2303
+        $this->groupManager
2304
+            ->expects($this->atLeastOnce())
2305
+            ->method('isAdmin')
2306
+            ->with('UserToEdit')
2307
+            ->willReturn(false);
2308
+        $targetUser
2309
+            ->expects($this->any())
2310
+            ->method('getUID')
2311
+            ->willReturn('UserToEdit');
2312
+
2313
+        $backend = $this->createMock(UserInterface::class);
2314
+        $targetUser
2315
+            ->expects($this->any())
2316
+            ->method('getBackend')
2317
+            ->willReturn($backend);
2318
+
2319
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2320
+    }
2321
+
2322
+    public static function dataEditUserSelfEditChangeLanguageButForced(): array {
2323
+        return [
2324
+            ['de'],
2325
+            [true],
2326
+        ];
2327
+    }
2328
+
2329
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataEditUserSelfEditChangeLanguageButForced')]
2330
+    public function testEditUserSelfEditChangeLanguageButForced($forced): void {
2331
+        $this->expectException(OCSException::class);
2332
+
2333
+        $this->config->expects($this->any())
2334
+            ->method('getSystemValue')
2335
+            ->willReturnMap([
2336
+                ['allow_user_to_change_display_name', true, true],
2337
+                ['force_language', false, $forced],
2338
+            ]);
2339
+
2340
+        $loggedInUser = $this->createMock(IUser::class);
2341
+        $loggedInUser
2342
+            ->expects($this->any())
2343
+            ->method('getUID')
2344
+            ->willReturn('UserToEdit');
2345
+        $targetUser = $this->createMock(IUser::class);
2346
+        $this->config->expects($this->never())
2347
+            ->method('setUserValue');
2348
+        $this->userSession
2349
+            ->expects($this->once())
2350
+            ->method('getUser')
2351
+            ->willReturn($loggedInUser);
2352
+        $this->userManager
2353
+            ->expects($this->once())
2354
+            ->method('get')
2355
+            ->with('UserToEdit')
2356
+            ->willReturn($targetUser);
2357
+        $this->groupManager
2358
+            ->expects($this->atLeastOnce())
2359
+            ->method('isAdmin')
2360
+            ->with('UserToEdit')
2361
+            ->willReturn(false);
2362
+        $targetUser
2363
+            ->expects($this->any())
2364
+            ->method('getUID')
2365
+            ->willReturn('UserToEdit');
2366
+
2367
+        $backend = $this->createMock(UserInterface::class);
2368
+        $targetUser
2369
+            ->expects($this->any())
2370
+            ->method('getBackend')
2371
+            ->willReturn($backend);
2372
+
2373
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2374
+    }
2375
+
2376
+    public function testEditUserAdminEditChangeLanguage(): void {
2377
+        $this->l10nFactory->expects($this->once())
2378
+            ->method('findAvailableLanguages')
2379
+            ->willReturn(['en', 'de', 'sv']);
2380
+
2381
+        $loggedInUser = $this->createMock(IUser::class);
2382
+        $loggedInUser
2383
+            ->expects($this->any())
2384
+            ->method('getUID')
2385
+            ->willReturn('admin');
2386
+        $targetUser = $this->createMock(IUser::class);
2387
+        $this->config->expects($this->once())
2388
+            ->method('setUserValue')
2389
+            ->with('UserToEdit', 'core', 'lang', 'de');
2390
+        $this->userSession
2391
+            ->expects($this->once())
2392
+            ->method('getUser')
2393
+            ->willReturn($loggedInUser);
2394
+        $this->userManager
2395
+            ->expects($this->once())
2396
+            ->method('get')
2397
+            ->with('UserToEdit')
2398
+            ->willReturn($targetUser);
2399
+        $this->groupManager
2400
+            ->expects($this->once())
2401
+            ->method('isAdmin')
2402
+            ->with('admin')
2403
+            ->willReturn(true);
2404
+        $subAdminManager = $this->createMock(SubAdmin::class);
2405
+        $this->groupManager
2406
+            ->expects($this->once())
2407
+            ->method('getSubAdmin')
2408
+            ->willReturn($subAdminManager);
2409
+        $targetUser
2410
+            ->expects($this->any())
2411
+            ->method('getUID')
2412
+            ->willReturn('UserToEdit');
2413
+
2414
+        $backend = $this->createMock(UserInterface::class);
2415
+        $targetUser
2416
+            ->expects($this->any())
2417
+            ->method('getBackend')
2418
+            ->willReturn($backend);
2419
+
2420
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData());
2421
+    }
2422
+
2423
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataEditUserSelfEditChangeLanguageButForced')]
2424
+    public function testEditUserAdminEditChangeLanguageInvalidLanguage(): void {
2425
+        $this->expectException(OCSException::class);
2426
+
2427
+
2428
+        $this->l10nFactory->expects($this->once())
2429
+            ->method('findAvailableLanguages')
2430
+            ->willReturn(['en', 'de', 'sv']);
2431
+
2432
+        $loggedInUser = $this->createMock(IUser::class);
2433
+        $loggedInUser
2434
+            ->expects($this->any())
2435
+            ->method('getUID')
2436
+            ->willReturn('admin');
2437
+        $targetUser = $this->createMock(IUser::class);
2438
+        $this->config->expects($this->never())
2439
+            ->method('setUserValue');
2440
+        $this->userSession
2441
+            ->expects($this->once())
2442
+            ->method('getUser')
2443
+            ->willReturn($loggedInUser);
2444
+        $this->userManager
2445
+            ->expects($this->once())
2446
+            ->method('get')
2447
+            ->with('UserToEdit')
2448
+            ->willReturn($targetUser);
2449
+        $this->groupManager
2450
+            ->expects($this->once())
2451
+            ->method('isAdmin')
2452
+            ->with('admin')
2453
+            ->willReturn(true);
2454
+        $subAdminManager = $this->createMock(SubAdmin::class);
2455
+        $this->groupManager
2456
+            ->expects($this->once())
2457
+            ->method('getSubAdmin')
2458
+            ->willReturn($subAdminManager);
2459
+        $targetUser
2460
+            ->expects($this->any())
2461
+            ->method('getUID')
2462
+            ->willReturn('UserToEdit');
2463
+
2464
+        $backend = $this->createMock(UserInterface::class);
2465
+        $targetUser
2466
+            ->expects($this->any())
2467
+            ->method('getBackend')
2468
+            ->willReturn($backend);
2469
+
2470
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'ru')->getData());
2471
+    }
2472
+
2473
+    public function testEditUserSubadminUserAccessible(): void {
2474
+        $this->config
2475
+            ->expects($this->once())
2476
+            ->method('getAppValue')
2477
+            ->willReturnCallback(function ($appid, $key, $default) {
2478
+                if ($key === 'max_quota') {
2479
+                    return '-1';
2480
+                }
2481
+                return null;
2482
+            });
2483
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2484
+        $loggedInUser
2485
+            ->expects($this->any())
2486
+            ->method('getUID')
2487
+            ->willReturn('subadmin');
2488
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2489
+        $targetUser->expects($this->once())
2490
+            ->method('setQuota')
2491
+            ->with('2.9 MB');
2492
+        $this->userSession
2493
+            ->expects($this->once())
2494
+            ->method('getUser')
2495
+            ->willReturn($loggedInUser);
2496
+        $this->userManager
2497
+            ->expects($this->once())
2498
+            ->method('get')
2499
+            ->with('UserToEdit')
2500
+            ->willReturn($targetUser);
2501
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2502
+            ->disableOriginalConstructor()
2503
+            ->getMock();
2504
+        $subAdminManager
2505
+            ->expects($this->once())
2506
+            ->method('isUserAccessible')
2507
+            ->with($loggedInUser, $targetUser)
2508
+            ->willReturn(true);
2509
+        $this->groupManager
2510
+            ->expects($this->once())
2511
+            ->method('getSubAdmin')
2512
+            ->willReturn($subAdminManager);
2513
+        $targetUser
2514
+            ->expects($this->any())
2515
+            ->method('getUID')
2516
+            ->willReturn('UID');
2517
+
2518
+        $backend = $this->createMock(UserInterface::class);
2519
+        $targetUser
2520
+            ->expects($this->any())
2521
+            ->method('getBackend')
2522
+            ->willReturn($backend);
2523
+
2524
+        $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData());
2525
+    }
2526
+
2527
+
2528
+    public function testEditUserSubadminUserInaccessible(): void {
2529
+        $this->expectException(OCSException::class);
2530
+        $this->expectExceptionCode(998);
2531
+
2532
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2533
+        $loggedInUser
2534
+            ->expects($this->any())
2535
+            ->method('getUID')
2536
+            ->willReturn('subadmin');
2537
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2538
+        $this->userSession
2539
+            ->expects($this->once())
2540
+            ->method('getUser')
2541
+            ->willReturn($loggedInUser);
2542
+        $this->userManager
2543
+            ->expects($this->once())
2544
+            ->method('get')
2545
+            ->with('UserToEdit')
2546
+            ->willReturn($targetUser);
2547
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2548
+            ->disableOriginalConstructor()
2549
+            ->getMock();
2550
+        $subAdminManager
2551
+            ->expects($this->once())
2552
+            ->method('isUserAccessible')
2553
+            ->with($loggedInUser, $targetUser)
2554
+            ->willReturn(false);
2555
+        $this->groupManager
2556
+            ->expects($this->once())
2557
+            ->method('getSubAdmin')
2558
+            ->willReturn($subAdminManager);
2559
+        $targetUser
2560
+            ->expects($this->any())
2561
+            ->method('getUID')
2562
+            ->willReturn('UID');
2563
+
2564
+        $this->api->editUser('UserToEdit', 'quota', 'value');
2565
+    }
2566
+
2567
+
2568
+    public function testDeleteUserNotExistingUser(): void {
2569
+        $this->expectException(OCSException::class);
2570
+        $this->expectExceptionCode(998);
2571
+
2572
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2573
+        $loggedInUser
2574
+            ->expects($this->any())
2575
+            ->method('getUID')
2576
+            ->willReturn('UserToEdit');
2577
+        $this->userSession
2578
+            ->expects($this->once())
2579
+            ->method('getUser')
2580
+            ->willReturn($loggedInUser);
2581
+        $this->userManager
2582
+            ->expects($this->once())
2583
+            ->method('get')
2584
+            ->with('UserToDelete')
2585
+            ->willReturn(null);
2586
+
2587
+        $this->api->deleteUser('UserToDelete');
2588
+    }
2589
+
2590
+
2591
+    public function testDeleteUserSelf(): void {
2592
+        $this->expectException(OCSException::class);
2593
+        $this->expectExceptionCode(101);
2594
+
2595
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2596
+        $loggedInUser
2597
+            ->expects($this->any())
2598
+            ->method('getUID')
2599
+            ->willReturn('UID');
2600
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2601
+        $targetUser
2602
+            ->expects($this->once())
2603
+            ->method('getUID')
2604
+            ->willReturn('UID');
2605
+        $this->userSession
2606
+            ->expects($this->once())
2607
+            ->method('getUser')
2608
+            ->willReturn($loggedInUser);
2609
+        $this->userManager
2610
+            ->expects($this->once())
2611
+            ->method('get')
2612
+            ->with('UserToDelete')
2613
+            ->willReturn($targetUser);
2614
+
2615
+        $this->api->deleteUser('UserToDelete');
2616
+    }
2617
+
2618
+    public function testDeleteSuccessfulUserAsAdmin(): void {
2619
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2620
+        $loggedInUser
2621
+            ->expects($this->any())
2622
+            ->method('getUID')
2623
+            ->willReturn('admin');
2624
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2625
+        $targetUser
2626
+            ->expects($this->once())
2627
+            ->method('getUID')
2628
+            ->willReturn('UID');
2629
+        $this->userSession
2630
+            ->expects($this->once())
2631
+            ->method('getUser')
2632
+            ->willReturn($loggedInUser);
2633
+        $this->userManager
2634
+            ->expects($this->once())
2635
+            ->method('get')
2636
+            ->with('UserToDelete')
2637
+            ->willReturn($targetUser);
2638
+        $this->groupManager
2639
+            ->expects($this->once())
2640
+            ->method('isAdmin')
2641
+            ->with('admin')
2642
+            ->willReturn(true);
2643
+        $targetUser
2644
+            ->expects($this->once())
2645
+            ->method('delete')
2646
+            ->willReturn(true);
2647
+
2648
+        $this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData());
2649
+    }
2650
+
2651
+
2652
+    public function testDeleteUnsuccessfulUserAsAdmin(): void {
2653
+        $this->expectException(OCSException::class);
2654
+        $this->expectExceptionCode(101);
2655
+
2656
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2657
+        $loggedInUser
2658
+            ->expects($this->any())
2659
+            ->method('getUID')
2660
+            ->willReturn('admin');
2661
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2662
+        $targetUser
2663
+            ->expects($this->once())
2664
+            ->method('getUID')
2665
+            ->willReturn('UID');
2666
+        $this->userSession
2667
+            ->expects($this->once())
2668
+            ->method('getUser')
2669
+            ->willReturn($loggedInUser);
2670
+        $this->userManager
2671
+            ->expects($this->once())
2672
+            ->method('get')
2673
+            ->with('UserToDelete')
2674
+            ->willReturn($targetUser);
2675
+        $this->groupManager
2676
+            ->expects($this->once())
2677
+            ->method('isAdmin')
2678
+            ->with('admin')
2679
+            ->willReturn(true);
2680
+        $targetUser
2681
+            ->expects($this->once())
2682
+            ->method('delete')
2683
+            ->willReturn(false);
2684
+
2685
+        $this->api->deleteUser('UserToDelete');
2686
+    }
2687
+
2688
+    public function testDeleteSuccessfulUserAsSubadmin(): void {
2689
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2690
+        $loggedInUser
2691
+            ->expects($this->any())
2692
+            ->method('getUID')
2693
+            ->willReturn('subadmin');
2694
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2695
+        $targetUser
2696
+            ->expects($this->once())
2697
+            ->method('getUID')
2698
+            ->willReturn('UID');
2699
+        $this->userSession
2700
+            ->expects($this->once())
2701
+            ->method('getUser')
2702
+            ->willReturn($loggedInUser);
2703
+        $this->userManager
2704
+            ->expects($this->once())
2705
+            ->method('get')
2706
+            ->with('UserToDelete')
2707
+            ->willReturn($targetUser);
2708
+        $this->groupManager
2709
+            ->expects($this->once())
2710
+            ->method('isAdmin')
2711
+            ->with('subadmin')
2712
+            ->willReturn(false);
2713
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2714
+            ->disableOriginalConstructor()->getMock();
2715
+        $subAdminManager
2716
+            ->expects($this->once())
2717
+            ->method('isUserAccessible')
2718
+            ->with($loggedInUser, $targetUser)
2719
+            ->willReturn(true);
2720
+        $this->groupManager
2721
+            ->expects($this->once())
2722
+            ->method('getSubAdmin')
2723
+            ->willReturn($subAdminManager);
2724
+        $targetUser
2725
+            ->expects($this->once())
2726
+            ->method('delete')
2727
+            ->willReturn(true);
2728
+
2729
+        $this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData());
2730
+    }
2731
+
2732
+
2733
+    public function testDeleteUnsuccessfulUserAsSubadmin(): void {
2734
+        $this->expectException(OCSException::class);
2735
+        $this->expectExceptionCode(101);
2736
+
2737
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2738
+        $loggedInUser
2739
+            ->expects($this->any())
2740
+            ->method('getUID')
2741
+            ->willReturn('subadmin');
2742
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2743
+        $targetUser
2744
+            ->expects($this->once())
2745
+            ->method('getUID')
2746
+            ->willReturn('UID');
2747
+        $this->userSession
2748
+            ->expects($this->once())
2749
+            ->method('getUser')
2750
+            ->willReturn($loggedInUser);
2751
+        $this->userManager
2752
+            ->expects($this->once())
2753
+            ->method('get')
2754
+            ->with('UserToDelete')
2755
+            ->willReturn($targetUser);
2756
+        $this->groupManager
2757
+            ->expects($this->once())
2758
+            ->method('isAdmin')
2759
+            ->with('subadmin')
2760
+            ->willReturn(false);
2761
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2762
+            ->disableOriginalConstructor()->getMock();
2763
+        $subAdminManager
2764
+            ->expects($this->once())
2765
+            ->method('isUserAccessible')
2766
+            ->with($loggedInUser, $targetUser)
2767
+            ->willReturn(true);
2768
+        $this->groupManager
2769
+            ->expects($this->once())
2770
+            ->method('getSubAdmin')
2771
+            ->willReturn($subAdminManager);
2772
+        $targetUser
2773
+            ->expects($this->once())
2774
+            ->method('delete')
2775
+            ->willReturn(false);
2776
+
2777
+        $this->api->deleteUser('UserToDelete');
2778
+    }
2779
+
2780
+
2781
+    public function testDeleteUserAsSubAdminAndUserIsNotAccessible(): void {
2782
+        $this->expectException(OCSException::class);
2783
+        $this->expectExceptionCode(998);
2784
+
2785
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2786
+        $loggedInUser
2787
+            ->expects($this->any())
2788
+            ->method('getUID')
2789
+            ->willReturn('subadmin');
2790
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2791
+        $targetUser
2792
+            ->expects($this->once())
2793
+            ->method('getUID')
2794
+            ->willReturn('UID');
2795
+        $this->userSession
2796
+            ->expects($this->once())
2797
+            ->method('getUser')
2798
+            ->willReturn($loggedInUser);
2799
+        $this->userManager
2800
+            ->expects($this->once())
2801
+            ->method('get')
2802
+            ->with('UserToDelete')
2803
+            ->willReturn($targetUser);
2804
+        $this->groupManager
2805
+            ->expects($this->once())
2806
+            ->method('isAdmin')
2807
+            ->with('subadmin')
2808
+            ->willReturn(false);
2809
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2810
+            ->disableOriginalConstructor()->getMock();
2811
+        $subAdminManager
2812
+            ->expects($this->once())
2813
+            ->method('isUserAccessible')
2814
+            ->with($loggedInUser, $targetUser)
2815
+            ->willReturn(false);
2816
+        $this->groupManager
2817
+            ->expects($this->once())
2818
+            ->method('getSubAdmin')
2819
+            ->willReturn($subAdminManager);
2820
+
2821
+        $this->api->deleteUser('UserToDelete');
2822
+    }
2823
+
2824
+
2825
+    public function testGetUsersGroupsTargetUserNotExisting(): void {
2826
+        $this->expectException(OCSException::class);
2827
+        $this->expectExceptionCode(998);
2828
+
2829
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2830
+        $this->userSession
2831
+            ->expects($this->once())
2832
+            ->method('getUser')
2833
+            ->willReturn($loggedInUser);
2834
+
2835
+        $this->api->getUsersGroups('UserToLookup');
2836
+    }
2837
+
2838
+    public function testGetUsersGroupsSelfTargetted(): void {
2839
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2840
+        $loggedInUser
2841
+            ->expects($this->exactly(3))
2842
+            ->method('getUID')
2843
+            ->willReturn('UserToLookup');
2844
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2845
+        $targetUser
2846
+            ->expects($this->once())
2847
+            ->method('getUID')
2848
+            ->willReturn('UserToLookup');
2849
+        $this->userSession
2850
+            ->expects($this->once())
2851
+            ->method('getUser')
2852
+            ->willReturn($loggedInUser);
2853
+        $this->userManager
2854
+            ->expects($this->once())
2855
+            ->method('get')
2856
+            ->with('UserToLookup')
2857
+            ->willReturn($targetUser);
2858
+        $this->groupManager
2859
+            ->expects($this->once())
2860
+            ->method('getUserGroupIds')
2861
+            ->with($targetUser)
2862
+            ->willReturn(['DummyValue']);
2863
+
2864
+        $this->assertEquals(['groups' => ['DummyValue']], $this->api->getUsersGroups('UserToLookup')->getData());
2865
+    }
2866
+
2867
+    public function testGetUsersGroupsForAdminUser(): void {
2868
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2869
+        $loggedInUser
2870
+            ->expects($this->exactly(3))
2871
+            ->method('getUID')
2872
+            ->willReturn('admin');
2873
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2874
+        $targetUser
2875
+            ->expects($this->once())
2876
+            ->method('getUID')
2877
+            ->willReturn('UserToLookup');
2878
+        $this->userSession
2879
+            ->expects($this->once())
2880
+            ->method('getUser')
2881
+            ->willReturn($loggedInUser);
2882
+        $this->userManager
2883
+            ->expects($this->once())
2884
+            ->method('get')
2885
+            ->with('UserToLookup')
2886
+            ->willReturn($targetUser);
2887
+        $this->groupManager
2888
+            ->expects($this->once())
2889
+            ->method('getUserGroupIds')
2890
+            ->with($targetUser)
2891
+            ->willReturn(['DummyValue']);
2892
+        $this->groupManager
2893
+            ->expects($this->once())
2894
+            ->method('isAdmin')
2895
+            ->with('admin')
2896
+            ->willReturn(true);
2897
+
2898
+        $this->assertEquals(['groups' => ['DummyValue']], $this->api->getUsersGroups('UserToLookup')->getData());
2899
+    }
2900
+
2901
+    public function testGetUsersGroupsForSubAdminUserAndUserIsAccessible(): void {
2902
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2903
+        $loggedInUser
2904
+            ->expects($this->exactly(3))
2905
+            ->method('getUID')
2906
+            ->willReturn('subadmin');
2907
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2908
+        $targetUser
2909
+            ->expects($this->once())
2910
+            ->method('getUID')
2911
+            ->willReturn('UserToLookup');
2912
+        $this->userSession
2913
+            ->expects($this->once())
2914
+            ->method('getUser')
2915
+            ->willReturn($loggedInUser);
2916
+        $this->userManager
2917
+            ->expects($this->once())
2918
+            ->method('get')
2919
+            ->with('UserToLookup')
2920
+            ->willReturn($targetUser);
2921
+        $this->groupManager
2922
+            ->expects($this->once())
2923
+            ->method('isAdmin')
2924
+            ->with('subadmin')
2925
+            ->willReturn(false);
2926
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2927
+            ->disableOriginalConstructor()->getMock();
2928
+        $subAdminManager
2929
+            ->expects($this->once())
2930
+            ->method('isUserAccessible')
2931
+            ->with($loggedInUser, $targetUser)
2932
+            ->willReturn(true);
2933
+        $this->groupManager
2934
+            ->expects($this->once())
2935
+            ->method('getSubAdmin')
2936
+            ->willReturn($subAdminManager);
2937
+        $group1 = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
2938
+        $group1
2939
+            ->expects($this->any())
2940
+            ->method('getGID')
2941
+            ->willReturn('Group1');
2942
+        $group2 = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
2943
+        $group2
2944
+            ->expects($this->any())
2945
+            ->method('getGID')
2946
+            ->willReturn('Group2');
2947
+        $subAdminManager
2948
+            ->expects($this->once())
2949
+            ->method('getSubAdminsGroups')
2950
+            ->with($loggedInUser)
2951
+            ->willReturn([$group1, $group2]);
2952
+        $this->groupManager
2953
+            ->expects($this->any())
2954
+            ->method('getUserGroupIds')
2955
+            ->with($targetUser)
2956
+            ->willReturn(['Group1']);
2957
+
2958
+        $this->assertEquals(['groups' => ['Group1']], $this->api->getUsersGroups('UserToLookup')->getData());
2959
+    }
2960
+
2961
+
2962
+    public function testGetUsersGroupsForSubAdminUserAndUserIsInaccessible(): void {
2963
+        $this->expectException(OCSException::class);
2964
+        $this->expectExceptionCode(998);
2965
+
2966
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2967
+        $loggedInUser
2968
+            ->expects($this->exactly(3))
2969
+            ->method('getUID')
2970
+            ->willReturn('subadmin');
2971
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
2972
+        $targetUser
2973
+            ->expects($this->once())
2974
+            ->method('getUID')
2975
+            ->willReturn('UserToLookup');
2976
+        $this->userSession
2977
+            ->expects($this->once())
2978
+            ->method('getUser')
2979
+            ->willReturn($loggedInUser);
2980
+        $this->userManager
2981
+            ->expects($this->once())
2982
+            ->method('get')
2983
+            ->with('UserToLookup')
2984
+            ->willReturn($targetUser);
2985
+        $this->groupManager
2986
+            ->expects($this->once())
2987
+            ->method('isAdmin')
2988
+            ->with('subadmin')
2989
+            ->willReturn(false);
2990
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
2991
+            ->disableOriginalConstructor()->getMock();
2992
+        $subAdminManager
2993
+            ->expects($this->once())
2994
+            ->method('isUserAccessible')
2995
+            ->with($loggedInUser, $targetUser)
2996
+            ->willReturn(false);
2997
+        $this->groupManager
2998
+            ->expects($this->once())
2999
+            ->method('getSubAdmin')
3000
+            ->willReturn($subAdminManager);
3001
+        $this->groupManager
3002
+            ->expects($this->any())
3003
+            ->method('getUserGroupIds')
3004
+            ->with($targetUser)
3005
+            ->willReturn(['Group1']);
3006
+
3007
+        $this->api->getUsersGroups('UserToLookup');
3008
+    }
3009
+
3010
+
3011
+    public function testAddToGroupWithTargetGroupNotExisting(): void {
3012
+        $this->expectException(OCSException::class);
3013
+        $this->expectExceptionCode(102);
3014
+
3015
+        $this->groupManager->expects($this->once())
3016
+            ->method('get')
3017
+            ->with('GroupToAddTo')
3018
+            ->willReturn(null);
3019
+
3020
+        $this->api->addToGroup('TargetUser', 'GroupToAddTo');
3021
+    }
3022
+
3023
+
3024
+    public function testAddToGroupWithNoGroupSpecified(): void {
3025
+        $this->expectException(OCSException::class);
3026
+        $this->expectExceptionCode(101);
3027
+
3028
+        $this->api->addToGroup('TargetUser');
3029
+    }
3030
+
3031
+
3032
+    public function testAddToGroupWithTargetUserNotExisting(): void {
3033
+        $this->expectException(OCSException::class);
3034
+        $this->expectExceptionCode(103);
3035
+
3036
+        $targetGroup = $this->createMock(IGroup::class);
3037
+        $this->groupManager->expects($this->once())
3038
+            ->method('get')
3039
+            ->with('GroupToAddTo')
3040
+            ->willReturn($targetGroup);
3041
+
3042
+        $this->api->addToGroup('TargetUser', 'GroupToAddTo');
3043
+    }
3044
+
3045
+
3046
+    public function testAddToGroupNoSubadmin(): void {
3047
+        $this->expectException(OCSException::class);
3048
+        $this->expectExceptionCode(104);
3049
+
3050
+        $targetUser = $this->createMock(IUser::class);
3051
+        $loggedInUser = $this->createMock(IUser::class);
3052
+        $loggedInUser->expects($this->exactly(2))
3053
+            ->method('getUID')
3054
+            ->willReturn('subadmin');
3055
+
3056
+        $targetGroup = $this->createMock(IGroup::class);
3057
+        $targetGroup->expects($this->never())
3058
+            ->method('addUser')
3059
+            ->with($targetUser);
3060
+
3061
+        $this->groupManager->expects($this->once())
3062
+            ->method('get')
3063
+            ->with('GroupToAddTo')
3064
+            ->willReturn($targetGroup);
3065
+
3066
+
3067
+        $subAdminManager = $this->createMock(SubAdmin::class);
3068
+        $subAdminManager->expects($this->once())
3069
+            ->method('isSubAdminOfGroup')
3070
+            ->with($loggedInUser, $targetGroup)
3071
+            ->willReturn(false);
3072
+
3073
+        $this->groupManager->expects($this->once())
3074
+            ->method('getSubAdmin')
3075
+            ->willReturn($subAdminManager);
3076
+        $this->groupManager->expects($this->once())
3077
+            ->method('isAdmin')
3078
+            ->with('subadmin')
3079
+            ->willReturn(false);
3080
+
3081
+        $this->userManager->expects($this->once())
3082
+            ->method('get')
3083
+            ->with('TargetUser')
3084
+            ->willReturn($targetUser);
3085
+
3086
+        $this->userSession->expects($this->once())
3087
+            ->method('getUser')
3088
+            ->willReturn($loggedInUser);
3089
+
3090
+        $this->api->addToGroup('TargetUser', 'GroupToAddTo');
3091
+    }
3092
+
3093
+    public function testAddToGroupSuccessAsSubadmin(): void {
3094
+        $targetUser = $this->createMock(IUser::class);
3095
+        $loggedInUser = $this->createMock(IUser::class);
3096
+        $loggedInUser->expects($this->exactly(2))
3097
+            ->method('getUID')
3098
+            ->willReturn('subadmin');
3099
+
3100
+        $targetGroup = $this->createMock(IGroup::class);
3101
+        $targetGroup->expects($this->once())
3102
+            ->method('addUser')
3103
+            ->with($targetUser);
3104
+
3105
+        $this->groupManager->expects($this->once())
3106
+            ->method('get')
3107
+            ->with('GroupToAddTo')
3108
+            ->willReturn($targetGroup);
3109 3109
 
3110
-
3111
-		$subAdminManager = $this->createMock(SubAdmin::class);
3112
-		$subAdminManager->expects($this->once())
3113
-			->method('isSubAdminOfGroup')
3114
-			->with($loggedInUser, $targetGroup)
3115
-			->willReturn(true);
3116
-
3117
-		$this->groupManager->expects($this->once())
3118
-			->method('getSubAdmin')
3119
-			->willReturn($subAdminManager);
3120
-		$this->groupManager->expects($this->once())
3121
-			->method('isAdmin')
3122
-			->with('subadmin')
3123
-			->willReturn(false);
3124
-
3125
-		$this->userManager->expects($this->once())
3126
-			->method('get')
3127
-			->with('TargetUser')
3128
-			->willReturn($targetUser);
3129
-
3130
-		$this->userSession->expects($this->once())
3131
-			->method('getUser')
3132
-			->willReturn($loggedInUser);
3133
-
3134
-		$this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo'));
3135
-	}
3136
-
3137
-	public function testAddToGroupSuccessAsAdmin(): void {
3138
-		$targetUser = $this->createMock(IUser::class);
3139
-		$loggedInUser = $this->createMock(IUser::class);
3140
-		$loggedInUser->expects($this->exactly(2))
3141
-			->method('getUID')
3142
-			->willReturn('admin');
3143
-
3144
-		$targetGroup = $this->createMock(IGroup::class);
3145
-		$targetGroup->expects($this->once())
3146
-			->method('addUser')
3147
-			->with($targetUser);
3148
-
3149
-		$this->groupManager->expects($this->once())
3150
-			->method('get')
3151
-			->with('GroupToAddTo')
3152
-			->willReturn($targetGroup);
3153
-
3154
-
3155
-		$subAdminManager = $this->createMock(SubAdmin::class);
3156
-		$subAdminManager->expects($this->never())
3157
-			->method('isSubAdminOfGroup');
3158
-
3159
-		$this->groupManager->expects($this->once())
3160
-			->method('getSubAdmin')
3161
-			->willReturn($subAdminManager);
3162
-		$this->groupManager->expects($this->once())
3163
-			->method('isAdmin')
3164
-			->with('admin')
3165
-			->willReturn(true);
3166
-
3167
-		$this->userManager->expects($this->once())
3168
-			->method('get')
3169
-			->with('TargetUser')
3170
-			->willReturn($targetUser);
3171
-
3172
-		$this->userSession->expects($this->once())
3173
-			->method('getUser')
3174
-			->willReturn($loggedInUser);
3175
-
3176
-		$this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo'));
3177
-	}
3178
-
3179
-
3180
-	public function testRemoveFromGroupWithNoTargetGroup(): void {
3181
-		$this->expectException(OCSException::class);
3182
-		$this->expectExceptionCode(101);
3183
-
3184
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3185
-		$this->userSession
3186
-			->expects($this->once())
3187
-			->method('getUser')
3188
-			->willReturn($loggedInUser);
3189
-
3190
-		$this->api->removeFromGroup('TargetUser', '');
3191
-	}
3192
-
3193
-
3194
-	public function testRemoveFromGroupWithEmptyTargetGroup(): void {
3195
-		$this->expectException(OCSException::class);
3196
-		$this->expectExceptionCode(101);
3197
-
3198
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3199
-		$this->userSession
3200
-			->expects($this->once())
3201
-			->method('getUser')
3202
-			->willReturn($loggedInUser);
3203
-
3204
-		$this->api->removeFromGroup('TargetUser', '');
3205
-	}
3206
-
3207
-
3208
-	public function testRemoveFromGroupWithNotExistingTargetGroup(): void {
3209
-		$this->expectException(OCSException::class);
3210
-		$this->expectExceptionCode(102);
3211
-
3212
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3213
-		$this->userSession
3214
-			->expects($this->once())
3215
-			->method('getUser')
3216
-			->willReturn($loggedInUser);
3217
-		$this->groupManager
3218
-			->expects($this->once())
3219
-			->method('get')
3220
-			->with('TargetGroup')
3221
-			->willReturn(null);
3222
-
3223
-		$this->api->removeFromGroup('TargetUser', 'TargetGroup');
3224
-	}
3225
-
3226
-
3227
-	public function testRemoveFromGroupWithNotExistingTargetUser(): void {
3228
-		$this->expectException(OCSException::class);
3229
-		$this->expectExceptionCode(103);
3230
-
3231
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3232
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3233
-		$this->userSession
3234
-			->expects($this->once())
3235
-			->method('getUser')
3236
-			->willReturn($loggedInUser);
3237
-		$this->groupManager
3238
-			->expects($this->once())
3239
-			->method('get')
3240
-			->with('TargetGroup')
3241
-			->willReturn($targetGroup);
3242
-		$this->userManager
3243
-			->expects($this->once())
3244
-			->method('get')
3245
-			->with('TargetUser')
3246
-			->willReturn(null);
3247
-
3248
-		$this->api->removeFromGroup('TargetUser', 'TargetGroup');
3249
-	}
3250
-
3251
-
3252
-	public function testRemoveFromGroupWithoutPermission(): void {
3253
-		$this->expectException(OCSException::class);
3254
-		$this->expectExceptionCode(104);
3255
-
3256
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3257
-		$loggedInUser
3258
-			->expects($this->exactly(2))
3259
-			->method('getUID')
3260
-			->willReturn('unauthorizedUser');
3261
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3262
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3263
-		$this->userSession
3264
-			->expects($this->once())
3265
-			->method('getUser')
3266
-			->willReturn($loggedInUser);
3267
-		$this->groupManager
3268
-			->expects($this->once())
3269
-			->method('get')
3270
-			->with('TargetGroup')
3271
-			->willReturn($targetGroup);
3272
-		$this->userManager
3273
-			->expects($this->once())
3274
-			->method('get')
3275
-			->with('TargetUser')
3276
-			->willReturn($targetUser);
3277
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3278
-			->disableOriginalConstructor()->getMock();
3279
-		$this->groupManager
3280
-			->expects($this->once())
3281
-			->method('getSubAdmin')
3282
-			->willReturn($subAdminManager);
3283
-		$this->groupManager
3284
-			->expects($this->once())
3285
-			->method('isAdmin')
3286
-			->with('unauthorizedUser')
3287
-			->willReturn(false);
3288
-
3289
-		$this->api->removeFromGroup('TargetUser', 'TargetGroup');
3290
-	}
3291
-
3292
-
3293
-	public function testRemoveFromGroupAsAdminFromAdmin(): void {
3294
-		$this->expectException(OCSException::class);
3295
-		$this->expectExceptionMessage('Cannot remove yourself from the admin group');
3296
-		$this->expectExceptionCode(105);
3297
-
3298
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3299
-		$loggedInUser
3300
-			->expects($this->any())
3301
-			->method('getUID')
3302
-			->willReturn('admin');
3303
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3304
-		$targetUser
3305
-			->expects($this->once())
3306
-			->method('getUID')
3307
-			->willReturn('admin');
3308
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3309
-		$targetGroup
3310
-			->expects($this->once())
3311
-			->method('getGID')
3312
-			->willReturn('admin');
3313
-		$this->userSession
3314
-			->expects($this->once())
3315
-			->method('getUser')
3316
-			->willReturn($loggedInUser);
3317
-		$this->groupManager
3318
-			->expects($this->once())
3319
-			->method('get')
3320
-			->with('admin')
3321
-			->willReturn($targetGroup);
3322
-		$this->userManager
3323
-			->expects($this->once())
3324
-			->method('get')
3325
-			->with('Admin')
3326
-			->willReturn($targetUser);
3327
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3328
-			->disableOriginalConstructor()->getMock();
3329
-		$this->groupManager
3330
-			->expects($this->once())
3331
-			->method('getSubAdmin')
3332
-			->willReturn($subAdminManager);
3333
-		$this->groupManager
3334
-			->expects($this->any())
3335
-			->method('isAdmin')
3336
-			->with('admin')
3337
-			->willReturn(true);
3338
-
3339
-		$this->api->removeFromGroup('Admin', 'admin');
3340
-	}
3341
-
3342
-
3343
-	public function testRemoveFromGroupAsSubAdminFromSubAdmin(): void {
3344
-		$this->expectException(OCSException::class);
3345
-		$this->expectExceptionMessage('Cannot remove yourself from this group as you are a sub-admin');
3346
-		$this->expectExceptionCode(105);
3347
-
3348
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3349
-		$loggedInUser
3350
-			->expects($this->any())
3351
-			->method('getUID')
3352
-			->willReturn('subadmin');
3353
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3354
-		$targetUser
3355
-			->expects($this->once())
3356
-			->method('getUID')
3357
-			->willReturn('subadmin');
3358
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3359
-		$targetGroup
3360
-			->expects($this->any())
3361
-			->method('getGID')
3362
-			->willReturn('subadmin');
3363
-		$this->userSession
3364
-			->expects($this->once())
3365
-			->method('getUser')
3366
-			->willReturn($loggedInUser);
3367
-		$this->groupManager
3368
-			->expects($this->once())
3369
-			->method('get')
3370
-			->with('subadmin')
3371
-			->willReturn($targetGroup);
3372
-		$this->userManager
3373
-			->expects($this->once())
3374
-			->method('get')
3375
-			->with('SubAdmin')
3376
-			->willReturn($targetUser);
3377
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3378
-			->disableOriginalConstructor()->getMock();
3379
-		$subAdminManager
3380
-			->expects($this->once())
3381
-			->method('isSubAdminOfGroup')
3382
-			->with($loggedInUser, $targetGroup)
3383
-			->willReturn(true);
3384
-		$this->groupManager
3385
-			->expects($this->once())
3386
-			->method('getSubAdmin')
3387
-			->willReturn($subAdminManager);
3388
-		$this->groupManager
3389
-			->expects($this->any())
3390
-			->method('isAdmin')
3391
-			->with('subadmin')
3392
-			->willReturn(false);
3393
-
3394
-		$this->api->removeFromGroup('SubAdmin', 'subadmin');
3395
-	}
3396
-
3397
-
3398
-	public function testRemoveFromGroupAsSubAdminFromLastSubAdminGroup(): void {
3399
-		$this->expectException(OCSException::class);
3400
-		$this->expectExceptionMessage('Not viable to remove user from the last group you are sub-admin of');
3401
-		$this->expectExceptionCode(105);
3402
-
3403
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3404
-		$loggedInUser
3405
-			->expects($this->any())
3406
-			->method('getUID')
3407
-			->willReturn('subadmin');
3408
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3409
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3410
-		$targetGroup
3411
-			->expects($this->any())
3412
-			->method('getGID')
3413
-			->willReturn('subadmin');
3414
-		$this->userSession
3415
-			->expects($this->once())
3416
-			->method('getUser')
3417
-			->willReturn($loggedInUser);
3418
-		$this->groupManager
3419
-			->expects($this->once())
3420
-			->method('get')
3421
-			->with('subadmin')
3422
-			->willReturn($targetGroup);
3423
-		$this->userManager
3424
-			->expects($this->once())
3425
-			->method('get')
3426
-			->with('AnotherUser')
3427
-			->willReturn($targetUser);
3428
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3429
-			->disableOriginalConstructor()->getMock();
3430
-		$subAdminManager
3431
-			->expects($this->once())
3432
-			->method('isSubAdminOfGroup')
3433
-			->with($loggedInUser, $targetGroup)
3434
-			->willReturn(true);
3435
-		$this->groupManager
3436
-			->expects($this->once())
3437
-			->method('getSubAdmin')
3438
-			->willReturn($subAdminManager);
3439
-		$subAdminManager
3440
-			->expects($this->once())
3441
-			->method('getSubAdminsGroups')
3442
-			->with($loggedInUser)
3443
-			->willReturn([$targetGroup]);
3444
-
3445
-		$this->groupManager
3446
-			->expects($this->any())
3447
-			->method('isAdmin')
3448
-			->with('subadmin')
3449
-			->willReturn(false);
3450
-		$this->groupManager
3451
-			->expects($this->once())
3452
-			->method('getUserGroupIds')
3453
-			->with($targetUser)
3454
-			->willReturn(['subadmin', 'other group']);
3455
-
3456
-		$this->api->removeFromGroup('AnotherUser', 'subadmin');
3457
-	}
3458
-
3459
-	public function testRemoveFromGroupSuccessful(): void {
3460
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3461
-		$loggedInUser
3462
-			->expects($this->any())
3463
-			->method('getUID')
3464
-			->willReturn('admin');
3465
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3466
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3467
-		$this->userSession
3468
-			->expects($this->once())
3469
-			->method('getUser')
3470
-			->willReturn($loggedInUser);
3471
-		$this->groupManager
3472
-			->expects($this->once())
3473
-			->method('get')
3474
-			->with('admin')
3475
-			->willReturn($targetGroup);
3476
-		$this->userManager
3477
-			->expects($this->once())
3478
-			->method('get')
3479
-			->with('AnotherUser')
3480
-			->willReturn($targetUser);
3481
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3482
-			->disableOriginalConstructor()->getMock();
3483
-		$this->groupManager
3484
-			->expects($this->once())
3485
-			->method('getSubAdmin')
3486
-			->willReturn($subAdminManager);
3487
-		$this->groupManager
3488
-			->expects($this->any())
3489
-			->method('isAdmin')
3490
-			->with('admin')
3491
-			->willReturn(true);
3492
-		$targetGroup
3493
-			->expects($this->once())
3494
-			->method('removeUser')
3495
-			->with($targetUser);
3496
-
3497
-		$this->assertEquals([], $this->api->removeFromGroup('AnotherUser', 'admin')->getData());
3498
-	}
3499
-
3500
-
3501
-	public function testAddSubAdminWithNotExistingTargetUser(): void {
3502
-		$this->expectException(OCSException::class);
3503
-		$this->expectExceptionMessage('User does not exist');
3504
-		$this->expectExceptionCode(101);
3505
-
3506
-		$this->userManager
3507
-			->expects($this->once())
3508
-			->method('get')
3509
-			->with('NotExistingUser')
3510
-			->willReturn(null);
3511
-
3512
-		$this->api->addSubAdmin('NotExistingUser', '');
3513
-	}
3514
-
3515
-
3516
-	public function testAddSubAdminWithNotExistingTargetGroup(): void {
3517
-		$this->expectException(OCSException::class);
3518
-		$this->expectExceptionMessage('Group does not exist');
3519
-		$this->expectExceptionCode(102);
3520
-
3521
-
3522
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3523
-		$this->userManager
3524
-			->expects($this->once())
3525
-			->method('get')
3526
-			->with('ExistingUser')
3527
-			->willReturn($targetUser);
3528
-		$this->groupManager
3529
-			->expects($this->once())
3530
-			->method('get')
3531
-			->with('NotExistingGroup')
3532
-			->willReturn(null);
3533
-
3534
-		$this->api->addSubAdmin('ExistingUser', 'NotExistingGroup');
3535
-	}
3536
-
3537
-
3538
-	public function testAddSubAdminToAdminGroup(): void {
3539
-		$this->expectException(OCSException::class);
3540
-		$this->expectExceptionMessage('Cannot create sub-admins for admin group');
3541
-		$this->expectExceptionCode(103);
3542
-
3543
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3544
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3545
-		$targetGroup
3546
-			->expects($this->once())
3547
-			->method('getGID')
3548
-			->willReturn('admin');
3549
-		$this->userManager
3550
-			->expects($this->once())
3551
-			->method('get')
3552
-			->with('ExistingUser')
3553
-			->willReturn($targetUser);
3554
-		$this->groupManager
3555
-			->expects($this->once())
3556
-			->method('get')
3557
-			->with('ADmiN')
3558
-			->willReturn($targetGroup);
3559
-
3560
-		$this->api->addSubAdmin('ExistingUser', 'ADmiN');
3561
-	}
3562
-
3563
-	public function testAddSubAdminTwice(): void {
3564
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3565
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3566
-		$this->userManager
3567
-			->expects($this->once())
3568
-			->method('get')
3569
-			->with('ExistingUser')
3570
-			->willReturn($targetUser);
3571
-		$this->groupManager
3572
-			->expects($this->once())
3573
-			->method('get')
3574
-			->with('TargetGroup')
3575
-			->willReturn($targetGroup);
3576
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3577
-			->disableOriginalConstructor()->getMock();
3578
-		$subAdminManager
3579
-			->expects($this->once())
3580
-			->method('isSubAdminOfGroup')
3581
-			->with($targetUser, $targetGroup)
3582
-			->willReturn(true);
3583
-		$this->groupManager
3584
-			->expects($this->once())
3585
-			->method('getSubAdmin')
3586
-			->willReturn($subAdminManager);
3587
-
3588
-		$this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData());
3589
-	}
3590
-
3591
-	public function testAddSubAdminSuccessful(): void {
3592
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3593
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3594
-		$this->userManager
3595
-			->expects($this->once())
3596
-			->method('get')
3597
-			->with('ExistingUser')
3598
-			->willReturn($targetUser);
3599
-		$this->groupManager
3600
-			->expects($this->once())
3601
-			->method('get')
3602
-			->with('TargetGroup')
3603
-			->willReturn($targetGroup);
3604
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3605
-			->disableOriginalConstructor()->getMock();
3606
-		$subAdminManager
3607
-			->expects($this->once())
3608
-			->method('isSubAdminOfGroup')
3609
-			->with($targetUser, $targetGroup)
3610
-			->willReturn(false);
3611
-		$subAdminManager
3612
-			->expects($this->once())
3613
-			->method('createSubAdmin')
3614
-			->with($targetUser, $targetGroup);
3615
-		$this->groupManager
3616
-			->expects($this->once())
3617
-			->method('getSubAdmin')
3618
-			->willReturn($subAdminManager);
3619
-
3620
-		$this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData());
3621
-	}
3622
-
3623
-
3624
-	public function testRemoveSubAdminNotExistingTargetUser(): void {
3625
-		$this->expectException(OCSException::class);
3626
-		$this->expectExceptionMessage('User does not exist');
3627
-		$this->expectExceptionCode(101);
3628
-
3629
-		$this->userManager
3630
-			->expects($this->once())
3631
-			->method('get')
3632
-			->with('NotExistingUser')
3633
-			->willReturn(null);
3634
-
3635
-		$this->api->removeSubAdmin('NotExistingUser', 'GroupToDeleteFrom');
3636
-	}
3637
-
3638
-
3639
-	public function testRemoveSubAdminNotExistingTargetGroup(): void {
3640
-		$this->expectException(OCSException::class);
3641
-		$this->expectExceptionMessage('Group does not exist');
3642
-		$this->expectExceptionCode(101);
3643
-
3644
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3645
-		$this->userManager
3646
-			->expects($this->once())
3647
-			->method('get')
3648
-			->with('ExistingUser')
3649
-			->willReturn($targetUser);
3650
-		$this->groupManager
3651
-			->expects($this->once())
3652
-			->method('get')
3653
-			->with('GroupToDeleteFrom')
3654
-			->willReturn(null);
3655
-
3656
-		$this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom');
3657
-	}
3658
-
3659
-
3660
-
3661
-	public function testRemoveSubAdminFromNotASubadmin(): void {
3662
-		$this->expectException(OCSException::class);
3663
-		$this->expectExceptionMessage('User is not a sub-admin of this group');
3664
-		$this->expectExceptionCode(102);
3665
-
3666
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3667
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3668
-		$this->userManager
3669
-			->expects($this->once())
3670
-			->method('get')
3671
-			->with('ExistingUser')
3672
-			->willReturn($targetUser);
3673
-		$this->groupManager
3674
-			->expects($this->once())
3675
-			->method('get')
3676
-			->with('GroupToDeleteFrom')
3677
-			->willReturn($targetGroup);
3678
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3679
-			->disableOriginalConstructor()->getMock();
3680
-		$subAdminManager
3681
-			->expects($this->once())
3682
-			->method('isSubAdminOfGroup')
3683
-			->with($targetUser, $targetGroup)
3684
-			->willReturn(false);
3685
-		$this->groupManager
3686
-			->expects($this->once())
3687
-			->method('getSubAdmin')
3688
-			->willReturn($subAdminManager);
3689
-
3690
-		$this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom');
3691
-	}
3692
-
3693
-	public function testRemoveSubAdminSuccessful(): void {
3694
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3695
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3696
-		$this->userManager
3697
-			->expects($this->once())
3698
-			->method('get')
3699
-			->with('ExistingUser')
3700
-			->willReturn($targetUser);
3701
-		$this->groupManager
3702
-			->expects($this->once())
3703
-			->method('get')
3704
-			->with('GroupToDeleteFrom')
3705
-			->willReturn($targetGroup);
3706
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3707
-			->disableOriginalConstructor()->getMock();
3708
-		$subAdminManager
3709
-			->expects($this->once())
3710
-			->method('isSubAdminOfGroup')
3711
-			->with($targetUser, $targetGroup)
3712
-			->willReturn(true);
3713
-		$subAdminManager
3714
-			->expects($this->once())
3715
-			->method('deleteSubAdmin')
3716
-			->with($targetUser, $targetGroup);
3717
-		$this->groupManager
3718
-			->expects($this->once())
3719
-			->method('getSubAdmin')
3720
-			->willReturn($subAdminManager);
3721
-
3722
-		$this->assertEquals([], $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom')->getData());
3723
-	}
3724
-
3725
-
3726
-	public function testGetUserSubAdminGroupsNotExistingTargetUser(): void {
3727
-		$this->expectException(OCSException::class);
3728
-		$this->expectExceptionMessage('User does not exist');
3729
-		$this->expectExceptionCode(404);
3730
-
3731
-		$this->userManager
3732
-			->expects($this->once())
3733
-			->method('get')
3734
-			->with('RequestedUser')
3735
-			->willReturn(null);
3736
-
3737
-		$this->api->getUserSubAdminGroups('RequestedUser');
3738
-	}
3739
-
3740
-	public function testGetUserSubAdminGroupsWithGroups(): void {
3741
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3742
-		$targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3743
-		$targetGroup
3744
-			->expects($this->once())
3745
-			->method('getGID')
3746
-			->willReturn('TargetGroup');
3747
-		$this->userManager
3748
-			->expects($this->once())
3749
-			->method('get')
3750
-			->with('RequestedUser')
3751
-			->willReturn($targetUser);
3752
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3753
-			->disableOriginalConstructor()->getMock();
3754
-		$subAdminManager
3755
-			->expects($this->once())
3756
-			->method('getSubAdminsGroups')
3757
-			->with($targetUser)
3758
-			->willReturn([$targetGroup]);
3759
-		$this->groupManager
3760
-			->expects($this->once())
3761
-			->method('getSubAdmin')
3762
-			->willReturn($subAdminManager);
3763
-
3764
-		$this->assertEquals(['TargetGroup'], $this->api->getUserSubAdminGroups('RequestedUser')->getData());
3765
-	}
3766
-
3767
-	public function testEnableUser(): void {
3768
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3769
-		$targetUser->expects($this->once())
3770
-			->method('setEnabled')
3771
-			->with(true);
3772
-		$this->userManager
3773
-			->expects($this->once())
3774
-			->method('get')
3775
-			->with('RequestedUser')
3776
-			->willReturn($targetUser);
3777
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3778
-		$loggedInUser
3779
-			->expects($this->exactly(3))
3780
-			->method('getUID')
3781
-			->willReturn('admin');
3782
-		$this->userSession
3783
-			->expects($this->once())
3784
-			->method('getUser')
3785
-			->willReturn($loggedInUser);
3786
-		$this->groupManager
3787
-			->expects($this->once())
3788
-			->method('isAdmin')
3789
-			->willReturn(true);
3790
-
3791
-		$this->assertEquals([], $this->api->enableUser('RequestedUser')->getData());
3792
-	}
3793
-
3794
-	public function testDisableUser(): void {
3795
-		$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3796
-		$targetUser->expects($this->once())
3797
-			->method('setEnabled')
3798
-			->with(false);
3799
-		$this->userManager
3800
-			->expects($this->once())
3801
-			->method('get')
3802
-			->with('RequestedUser')
3803
-			->willReturn($targetUser);
3804
-		$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3805
-		$loggedInUser
3806
-			->expects($this->exactly(3))
3807
-			->method('getUID')
3808
-			->willReturn('admin');
3809
-		$this->userSession
3810
-			->expects($this->once())
3811
-			->method('getUser')
3812
-			->willReturn($loggedInUser);
3813
-		$this->groupManager
3814
-			->expects($this->once())
3815
-			->method('isAdmin')
3816
-			->willReturn(true);
3817
-
3818
-		$this->assertEquals([], $this->api->disableUser('RequestedUser')->getData());
3819
-	}
3820
-
3821
-	public function testGetCurrentUserLoggedIn(): void {
3822
-		$user = $this->createMock(IUser::class);
3823
-		$user->expects($this->once())->method('getUID')->willReturn('UID');
3824
-
3825
-		$this->userSession->expects($this->once())->method('getUser')
3826
-			->willReturn($user);
3827
-
3828
-		/** @var UsersController | MockObject $api */
3829
-		$api = $this->getMockBuilder(UsersController::class)
3830
-			->setConstructorArgs([
3831
-				'provisioning_api',
3832
-				$this->request,
3833
-				$this->userManager,
3834
-				$this->config,
3835
-				$this->groupManager,
3836
-				$this->userSession,
3837
-				$this->accountManager,
3838
-				$this->subAdminManager,
3839
-				$this->l10nFactory,
3840
-				$this->rootFolder,
3841
-				$this->urlGenerator,
3842
-				$this->logger,
3843
-				$this->newUserMailHelper,
3844
-				$this->secureRandom,
3845
-				$this->remoteWipe,
3846
-				$this->knownUserService,
3847
-				$this->eventDispatcher,
3848
-				$this->phoneNumberUtil,
3849
-				$this->appManager,
3850
-			])
3851
-			->onlyMethods(['getUserData'])
3852
-			->getMock();
3853
-
3854
-		$api->expects($this->once())->method('getUserData')->with('UID', true)
3855
-			->willReturn(
3856
-				[
3857
-					'id' => 'UID',
3858
-					'enabled' => 'true',
3859
-					'quota' => ['DummyValue'],
3860
-					'email' => '[email protected]',
3861
-					'displayname' => 'Demo User',
3862
-					'display-name' => 'Demo User',
3863
-					'phone' => 'phone',
3864
-					'address' => 'address',
3865
-					'website' => 'website',
3866
-					'twitter' => 'twitter',
3867
-					'bluesky' => 'bluesky',
3868
-					'fediverse' => 'fediverse',
3869
-					'organisation' => 'organisation',
3870
-					'role' => 'role',
3871
-					'headline' => 'headline',
3872
-					'biography' => 'biography',
3873
-					'profile_enabled' => '1',
3874
-					'pronouns' => 'they/them',
3875
-				]
3876
-			);
3877
-
3878
-		$expected = [
3879
-			'id' => 'UID',
3880
-			'enabled' => 'true',
3881
-			'quota' => ['DummyValue'],
3882
-			'email' => '[email protected]',
3883
-			'displayname' => 'Demo User',
3884
-			'display-name' => 'Demo User',
3885
-			'phone' => 'phone',
3886
-			'address' => 'address',
3887
-			'website' => 'website',
3888
-			'twitter' => 'twitter',
3889
-			'bluesky' => 'bluesky',
3890
-			'fediverse' => 'fediverse',
3891
-			'organisation' => 'organisation',
3892
-			'role' => 'role',
3893
-			'headline' => 'headline',
3894
-			'biography' => 'biography',
3895
-			'profile_enabled' => '1',
3896
-			'pronouns' => 'they/them',
3897
-		];
3898
-
3899
-		$this->assertSame($expected, $api->getCurrentUser()->getData());
3900
-	}
3901
-
3902
-
3903
-	public function testGetCurrentUserNotLoggedIn(): void {
3904
-		$this->expectException(OCSException::class);
3905
-
3906
-
3907
-		$this->userSession->expects($this->once())->method('getUser')
3908
-			->willReturn(null);
3909
-
3910
-		$this->api->getCurrentUser();
3911
-	}
3912
-
3913
-	public function testGetUser(): void {
3914
-		$loggedInUser = $this->createMock(IUser::class);
3915
-		$loggedInUser
3916
-			->method('getUID')
3917
-			->willReturn('currentuser');
3918
-		$this->userSession
3919
-			->method('getUser')
3920
-			->willReturn($loggedInUser);
3921
-
3922
-		/** @var UsersController | MockObject $api */
3923
-		$api = $this->getMockBuilder(UsersController::class)
3924
-			->setConstructorArgs([
3925
-				'provisioning_api',
3926
-				$this->request,
3927
-				$this->userManager,
3928
-				$this->config,
3929
-				$this->groupManager,
3930
-				$this->userSession,
3931
-				$this->accountManager,
3932
-				$this->subAdminManager,
3933
-				$this->l10nFactory,
3934
-				$this->rootFolder,
3935
-				$this->urlGenerator,
3936
-				$this->logger,
3937
-				$this->newUserMailHelper,
3938
-				$this->secureRandom,
3939
-				$this->remoteWipe,
3940
-				$this->knownUserService,
3941
-				$this->eventDispatcher,
3942
-				$this->phoneNumberUtil,
3943
-				$this->appManager,
3944
-			])
3945
-			->onlyMethods(['getUserData'])
3946
-			->getMock();
3947
-
3948
-		$expected = [
3949
-			'id' => 'UID',
3950
-			'enabled' => 'true',
3951
-			'quota' => ['DummyValue'],
3952
-			'email' => '[email protected]',
3953
-			'phone' => 'phone',
3954
-			'address' => 'address',
3955
-			'website' => 'website',
3956
-			'twitter' => 'twitter',
3957
-			'bluesky' => 'bluesky',
3958
-			'fediverse' => 'fediverse',
3959
-			'displayname' => 'Demo User',
3960
-			'display-name' => 'Demo User',
3961
-			'organisation' => 'organisation',
3962
-			'role' => 'role',
3963
-			'headline' => 'headline',
3964
-			'biography' => 'biography',
3965
-			'profile_enabled' => '1',
3966
-			'pronouns' => 'they/them',
3967
-		];
3968
-
3969
-		$api->expects($this->exactly(2))
3970
-			->method('getUserData')
3971
-			->willReturnMap([
3972
-				['uid', false, $expected],
3973
-				['currentuser', true, $expected],
3974
-			]);
3975
-
3976
-		$this->assertSame($expected, $api->getUser('uid')->getData());
3977
-
3978
-		$this->assertSame($expected, $api->getUser('currentuser')->getData());
3979
-	}
3980
-
3981
-
3982
-	public function testResendWelcomeMessageWithNotExistingTargetUser(): void {
3983
-		$this->expectException(OCSException::class);
3984
-		$this->expectExceptionCode(998);
3985
-
3986
-		$this->userManager
3987
-			->expects($this->once())
3988
-			->method('get')
3989
-			->with('NotExistingUser')
3990
-			->willReturn(null);
3991
-
3992
-		$this->api->resendWelcomeMessage('NotExistingUser');
3993
-	}
3994
-
3995
-
3996
-	public function testResendWelcomeMessageAsSubAdminAndUserIsNotAccessible(): void {
3997
-		$this->expectException(OCSException::class);
3998
-		$this->expectExceptionCode(998);
3999
-
4000
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4001
-			->disableOriginalConstructor()
4002
-			->getMock();
4003
-		$loggedInUser
4004
-			->expects($this->exactly(2))
4005
-			->method('getUID')
4006
-			->willReturn('subadmin');
4007
-		$targetUser = $this->getMockBuilder(IUser::class)
4008
-			->disableOriginalConstructor()
4009
-			->getMock();
4010
-		$this->userSession
4011
-			->expects($this->once())
4012
-			->method('getUser')
4013
-			->willReturn($loggedInUser);
4014
-		$this->userManager
4015
-			->expects($this->once())
4016
-			->method('get')
4017
-			->with('UserToGet')
4018
-			->willReturn($targetUser);
4019
-		$this->groupManager
4020
-			->expects($this->once())
4021
-			->method('isAdmin')
4022
-			->with('subadmin')
4023
-			->willReturn(false);
4024
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4025
-			->disableOriginalConstructor()
4026
-			->getMock();
4027
-		$subAdminManager
4028
-			->expects($this->once())
4029
-			->method('isUserAccessible')
4030
-			->with($loggedInUser, $targetUser)
4031
-			->willReturn(false);
4032
-		$this->groupManager
4033
-			->expects($this->once())
4034
-			->method('getSubAdmin')
4035
-			->willReturn($subAdminManager);
4036
-
4037
-		$this->api->resendWelcomeMessage('UserToGet');
4038
-	}
4039
-
4040
-
4041
-	public function testResendWelcomeMessageNoEmail(): void {
4042
-		$this->expectException(OCSException::class);
4043
-		$this->expectExceptionMessage('Email address not available');
4044
-		$this->expectExceptionCode(101);
4045
-
4046
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4047
-			->disableOriginalConstructor()
4048
-			->getMock();
4049
-		$targetUser = $this->getMockBuilder(IUser::class)
4050
-			->disableOriginalConstructor()
4051
-			->getMock();
4052
-		$this->userSession
4053
-			->expects($this->once())
4054
-			->method('getUser')
4055
-			->willReturn($loggedInUser);
4056
-		$this->userManager
4057
-			->expects($this->once())
4058
-			->method('get')
4059
-			->with('UserToGet')
4060
-			->willReturn($targetUser);
4061
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4062
-			->disableOriginalConstructor()
4063
-			->getMock();
4064
-		$subAdminManager
4065
-			->expects($this->once())
4066
-			->method('isUserAccessible')
4067
-			->with($loggedInUser, $targetUser)
4068
-			->willReturn(true);
4069
-		$this->groupManager
4070
-			->expects($this->once())
4071
-			->method('getSubAdmin')
4072
-			->willReturn($subAdminManager);
4073
-		$loggedInUser
4074
-			->expects($this->exactly(2))
4075
-			->method('getUID')
4076
-			->willReturn('logged-user-id');
4077
-		$targetUser
4078
-			->expects($this->once())
4079
-			->method('getEmailAddress')
4080
-			->willReturn('');
4081
-
4082
-		$this->api->resendWelcomeMessage('UserToGet');
4083
-	}
4084
-
4085
-
4086
-	public function testResendWelcomeMessageNullEmail(): void {
4087
-		$this->expectException(OCSException::class);
4088
-		$this->expectExceptionMessage('Email address not available');
4089
-		$this->expectExceptionCode(101);
4090
-
4091
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4092
-			->disableOriginalConstructor()
4093
-			->getMock();
4094
-		$targetUser = $this->getMockBuilder(IUser::class)
4095
-			->disableOriginalConstructor()
4096
-			->getMock();
4097
-		$this->userSession
4098
-			->expects($this->once())
4099
-			->method('getUser')
4100
-			->willReturn($loggedInUser);
4101
-		$this->userManager
4102
-			->expects($this->once())
4103
-			->method('get')
4104
-			->with('UserToGet')
4105
-			->willReturn($targetUser);
4106
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4107
-			->disableOriginalConstructor()
4108
-			->getMock();
4109
-		$subAdminManager
4110
-			->expects($this->once())
4111
-			->method('isUserAccessible')
4112
-			->with($loggedInUser, $targetUser)
4113
-			->willReturn(true);
4114
-		$this->groupManager
4115
-			->expects($this->once())
4116
-			->method('getSubAdmin')
4117
-			->willReturn($subAdminManager);
4118
-		$loggedInUser
4119
-			->expects($this->exactly(2))
4120
-			->method('getUID')
4121
-			->willReturn('logged-user-id');
4122
-		$targetUser
4123
-			->expects($this->once())
4124
-			->method('getEmailAddress')
4125
-			->willReturn(null);
4126
-
4127
-		$this->api->resendWelcomeMessage('UserToGet');
4128
-	}
4129
-
4130
-	public function testResendWelcomeMessageSuccess(): void {
4131
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4132
-			->disableOriginalConstructor()
4133
-			->getMock();
4134
-		$targetUser = $this->getMockBuilder(IUser::class)
4135
-			->disableOriginalConstructor()
4136
-			->getMock();
4137
-		$loggedInUser
4138
-			->method('getUID')
4139
-			->willReturn('logged-user-id');
4140
-		$targetUser
4141
-			->method('getUID')
4142
-			->willReturn('user-id');
4143
-		$this->userSession
4144
-			->expects($this->once())
4145
-			->method('getUser')
4146
-			->willReturn($loggedInUser);
4147
-		$this->userManager
4148
-			->expects($this->once())
4149
-			->method('get')
4150
-			->with('UserToGet')
4151
-			->willReturn($targetUser);
4152
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4153
-			->disableOriginalConstructor()
4154
-			->getMock();
4155
-		$subAdminManager
4156
-			->expects($this->once())
4157
-			->method('isUserAccessible')
4158
-			->with($loggedInUser, $targetUser)
4159
-			->willReturn(true);
4160
-		$this->groupManager
4161
-			->expects($this->once())
4162
-			->method('getSubAdmin')
4163
-			->willReturn($subAdminManager);
4164
-		$targetUser
4165
-			->expects($this->once())
4166
-			->method('getEmailAddress')
4167
-			->willReturn('[email protected]');
4168
-		$emailTemplate = $this->createMock(IEMailTemplate::class);
4169
-		$this->newUserMailHelper
4170
-			->expects($this->once())
4171
-			->method('generateTemplate')
4172
-			->willReturn($emailTemplate);
4173
-		$this->newUserMailHelper
4174
-			->expects($this->once())
4175
-			->method('sendMail')
4176
-			->with($targetUser, $emailTemplate);
4177
-
4178
-		$this->api->resendWelcomeMessage('UserToGet');
4179
-	}
4180
-
4181
-	public function testResendWelcomeMessageSuccessWithFallbackLanguage(): void {
4182
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4183
-			->disableOriginalConstructor()
4184
-			->getMock();
4185
-		$targetUser = $this->getMockBuilder(IUser::class)
4186
-			->disableOriginalConstructor()
4187
-			->getMock();
4188
-		$loggedInUser
4189
-			->method('getUID')
4190
-			->willReturn('logged-user-id');
4191
-		$targetUser
4192
-			->method('getUID')
4193
-			->willReturn('user-id');
4194
-		$this->userSession
4195
-			->expects($this->once())
4196
-			->method('getUser')
4197
-			->willReturn($loggedInUser);
4198
-		$this->userManager
4199
-			->expects($this->once())
4200
-			->method('get')
4201
-			->with('UserToGet')
4202
-			->willReturn($targetUser);
4203
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4204
-			->disableOriginalConstructor()
4205
-			->getMock();
4206
-		$subAdminManager
4207
-			->expects($this->once())
4208
-			->method('isUserAccessible')
4209
-			->with($loggedInUser, $targetUser)
4210
-			->willReturn(true);
4211
-		$this->groupManager
4212
-			->expects($this->once())
4213
-			->method('getSubAdmin')
4214
-			->willReturn($subAdminManager);
4215
-		$targetUser
4216
-			->expects($this->once())
4217
-			->method('getEmailAddress')
4218
-			->willReturn('[email protected]');
4219
-		$emailTemplate = $this->createMock(IEMailTemplate::class);
4220
-		$this->newUserMailHelper
4221
-			->expects($this->once())
4222
-			->method('generateTemplate')
4223
-			->willReturn($emailTemplate);
4224
-		$this->newUserMailHelper
4225
-			->expects($this->once())
4226
-			->method('sendMail')
4227
-			->with($targetUser, $emailTemplate);
4228
-
4229
-		$this->api->resendWelcomeMessage('UserToGet');
4230
-	}
4231
-
4232
-
4233
-	public function testResendWelcomeMessageFailed(): void {
4234
-		$this->expectException(OCSException::class);
4235
-		$this->expectExceptionMessage('Sending email failed');
4236
-		$this->expectExceptionCode(102);
4237
-
4238
-		$loggedInUser = $this->getMockBuilder(IUser::class)
4239
-			->disableOriginalConstructor()
4240
-			->getMock();
4241
-		$targetUser = $this->getMockBuilder(IUser::class)
4242
-			->disableOriginalConstructor()
4243
-			->getMock();
4244
-		$loggedInUser
4245
-			->expects($this->exactly(2))
4246
-			->method('getUID')
4247
-			->willReturn('logged-user-id');
4248
-		$targetUser
4249
-			->method('getUID')
4250
-			->willReturn('user-id');
4251
-		$this->userSession
4252
-			->expects($this->once())
4253
-			->method('getUser')
4254
-			->willReturn($loggedInUser);
4255
-		$this->userManager
4256
-			->expects($this->once())
4257
-			->method('get')
4258
-			->with('UserToGet')
4259
-			->willReturn($targetUser);
4260
-		$subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4261
-			->disableOriginalConstructor()
4262
-			->getMock();
4263
-		$subAdminManager
4264
-			->expects($this->once())
4265
-			->method('isUserAccessible')
4266
-			->with($loggedInUser, $targetUser)
4267
-			->willReturn(true);
4268
-		$this->groupManager
4269
-			->expects($this->once())
4270
-			->method('getSubAdmin')
4271
-			->willReturn($subAdminManager);
4272
-		$targetUser
4273
-			->expects($this->once())
4274
-			->method('getEmailAddress')
4275
-			->willReturn('[email protected]');
4276
-		$emailTemplate = $this->createMock(IEMailTemplate::class);
4277
-		$this->newUserMailHelper
4278
-			->expects($this->once())
4279
-			->method('generateTemplate')
4280
-			->willReturn($emailTemplate);
4281
-		$this->newUserMailHelper
4282
-			->expects($this->once())
4283
-			->method('sendMail')
4284
-			->with($targetUser, $emailTemplate)
4285
-			->willThrowException(new \Exception());
4286
-
4287
-		$this->api->resendWelcomeMessage('UserToGet');
4288
-	}
4289
-
4290
-
4291
-	public static function dataGetEditableFields(): array {
4292
-		return [
4293
-			[false, true, ISetDisplayNameBackend::class, [
4294
-				IAccountManager::PROPERTY_EMAIL,
4295
-				IAccountManager::COLLECTION_EMAIL,
4296
-				IAccountManager::PROPERTY_PHONE,
4297
-				IAccountManager::PROPERTY_ADDRESS,
4298
-				IAccountManager::PROPERTY_WEBSITE,
4299
-				IAccountManager::PROPERTY_TWITTER,
4300
-				IAccountManager::PROPERTY_BLUESKY,
4301
-				IAccountManager::PROPERTY_FEDIVERSE,
4302
-				IAccountManager::PROPERTY_ORGANISATION,
4303
-				IAccountManager::PROPERTY_ROLE,
4304
-				IAccountManager::PROPERTY_HEADLINE,
4305
-				IAccountManager::PROPERTY_BIOGRAPHY,
4306
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4307
-				IAccountManager::PROPERTY_PRONOUNS,
4308
-			]],
4309
-			[true, false, ISetDisplayNameBackend::class, [
4310
-				IAccountManager::PROPERTY_DISPLAYNAME,
4311
-				IAccountManager::COLLECTION_EMAIL,
4312
-				IAccountManager::PROPERTY_PHONE,
4313
-				IAccountManager::PROPERTY_ADDRESS,
4314
-				IAccountManager::PROPERTY_WEBSITE,
4315
-				IAccountManager::PROPERTY_TWITTER,
4316
-				IAccountManager::PROPERTY_BLUESKY,
4317
-				IAccountManager::PROPERTY_FEDIVERSE,
4318
-				IAccountManager::PROPERTY_ORGANISATION,
4319
-				IAccountManager::PROPERTY_ROLE,
4320
-				IAccountManager::PROPERTY_HEADLINE,
4321
-				IAccountManager::PROPERTY_BIOGRAPHY,
4322
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4323
-				IAccountManager::PROPERTY_PRONOUNS,
4324
-			]],
4325
-			[true, true, ISetDisplayNameBackend::class, [
4326
-				IAccountManager::PROPERTY_DISPLAYNAME,
4327
-				IAccountManager::PROPERTY_EMAIL,
4328
-				IAccountManager::COLLECTION_EMAIL,
4329
-				IAccountManager::PROPERTY_PHONE,
4330
-				IAccountManager::PROPERTY_ADDRESS,
4331
-				IAccountManager::PROPERTY_WEBSITE,
4332
-				IAccountManager::PROPERTY_TWITTER,
4333
-				IAccountManager::PROPERTY_BLUESKY,
4334
-				IAccountManager::PROPERTY_FEDIVERSE,
4335
-				IAccountManager::PROPERTY_ORGANISATION,
4336
-				IAccountManager::PROPERTY_ROLE,
4337
-				IAccountManager::PROPERTY_HEADLINE,
4338
-				IAccountManager::PROPERTY_BIOGRAPHY,
4339
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4340
-				IAccountManager::PROPERTY_PRONOUNS,
4341
-			]],
4342
-			[false, false, ISetDisplayNameBackend::class, [
4343
-				IAccountManager::COLLECTION_EMAIL,
4344
-				IAccountManager::PROPERTY_PHONE,
4345
-				IAccountManager::PROPERTY_ADDRESS,
4346
-				IAccountManager::PROPERTY_WEBSITE,
4347
-				IAccountManager::PROPERTY_TWITTER,
4348
-				IAccountManager::PROPERTY_BLUESKY,
4349
-				IAccountManager::PROPERTY_FEDIVERSE,
4350
-				IAccountManager::PROPERTY_ORGANISATION,
4351
-				IAccountManager::PROPERTY_ROLE,
4352
-				IAccountManager::PROPERTY_HEADLINE,
4353
-				IAccountManager::PROPERTY_BIOGRAPHY,
4354
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4355
-				IAccountManager::PROPERTY_PRONOUNS,
4356
-			]],
4357
-			[false, true, UserInterface::class, [
4358
-				IAccountManager::PROPERTY_EMAIL,
4359
-				IAccountManager::COLLECTION_EMAIL,
4360
-				IAccountManager::PROPERTY_PHONE,
4361
-				IAccountManager::PROPERTY_ADDRESS,
4362
-				IAccountManager::PROPERTY_WEBSITE,
4363
-				IAccountManager::PROPERTY_TWITTER,
4364
-				IAccountManager::PROPERTY_BLUESKY,
4365
-				IAccountManager::PROPERTY_FEDIVERSE,
4366
-				IAccountManager::PROPERTY_ORGANISATION,
4367
-				IAccountManager::PROPERTY_ROLE,
4368
-				IAccountManager::PROPERTY_HEADLINE,
4369
-				IAccountManager::PROPERTY_BIOGRAPHY,
4370
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4371
-				IAccountManager::PROPERTY_PRONOUNS,
4372
-			]],
4373
-			[true, false, UserInterface::class, [
4374
-				IAccountManager::COLLECTION_EMAIL,
4375
-				IAccountManager::PROPERTY_PHONE,
4376
-				IAccountManager::PROPERTY_ADDRESS,
4377
-				IAccountManager::PROPERTY_WEBSITE,
4378
-				IAccountManager::PROPERTY_TWITTER,
4379
-				IAccountManager::PROPERTY_BLUESKY,
4380
-				IAccountManager::PROPERTY_FEDIVERSE,
4381
-				IAccountManager::PROPERTY_ORGANISATION,
4382
-				IAccountManager::PROPERTY_ROLE,
4383
-				IAccountManager::PROPERTY_HEADLINE,
4384
-				IAccountManager::PROPERTY_BIOGRAPHY,
4385
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4386
-				IAccountManager::PROPERTY_PRONOUNS,
4387
-			]],
4388
-			[true, true, UserInterface::class, [
4389
-				IAccountManager::PROPERTY_EMAIL,
4390
-				IAccountManager::COLLECTION_EMAIL,
4391
-				IAccountManager::PROPERTY_PHONE,
4392
-				IAccountManager::PROPERTY_ADDRESS,
4393
-				IAccountManager::PROPERTY_WEBSITE,
4394
-				IAccountManager::PROPERTY_TWITTER,
4395
-				IAccountManager::PROPERTY_BLUESKY,
4396
-				IAccountManager::PROPERTY_FEDIVERSE,
4397
-				IAccountManager::PROPERTY_ORGANISATION,
4398
-				IAccountManager::PROPERTY_ROLE,
4399
-				IAccountManager::PROPERTY_HEADLINE,
4400
-				IAccountManager::PROPERTY_BIOGRAPHY,
4401
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4402
-				IAccountManager::PROPERTY_PRONOUNS,
4403
-			]],
4404
-			[false, false, UserInterface::class, [
4405
-				IAccountManager::COLLECTION_EMAIL,
4406
-				IAccountManager::PROPERTY_PHONE,
4407
-				IAccountManager::PROPERTY_ADDRESS,
4408
-				IAccountManager::PROPERTY_WEBSITE,
4409
-				IAccountManager::PROPERTY_TWITTER,
4410
-				IAccountManager::PROPERTY_BLUESKY,
4411
-				IAccountManager::PROPERTY_FEDIVERSE,
4412
-				IAccountManager::PROPERTY_ORGANISATION,
4413
-				IAccountManager::PROPERTY_ROLE,
4414
-				IAccountManager::PROPERTY_HEADLINE,
4415
-				IAccountManager::PROPERTY_BIOGRAPHY,
4416
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
4417
-				IAccountManager::PROPERTY_PRONOUNS,
4418
-			]],
4419
-		];
4420
-	}
4421
-
4422
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataGetEditableFields')]
4423
-	public function testGetEditableFields(bool $allowedToChangeDisplayName, bool $allowedToChangeEmail, string $userBackend, array $expected): void {
4424
-		$this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => match ($key) {
4425
-			'allow_user_to_change_display_name' => $allowedToChangeDisplayName,
4426
-			'allow_user_to_change_email' => $allowedToChangeEmail,
4427
-			default => throw new RuntimeException('Unexpected system config key: ' . $key),
4428
-		});
4429
-
4430
-		$user = $this->createMock(IUser::class);
4431
-		$this->userSession->method('getUser')
4432
-			->willReturn($user);
4433
-
4434
-		$backend = $this->createMock($userBackend);
4435
-
4436
-		$user->method('getUID')
4437
-			->willReturn('userId');
4438
-		$user->method('getBackend')
4439
-			->willReturn($backend);
4440
-
4441
-		$expectedResp = new DataResponse($expected);
4442
-		$this->assertEquals($expectedResp, $this->api->getEditableFields('userId'));
4443
-	}
4444
-
4445
-	private function mockAccount($targetUser, $accountProperties) {
4446
-		$mockedProperties = [];
4447
-
4448
-		foreach ($accountProperties as $propertyName => $data) {
4449
-			$mockedProperty = $this->createMock(IAccountProperty::class);
4450
-			$mockedProperty->method('getValue')->willReturn($data['value'] ?? '');
4451
-			$mockedProperty->method('getScope')->willReturn($data['scope'] ?? '');
4452
-			$mockedProperties[] = [$propertyName, $mockedProperty];
4453
-		}
4454
-
4455
-		$account = $this->createMock(IAccount::class);
4456
-		$account->method('getProperty')
4457
-			->willReturnMap($mockedProperties);
4458
-
4459
-		$this->accountManager->expects($this->any())->method('getAccount')
4460
-			->with($targetUser)
4461
-			->willReturn($account);
4462
-	}
3110
+
3111
+        $subAdminManager = $this->createMock(SubAdmin::class);
3112
+        $subAdminManager->expects($this->once())
3113
+            ->method('isSubAdminOfGroup')
3114
+            ->with($loggedInUser, $targetGroup)
3115
+            ->willReturn(true);
3116
+
3117
+        $this->groupManager->expects($this->once())
3118
+            ->method('getSubAdmin')
3119
+            ->willReturn($subAdminManager);
3120
+        $this->groupManager->expects($this->once())
3121
+            ->method('isAdmin')
3122
+            ->with('subadmin')
3123
+            ->willReturn(false);
3124
+
3125
+        $this->userManager->expects($this->once())
3126
+            ->method('get')
3127
+            ->with('TargetUser')
3128
+            ->willReturn($targetUser);
3129
+
3130
+        $this->userSession->expects($this->once())
3131
+            ->method('getUser')
3132
+            ->willReturn($loggedInUser);
3133
+
3134
+        $this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo'));
3135
+    }
3136
+
3137
+    public function testAddToGroupSuccessAsAdmin(): void {
3138
+        $targetUser = $this->createMock(IUser::class);
3139
+        $loggedInUser = $this->createMock(IUser::class);
3140
+        $loggedInUser->expects($this->exactly(2))
3141
+            ->method('getUID')
3142
+            ->willReturn('admin');
3143
+
3144
+        $targetGroup = $this->createMock(IGroup::class);
3145
+        $targetGroup->expects($this->once())
3146
+            ->method('addUser')
3147
+            ->with($targetUser);
3148
+
3149
+        $this->groupManager->expects($this->once())
3150
+            ->method('get')
3151
+            ->with('GroupToAddTo')
3152
+            ->willReturn($targetGroup);
3153
+
3154
+
3155
+        $subAdminManager = $this->createMock(SubAdmin::class);
3156
+        $subAdminManager->expects($this->never())
3157
+            ->method('isSubAdminOfGroup');
3158
+
3159
+        $this->groupManager->expects($this->once())
3160
+            ->method('getSubAdmin')
3161
+            ->willReturn($subAdminManager);
3162
+        $this->groupManager->expects($this->once())
3163
+            ->method('isAdmin')
3164
+            ->with('admin')
3165
+            ->willReturn(true);
3166
+
3167
+        $this->userManager->expects($this->once())
3168
+            ->method('get')
3169
+            ->with('TargetUser')
3170
+            ->willReturn($targetUser);
3171
+
3172
+        $this->userSession->expects($this->once())
3173
+            ->method('getUser')
3174
+            ->willReturn($loggedInUser);
3175
+
3176
+        $this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo'));
3177
+    }
3178
+
3179
+
3180
+    public function testRemoveFromGroupWithNoTargetGroup(): void {
3181
+        $this->expectException(OCSException::class);
3182
+        $this->expectExceptionCode(101);
3183
+
3184
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3185
+        $this->userSession
3186
+            ->expects($this->once())
3187
+            ->method('getUser')
3188
+            ->willReturn($loggedInUser);
3189
+
3190
+        $this->api->removeFromGroup('TargetUser', '');
3191
+    }
3192
+
3193
+
3194
+    public function testRemoveFromGroupWithEmptyTargetGroup(): void {
3195
+        $this->expectException(OCSException::class);
3196
+        $this->expectExceptionCode(101);
3197
+
3198
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3199
+        $this->userSession
3200
+            ->expects($this->once())
3201
+            ->method('getUser')
3202
+            ->willReturn($loggedInUser);
3203
+
3204
+        $this->api->removeFromGroup('TargetUser', '');
3205
+    }
3206
+
3207
+
3208
+    public function testRemoveFromGroupWithNotExistingTargetGroup(): void {
3209
+        $this->expectException(OCSException::class);
3210
+        $this->expectExceptionCode(102);
3211
+
3212
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3213
+        $this->userSession
3214
+            ->expects($this->once())
3215
+            ->method('getUser')
3216
+            ->willReturn($loggedInUser);
3217
+        $this->groupManager
3218
+            ->expects($this->once())
3219
+            ->method('get')
3220
+            ->with('TargetGroup')
3221
+            ->willReturn(null);
3222
+
3223
+        $this->api->removeFromGroup('TargetUser', 'TargetGroup');
3224
+    }
3225
+
3226
+
3227
+    public function testRemoveFromGroupWithNotExistingTargetUser(): void {
3228
+        $this->expectException(OCSException::class);
3229
+        $this->expectExceptionCode(103);
3230
+
3231
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3232
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3233
+        $this->userSession
3234
+            ->expects($this->once())
3235
+            ->method('getUser')
3236
+            ->willReturn($loggedInUser);
3237
+        $this->groupManager
3238
+            ->expects($this->once())
3239
+            ->method('get')
3240
+            ->with('TargetGroup')
3241
+            ->willReturn($targetGroup);
3242
+        $this->userManager
3243
+            ->expects($this->once())
3244
+            ->method('get')
3245
+            ->with('TargetUser')
3246
+            ->willReturn(null);
3247
+
3248
+        $this->api->removeFromGroup('TargetUser', 'TargetGroup');
3249
+    }
3250
+
3251
+
3252
+    public function testRemoveFromGroupWithoutPermission(): void {
3253
+        $this->expectException(OCSException::class);
3254
+        $this->expectExceptionCode(104);
3255
+
3256
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3257
+        $loggedInUser
3258
+            ->expects($this->exactly(2))
3259
+            ->method('getUID')
3260
+            ->willReturn('unauthorizedUser');
3261
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3262
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3263
+        $this->userSession
3264
+            ->expects($this->once())
3265
+            ->method('getUser')
3266
+            ->willReturn($loggedInUser);
3267
+        $this->groupManager
3268
+            ->expects($this->once())
3269
+            ->method('get')
3270
+            ->with('TargetGroup')
3271
+            ->willReturn($targetGroup);
3272
+        $this->userManager
3273
+            ->expects($this->once())
3274
+            ->method('get')
3275
+            ->with('TargetUser')
3276
+            ->willReturn($targetUser);
3277
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3278
+            ->disableOriginalConstructor()->getMock();
3279
+        $this->groupManager
3280
+            ->expects($this->once())
3281
+            ->method('getSubAdmin')
3282
+            ->willReturn($subAdminManager);
3283
+        $this->groupManager
3284
+            ->expects($this->once())
3285
+            ->method('isAdmin')
3286
+            ->with('unauthorizedUser')
3287
+            ->willReturn(false);
3288
+
3289
+        $this->api->removeFromGroup('TargetUser', 'TargetGroup');
3290
+    }
3291
+
3292
+
3293
+    public function testRemoveFromGroupAsAdminFromAdmin(): void {
3294
+        $this->expectException(OCSException::class);
3295
+        $this->expectExceptionMessage('Cannot remove yourself from the admin group');
3296
+        $this->expectExceptionCode(105);
3297
+
3298
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3299
+        $loggedInUser
3300
+            ->expects($this->any())
3301
+            ->method('getUID')
3302
+            ->willReturn('admin');
3303
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3304
+        $targetUser
3305
+            ->expects($this->once())
3306
+            ->method('getUID')
3307
+            ->willReturn('admin');
3308
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3309
+        $targetGroup
3310
+            ->expects($this->once())
3311
+            ->method('getGID')
3312
+            ->willReturn('admin');
3313
+        $this->userSession
3314
+            ->expects($this->once())
3315
+            ->method('getUser')
3316
+            ->willReturn($loggedInUser);
3317
+        $this->groupManager
3318
+            ->expects($this->once())
3319
+            ->method('get')
3320
+            ->with('admin')
3321
+            ->willReturn($targetGroup);
3322
+        $this->userManager
3323
+            ->expects($this->once())
3324
+            ->method('get')
3325
+            ->with('Admin')
3326
+            ->willReturn($targetUser);
3327
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3328
+            ->disableOriginalConstructor()->getMock();
3329
+        $this->groupManager
3330
+            ->expects($this->once())
3331
+            ->method('getSubAdmin')
3332
+            ->willReturn($subAdminManager);
3333
+        $this->groupManager
3334
+            ->expects($this->any())
3335
+            ->method('isAdmin')
3336
+            ->with('admin')
3337
+            ->willReturn(true);
3338
+
3339
+        $this->api->removeFromGroup('Admin', 'admin');
3340
+    }
3341
+
3342
+
3343
+    public function testRemoveFromGroupAsSubAdminFromSubAdmin(): void {
3344
+        $this->expectException(OCSException::class);
3345
+        $this->expectExceptionMessage('Cannot remove yourself from this group as you are a sub-admin');
3346
+        $this->expectExceptionCode(105);
3347
+
3348
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3349
+        $loggedInUser
3350
+            ->expects($this->any())
3351
+            ->method('getUID')
3352
+            ->willReturn('subadmin');
3353
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3354
+        $targetUser
3355
+            ->expects($this->once())
3356
+            ->method('getUID')
3357
+            ->willReturn('subadmin');
3358
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3359
+        $targetGroup
3360
+            ->expects($this->any())
3361
+            ->method('getGID')
3362
+            ->willReturn('subadmin');
3363
+        $this->userSession
3364
+            ->expects($this->once())
3365
+            ->method('getUser')
3366
+            ->willReturn($loggedInUser);
3367
+        $this->groupManager
3368
+            ->expects($this->once())
3369
+            ->method('get')
3370
+            ->with('subadmin')
3371
+            ->willReturn($targetGroup);
3372
+        $this->userManager
3373
+            ->expects($this->once())
3374
+            ->method('get')
3375
+            ->with('SubAdmin')
3376
+            ->willReturn($targetUser);
3377
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3378
+            ->disableOriginalConstructor()->getMock();
3379
+        $subAdminManager
3380
+            ->expects($this->once())
3381
+            ->method('isSubAdminOfGroup')
3382
+            ->with($loggedInUser, $targetGroup)
3383
+            ->willReturn(true);
3384
+        $this->groupManager
3385
+            ->expects($this->once())
3386
+            ->method('getSubAdmin')
3387
+            ->willReturn($subAdminManager);
3388
+        $this->groupManager
3389
+            ->expects($this->any())
3390
+            ->method('isAdmin')
3391
+            ->with('subadmin')
3392
+            ->willReturn(false);
3393
+
3394
+        $this->api->removeFromGroup('SubAdmin', 'subadmin');
3395
+    }
3396
+
3397
+
3398
+    public function testRemoveFromGroupAsSubAdminFromLastSubAdminGroup(): void {
3399
+        $this->expectException(OCSException::class);
3400
+        $this->expectExceptionMessage('Not viable to remove user from the last group you are sub-admin of');
3401
+        $this->expectExceptionCode(105);
3402
+
3403
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3404
+        $loggedInUser
3405
+            ->expects($this->any())
3406
+            ->method('getUID')
3407
+            ->willReturn('subadmin');
3408
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3409
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3410
+        $targetGroup
3411
+            ->expects($this->any())
3412
+            ->method('getGID')
3413
+            ->willReturn('subadmin');
3414
+        $this->userSession
3415
+            ->expects($this->once())
3416
+            ->method('getUser')
3417
+            ->willReturn($loggedInUser);
3418
+        $this->groupManager
3419
+            ->expects($this->once())
3420
+            ->method('get')
3421
+            ->with('subadmin')
3422
+            ->willReturn($targetGroup);
3423
+        $this->userManager
3424
+            ->expects($this->once())
3425
+            ->method('get')
3426
+            ->with('AnotherUser')
3427
+            ->willReturn($targetUser);
3428
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3429
+            ->disableOriginalConstructor()->getMock();
3430
+        $subAdminManager
3431
+            ->expects($this->once())
3432
+            ->method('isSubAdminOfGroup')
3433
+            ->with($loggedInUser, $targetGroup)
3434
+            ->willReturn(true);
3435
+        $this->groupManager
3436
+            ->expects($this->once())
3437
+            ->method('getSubAdmin')
3438
+            ->willReturn($subAdminManager);
3439
+        $subAdminManager
3440
+            ->expects($this->once())
3441
+            ->method('getSubAdminsGroups')
3442
+            ->with($loggedInUser)
3443
+            ->willReturn([$targetGroup]);
3444
+
3445
+        $this->groupManager
3446
+            ->expects($this->any())
3447
+            ->method('isAdmin')
3448
+            ->with('subadmin')
3449
+            ->willReturn(false);
3450
+        $this->groupManager
3451
+            ->expects($this->once())
3452
+            ->method('getUserGroupIds')
3453
+            ->with($targetUser)
3454
+            ->willReturn(['subadmin', 'other group']);
3455
+
3456
+        $this->api->removeFromGroup('AnotherUser', 'subadmin');
3457
+    }
3458
+
3459
+    public function testRemoveFromGroupSuccessful(): void {
3460
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3461
+        $loggedInUser
3462
+            ->expects($this->any())
3463
+            ->method('getUID')
3464
+            ->willReturn('admin');
3465
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3466
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3467
+        $this->userSession
3468
+            ->expects($this->once())
3469
+            ->method('getUser')
3470
+            ->willReturn($loggedInUser);
3471
+        $this->groupManager
3472
+            ->expects($this->once())
3473
+            ->method('get')
3474
+            ->with('admin')
3475
+            ->willReturn($targetGroup);
3476
+        $this->userManager
3477
+            ->expects($this->once())
3478
+            ->method('get')
3479
+            ->with('AnotherUser')
3480
+            ->willReturn($targetUser);
3481
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3482
+            ->disableOriginalConstructor()->getMock();
3483
+        $this->groupManager
3484
+            ->expects($this->once())
3485
+            ->method('getSubAdmin')
3486
+            ->willReturn($subAdminManager);
3487
+        $this->groupManager
3488
+            ->expects($this->any())
3489
+            ->method('isAdmin')
3490
+            ->with('admin')
3491
+            ->willReturn(true);
3492
+        $targetGroup
3493
+            ->expects($this->once())
3494
+            ->method('removeUser')
3495
+            ->with($targetUser);
3496
+
3497
+        $this->assertEquals([], $this->api->removeFromGroup('AnotherUser', 'admin')->getData());
3498
+    }
3499
+
3500
+
3501
+    public function testAddSubAdminWithNotExistingTargetUser(): void {
3502
+        $this->expectException(OCSException::class);
3503
+        $this->expectExceptionMessage('User does not exist');
3504
+        $this->expectExceptionCode(101);
3505
+
3506
+        $this->userManager
3507
+            ->expects($this->once())
3508
+            ->method('get')
3509
+            ->with('NotExistingUser')
3510
+            ->willReturn(null);
3511
+
3512
+        $this->api->addSubAdmin('NotExistingUser', '');
3513
+    }
3514
+
3515
+
3516
+    public function testAddSubAdminWithNotExistingTargetGroup(): void {
3517
+        $this->expectException(OCSException::class);
3518
+        $this->expectExceptionMessage('Group does not exist');
3519
+        $this->expectExceptionCode(102);
3520
+
3521
+
3522
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3523
+        $this->userManager
3524
+            ->expects($this->once())
3525
+            ->method('get')
3526
+            ->with('ExistingUser')
3527
+            ->willReturn($targetUser);
3528
+        $this->groupManager
3529
+            ->expects($this->once())
3530
+            ->method('get')
3531
+            ->with('NotExistingGroup')
3532
+            ->willReturn(null);
3533
+
3534
+        $this->api->addSubAdmin('ExistingUser', 'NotExistingGroup');
3535
+    }
3536
+
3537
+
3538
+    public function testAddSubAdminToAdminGroup(): void {
3539
+        $this->expectException(OCSException::class);
3540
+        $this->expectExceptionMessage('Cannot create sub-admins for admin group');
3541
+        $this->expectExceptionCode(103);
3542
+
3543
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3544
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3545
+        $targetGroup
3546
+            ->expects($this->once())
3547
+            ->method('getGID')
3548
+            ->willReturn('admin');
3549
+        $this->userManager
3550
+            ->expects($this->once())
3551
+            ->method('get')
3552
+            ->with('ExistingUser')
3553
+            ->willReturn($targetUser);
3554
+        $this->groupManager
3555
+            ->expects($this->once())
3556
+            ->method('get')
3557
+            ->with('ADmiN')
3558
+            ->willReturn($targetGroup);
3559
+
3560
+        $this->api->addSubAdmin('ExistingUser', 'ADmiN');
3561
+    }
3562
+
3563
+    public function testAddSubAdminTwice(): void {
3564
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3565
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3566
+        $this->userManager
3567
+            ->expects($this->once())
3568
+            ->method('get')
3569
+            ->with('ExistingUser')
3570
+            ->willReturn($targetUser);
3571
+        $this->groupManager
3572
+            ->expects($this->once())
3573
+            ->method('get')
3574
+            ->with('TargetGroup')
3575
+            ->willReturn($targetGroup);
3576
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3577
+            ->disableOriginalConstructor()->getMock();
3578
+        $subAdminManager
3579
+            ->expects($this->once())
3580
+            ->method('isSubAdminOfGroup')
3581
+            ->with($targetUser, $targetGroup)
3582
+            ->willReturn(true);
3583
+        $this->groupManager
3584
+            ->expects($this->once())
3585
+            ->method('getSubAdmin')
3586
+            ->willReturn($subAdminManager);
3587
+
3588
+        $this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData());
3589
+    }
3590
+
3591
+    public function testAddSubAdminSuccessful(): void {
3592
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3593
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3594
+        $this->userManager
3595
+            ->expects($this->once())
3596
+            ->method('get')
3597
+            ->with('ExistingUser')
3598
+            ->willReturn($targetUser);
3599
+        $this->groupManager
3600
+            ->expects($this->once())
3601
+            ->method('get')
3602
+            ->with('TargetGroup')
3603
+            ->willReturn($targetGroup);
3604
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3605
+            ->disableOriginalConstructor()->getMock();
3606
+        $subAdminManager
3607
+            ->expects($this->once())
3608
+            ->method('isSubAdminOfGroup')
3609
+            ->with($targetUser, $targetGroup)
3610
+            ->willReturn(false);
3611
+        $subAdminManager
3612
+            ->expects($this->once())
3613
+            ->method('createSubAdmin')
3614
+            ->with($targetUser, $targetGroup);
3615
+        $this->groupManager
3616
+            ->expects($this->once())
3617
+            ->method('getSubAdmin')
3618
+            ->willReturn($subAdminManager);
3619
+
3620
+        $this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData());
3621
+    }
3622
+
3623
+
3624
+    public function testRemoveSubAdminNotExistingTargetUser(): void {
3625
+        $this->expectException(OCSException::class);
3626
+        $this->expectExceptionMessage('User does not exist');
3627
+        $this->expectExceptionCode(101);
3628
+
3629
+        $this->userManager
3630
+            ->expects($this->once())
3631
+            ->method('get')
3632
+            ->with('NotExistingUser')
3633
+            ->willReturn(null);
3634
+
3635
+        $this->api->removeSubAdmin('NotExistingUser', 'GroupToDeleteFrom');
3636
+    }
3637
+
3638
+
3639
+    public function testRemoveSubAdminNotExistingTargetGroup(): void {
3640
+        $this->expectException(OCSException::class);
3641
+        $this->expectExceptionMessage('Group does not exist');
3642
+        $this->expectExceptionCode(101);
3643
+
3644
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3645
+        $this->userManager
3646
+            ->expects($this->once())
3647
+            ->method('get')
3648
+            ->with('ExistingUser')
3649
+            ->willReturn($targetUser);
3650
+        $this->groupManager
3651
+            ->expects($this->once())
3652
+            ->method('get')
3653
+            ->with('GroupToDeleteFrom')
3654
+            ->willReturn(null);
3655
+
3656
+        $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom');
3657
+    }
3658
+
3659
+
3660
+
3661
+    public function testRemoveSubAdminFromNotASubadmin(): void {
3662
+        $this->expectException(OCSException::class);
3663
+        $this->expectExceptionMessage('User is not a sub-admin of this group');
3664
+        $this->expectExceptionCode(102);
3665
+
3666
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3667
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3668
+        $this->userManager
3669
+            ->expects($this->once())
3670
+            ->method('get')
3671
+            ->with('ExistingUser')
3672
+            ->willReturn($targetUser);
3673
+        $this->groupManager
3674
+            ->expects($this->once())
3675
+            ->method('get')
3676
+            ->with('GroupToDeleteFrom')
3677
+            ->willReturn($targetGroup);
3678
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3679
+            ->disableOriginalConstructor()->getMock();
3680
+        $subAdminManager
3681
+            ->expects($this->once())
3682
+            ->method('isSubAdminOfGroup')
3683
+            ->with($targetUser, $targetGroup)
3684
+            ->willReturn(false);
3685
+        $this->groupManager
3686
+            ->expects($this->once())
3687
+            ->method('getSubAdmin')
3688
+            ->willReturn($subAdminManager);
3689
+
3690
+        $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom');
3691
+    }
3692
+
3693
+    public function testRemoveSubAdminSuccessful(): void {
3694
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3695
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3696
+        $this->userManager
3697
+            ->expects($this->once())
3698
+            ->method('get')
3699
+            ->with('ExistingUser')
3700
+            ->willReturn($targetUser);
3701
+        $this->groupManager
3702
+            ->expects($this->once())
3703
+            ->method('get')
3704
+            ->with('GroupToDeleteFrom')
3705
+            ->willReturn($targetGroup);
3706
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3707
+            ->disableOriginalConstructor()->getMock();
3708
+        $subAdminManager
3709
+            ->expects($this->once())
3710
+            ->method('isSubAdminOfGroup')
3711
+            ->with($targetUser, $targetGroup)
3712
+            ->willReturn(true);
3713
+        $subAdminManager
3714
+            ->expects($this->once())
3715
+            ->method('deleteSubAdmin')
3716
+            ->with($targetUser, $targetGroup);
3717
+        $this->groupManager
3718
+            ->expects($this->once())
3719
+            ->method('getSubAdmin')
3720
+            ->willReturn($subAdminManager);
3721
+
3722
+        $this->assertEquals([], $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom')->getData());
3723
+    }
3724
+
3725
+
3726
+    public function testGetUserSubAdminGroupsNotExistingTargetUser(): void {
3727
+        $this->expectException(OCSException::class);
3728
+        $this->expectExceptionMessage('User does not exist');
3729
+        $this->expectExceptionCode(404);
3730
+
3731
+        $this->userManager
3732
+            ->expects($this->once())
3733
+            ->method('get')
3734
+            ->with('RequestedUser')
3735
+            ->willReturn(null);
3736
+
3737
+        $this->api->getUserSubAdminGroups('RequestedUser');
3738
+    }
3739
+
3740
+    public function testGetUserSubAdminGroupsWithGroups(): void {
3741
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3742
+        $targetGroup = $this->getMockBuilder('\OCP\IGroup')->disableOriginalConstructor()->getMock();
3743
+        $targetGroup
3744
+            ->expects($this->once())
3745
+            ->method('getGID')
3746
+            ->willReturn('TargetGroup');
3747
+        $this->userManager
3748
+            ->expects($this->once())
3749
+            ->method('get')
3750
+            ->with('RequestedUser')
3751
+            ->willReturn($targetUser);
3752
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
3753
+            ->disableOriginalConstructor()->getMock();
3754
+        $subAdminManager
3755
+            ->expects($this->once())
3756
+            ->method('getSubAdminsGroups')
3757
+            ->with($targetUser)
3758
+            ->willReturn([$targetGroup]);
3759
+        $this->groupManager
3760
+            ->expects($this->once())
3761
+            ->method('getSubAdmin')
3762
+            ->willReturn($subAdminManager);
3763
+
3764
+        $this->assertEquals(['TargetGroup'], $this->api->getUserSubAdminGroups('RequestedUser')->getData());
3765
+    }
3766
+
3767
+    public function testEnableUser(): void {
3768
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3769
+        $targetUser->expects($this->once())
3770
+            ->method('setEnabled')
3771
+            ->with(true);
3772
+        $this->userManager
3773
+            ->expects($this->once())
3774
+            ->method('get')
3775
+            ->with('RequestedUser')
3776
+            ->willReturn($targetUser);
3777
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3778
+        $loggedInUser
3779
+            ->expects($this->exactly(3))
3780
+            ->method('getUID')
3781
+            ->willReturn('admin');
3782
+        $this->userSession
3783
+            ->expects($this->once())
3784
+            ->method('getUser')
3785
+            ->willReturn($loggedInUser);
3786
+        $this->groupManager
3787
+            ->expects($this->once())
3788
+            ->method('isAdmin')
3789
+            ->willReturn(true);
3790
+
3791
+        $this->assertEquals([], $this->api->enableUser('RequestedUser')->getData());
3792
+    }
3793
+
3794
+    public function testDisableUser(): void {
3795
+        $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3796
+        $targetUser->expects($this->once())
3797
+            ->method('setEnabled')
3798
+            ->with(false);
3799
+        $this->userManager
3800
+            ->expects($this->once())
3801
+            ->method('get')
3802
+            ->with('RequestedUser')
3803
+            ->willReturn($targetUser);
3804
+        $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
3805
+        $loggedInUser
3806
+            ->expects($this->exactly(3))
3807
+            ->method('getUID')
3808
+            ->willReturn('admin');
3809
+        $this->userSession
3810
+            ->expects($this->once())
3811
+            ->method('getUser')
3812
+            ->willReturn($loggedInUser);
3813
+        $this->groupManager
3814
+            ->expects($this->once())
3815
+            ->method('isAdmin')
3816
+            ->willReturn(true);
3817
+
3818
+        $this->assertEquals([], $this->api->disableUser('RequestedUser')->getData());
3819
+    }
3820
+
3821
+    public function testGetCurrentUserLoggedIn(): void {
3822
+        $user = $this->createMock(IUser::class);
3823
+        $user->expects($this->once())->method('getUID')->willReturn('UID');
3824
+
3825
+        $this->userSession->expects($this->once())->method('getUser')
3826
+            ->willReturn($user);
3827
+
3828
+        /** @var UsersController | MockObject $api */
3829
+        $api = $this->getMockBuilder(UsersController::class)
3830
+            ->setConstructorArgs([
3831
+                'provisioning_api',
3832
+                $this->request,
3833
+                $this->userManager,
3834
+                $this->config,
3835
+                $this->groupManager,
3836
+                $this->userSession,
3837
+                $this->accountManager,
3838
+                $this->subAdminManager,
3839
+                $this->l10nFactory,
3840
+                $this->rootFolder,
3841
+                $this->urlGenerator,
3842
+                $this->logger,
3843
+                $this->newUserMailHelper,
3844
+                $this->secureRandom,
3845
+                $this->remoteWipe,
3846
+                $this->knownUserService,
3847
+                $this->eventDispatcher,
3848
+                $this->phoneNumberUtil,
3849
+                $this->appManager,
3850
+            ])
3851
+            ->onlyMethods(['getUserData'])
3852
+            ->getMock();
3853
+
3854
+        $api->expects($this->once())->method('getUserData')->with('UID', true)
3855
+            ->willReturn(
3856
+                [
3857
+                    'id' => 'UID',
3858
+                    'enabled' => 'true',
3859
+                    'quota' => ['DummyValue'],
3860
+                    'email' => '[email protected]',
3861
+                    'displayname' => 'Demo User',
3862
+                    'display-name' => 'Demo User',
3863
+                    'phone' => 'phone',
3864
+                    'address' => 'address',
3865
+                    'website' => 'website',
3866
+                    'twitter' => 'twitter',
3867
+                    'bluesky' => 'bluesky',
3868
+                    'fediverse' => 'fediverse',
3869
+                    'organisation' => 'organisation',
3870
+                    'role' => 'role',
3871
+                    'headline' => 'headline',
3872
+                    'biography' => 'biography',
3873
+                    'profile_enabled' => '1',
3874
+                    'pronouns' => 'they/them',
3875
+                ]
3876
+            );
3877
+
3878
+        $expected = [
3879
+            'id' => 'UID',
3880
+            'enabled' => 'true',
3881
+            'quota' => ['DummyValue'],
3882
+            'email' => '[email protected]',
3883
+            'displayname' => 'Demo User',
3884
+            'display-name' => 'Demo User',
3885
+            'phone' => 'phone',
3886
+            'address' => 'address',
3887
+            'website' => 'website',
3888
+            'twitter' => 'twitter',
3889
+            'bluesky' => 'bluesky',
3890
+            'fediverse' => 'fediverse',
3891
+            'organisation' => 'organisation',
3892
+            'role' => 'role',
3893
+            'headline' => 'headline',
3894
+            'biography' => 'biography',
3895
+            'profile_enabled' => '1',
3896
+            'pronouns' => 'they/them',
3897
+        ];
3898
+
3899
+        $this->assertSame($expected, $api->getCurrentUser()->getData());
3900
+    }
3901
+
3902
+
3903
+    public function testGetCurrentUserNotLoggedIn(): void {
3904
+        $this->expectException(OCSException::class);
3905
+
3906
+
3907
+        $this->userSession->expects($this->once())->method('getUser')
3908
+            ->willReturn(null);
3909
+
3910
+        $this->api->getCurrentUser();
3911
+    }
3912
+
3913
+    public function testGetUser(): void {
3914
+        $loggedInUser = $this->createMock(IUser::class);
3915
+        $loggedInUser
3916
+            ->method('getUID')
3917
+            ->willReturn('currentuser');
3918
+        $this->userSession
3919
+            ->method('getUser')
3920
+            ->willReturn($loggedInUser);
3921
+
3922
+        /** @var UsersController | MockObject $api */
3923
+        $api = $this->getMockBuilder(UsersController::class)
3924
+            ->setConstructorArgs([
3925
+                'provisioning_api',
3926
+                $this->request,
3927
+                $this->userManager,
3928
+                $this->config,
3929
+                $this->groupManager,
3930
+                $this->userSession,
3931
+                $this->accountManager,
3932
+                $this->subAdminManager,
3933
+                $this->l10nFactory,
3934
+                $this->rootFolder,
3935
+                $this->urlGenerator,
3936
+                $this->logger,
3937
+                $this->newUserMailHelper,
3938
+                $this->secureRandom,
3939
+                $this->remoteWipe,
3940
+                $this->knownUserService,
3941
+                $this->eventDispatcher,
3942
+                $this->phoneNumberUtil,
3943
+                $this->appManager,
3944
+            ])
3945
+            ->onlyMethods(['getUserData'])
3946
+            ->getMock();
3947
+
3948
+        $expected = [
3949
+            'id' => 'UID',
3950
+            'enabled' => 'true',
3951
+            'quota' => ['DummyValue'],
3952
+            'email' => '[email protected]',
3953
+            'phone' => 'phone',
3954
+            'address' => 'address',
3955
+            'website' => 'website',
3956
+            'twitter' => 'twitter',
3957
+            'bluesky' => 'bluesky',
3958
+            'fediverse' => 'fediverse',
3959
+            'displayname' => 'Demo User',
3960
+            'display-name' => 'Demo User',
3961
+            'organisation' => 'organisation',
3962
+            'role' => 'role',
3963
+            'headline' => 'headline',
3964
+            'biography' => 'biography',
3965
+            'profile_enabled' => '1',
3966
+            'pronouns' => 'they/them',
3967
+        ];
3968
+
3969
+        $api->expects($this->exactly(2))
3970
+            ->method('getUserData')
3971
+            ->willReturnMap([
3972
+                ['uid', false, $expected],
3973
+                ['currentuser', true, $expected],
3974
+            ]);
3975
+
3976
+        $this->assertSame($expected, $api->getUser('uid')->getData());
3977
+
3978
+        $this->assertSame($expected, $api->getUser('currentuser')->getData());
3979
+    }
3980
+
3981
+
3982
+    public function testResendWelcomeMessageWithNotExistingTargetUser(): void {
3983
+        $this->expectException(OCSException::class);
3984
+        $this->expectExceptionCode(998);
3985
+
3986
+        $this->userManager
3987
+            ->expects($this->once())
3988
+            ->method('get')
3989
+            ->with('NotExistingUser')
3990
+            ->willReturn(null);
3991
+
3992
+        $this->api->resendWelcomeMessage('NotExistingUser');
3993
+    }
3994
+
3995
+
3996
+    public function testResendWelcomeMessageAsSubAdminAndUserIsNotAccessible(): void {
3997
+        $this->expectException(OCSException::class);
3998
+        $this->expectExceptionCode(998);
3999
+
4000
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4001
+            ->disableOriginalConstructor()
4002
+            ->getMock();
4003
+        $loggedInUser
4004
+            ->expects($this->exactly(2))
4005
+            ->method('getUID')
4006
+            ->willReturn('subadmin');
4007
+        $targetUser = $this->getMockBuilder(IUser::class)
4008
+            ->disableOriginalConstructor()
4009
+            ->getMock();
4010
+        $this->userSession
4011
+            ->expects($this->once())
4012
+            ->method('getUser')
4013
+            ->willReturn($loggedInUser);
4014
+        $this->userManager
4015
+            ->expects($this->once())
4016
+            ->method('get')
4017
+            ->with('UserToGet')
4018
+            ->willReturn($targetUser);
4019
+        $this->groupManager
4020
+            ->expects($this->once())
4021
+            ->method('isAdmin')
4022
+            ->with('subadmin')
4023
+            ->willReturn(false);
4024
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4025
+            ->disableOriginalConstructor()
4026
+            ->getMock();
4027
+        $subAdminManager
4028
+            ->expects($this->once())
4029
+            ->method('isUserAccessible')
4030
+            ->with($loggedInUser, $targetUser)
4031
+            ->willReturn(false);
4032
+        $this->groupManager
4033
+            ->expects($this->once())
4034
+            ->method('getSubAdmin')
4035
+            ->willReturn($subAdminManager);
4036
+
4037
+        $this->api->resendWelcomeMessage('UserToGet');
4038
+    }
4039
+
4040
+
4041
+    public function testResendWelcomeMessageNoEmail(): void {
4042
+        $this->expectException(OCSException::class);
4043
+        $this->expectExceptionMessage('Email address not available');
4044
+        $this->expectExceptionCode(101);
4045
+
4046
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4047
+            ->disableOriginalConstructor()
4048
+            ->getMock();
4049
+        $targetUser = $this->getMockBuilder(IUser::class)
4050
+            ->disableOriginalConstructor()
4051
+            ->getMock();
4052
+        $this->userSession
4053
+            ->expects($this->once())
4054
+            ->method('getUser')
4055
+            ->willReturn($loggedInUser);
4056
+        $this->userManager
4057
+            ->expects($this->once())
4058
+            ->method('get')
4059
+            ->with('UserToGet')
4060
+            ->willReturn($targetUser);
4061
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4062
+            ->disableOriginalConstructor()
4063
+            ->getMock();
4064
+        $subAdminManager
4065
+            ->expects($this->once())
4066
+            ->method('isUserAccessible')
4067
+            ->with($loggedInUser, $targetUser)
4068
+            ->willReturn(true);
4069
+        $this->groupManager
4070
+            ->expects($this->once())
4071
+            ->method('getSubAdmin')
4072
+            ->willReturn($subAdminManager);
4073
+        $loggedInUser
4074
+            ->expects($this->exactly(2))
4075
+            ->method('getUID')
4076
+            ->willReturn('logged-user-id');
4077
+        $targetUser
4078
+            ->expects($this->once())
4079
+            ->method('getEmailAddress')
4080
+            ->willReturn('');
4081
+
4082
+        $this->api->resendWelcomeMessage('UserToGet');
4083
+    }
4084
+
4085
+
4086
+    public function testResendWelcomeMessageNullEmail(): void {
4087
+        $this->expectException(OCSException::class);
4088
+        $this->expectExceptionMessage('Email address not available');
4089
+        $this->expectExceptionCode(101);
4090
+
4091
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4092
+            ->disableOriginalConstructor()
4093
+            ->getMock();
4094
+        $targetUser = $this->getMockBuilder(IUser::class)
4095
+            ->disableOriginalConstructor()
4096
+            ->getMock();
4097
+        $this->userSession
4098
+            ->expects($this->once())
4099
+            ->method('getUser')
4100
+            ->willReturn($loggedInUser);
4101
+        $this->userManager
4102
+            ->expects($this->once())
4103
+            ->method('get')
4104
+            ->with('UserToGet')
4105
+            ->willReturn($targetUser);
4106
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4107
+            ->disableOriginalConstructor()
4108
+            ->getMock();
4109
+        $subAdminManager
4110
+            ->expects($this->once())
4111
+            ->method('isUserAccessible')
4112
+            ->with($loggedInUser, $targetUser)
4113
+            ->willReturn(true);
4114
+        $this->groupManager
4115
+            ->expects($this->once())
4116
+            ->method('getSubAdmin')
4117
+            ->willReturn($subAdminManager);
4118
+        $loggedInUser
4119
+            ->expects($this->exactly(2))
4120
+            ->method('getUID')
4121
+            ->willReturn('logged-user-id');
4122
+        $targetUser
4123
+            ->expects($this->once())
4124
+            ->method('getEmailAddress')
4125
+            ->willReturn(null);
4126
+
4127
+        $this->api->resendWelcomeMessage('UserToGet');
4128
+    }
4129
+
4130
+    public function testResendWelcomeMessageSuccess(): void {
4131
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4132
+            ->disableOriginalConstructor()
4133
+            ->getMock();
4134
+        $targetUser = $this->getMockBuilder(IUser::class)
4135
+            ->disableOriginalConstructor()
4136
+            ->getMock();
4137
+        $loggedInUser
4138
+            ->method('getUID')
4139
+            ->willReturn('logged-user-id');
4140
+        $targetUser
4141
+            ->method('getUID')
4142
+            ->willReturn('user-id');
4143
+        $this->userSession
4144
+            ->expects($this->once())
4145
+            ->method('getUser')
4146
+            ->willReturn($loggedInUser);
4147
+        $this->userManager
4148
+            ->expects($this->once())
4149
+            ->method('get')
4150
+            ->with('UserToGet')
4151
+            ->willReturn($targetUser);
4152
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4153
+            ->disableOriginalConstructor()
4154
+            ->getMock();
4155
+        $subAdminManager
4156
+            ->expects($this->once())
4157
+            ->method('isUserAccessible')
4158
+            ->with($loggedInUser, $targetUser)
4159
+            ->willReturn(true);
4160
+        $this->groupManager
4161
+            ->expects($this->once())
4162
+            ->method('getSubAdmin')
4163
+            ->willReturn($subAdminManager);
4164
+        $targetUser
4165
+            ->expects($this->once())
4166
+            ->method('getEmailAddress')
4167
+            ->willReturn('[email protected]');
4168
+        $emailTemplate = $this->createMock(IEMailTemplate::class);
4169
+        $this->newUserMailHelper
4170
+            ->expects($this->once())
4171
+            ->method('generateTemplate')
4172
+            ->willReturn($emailTemplate);
4173
+        $this->newUserMailHelper
4174
+            ->expects($this->once())
4175
+            ->method('sendMail')
4176
+            ->with($targetUser, $emailTemplate);
4177
+
4178
+        $this->api->resendWelcomeMessage('UserToGet');
4179
+    }
4180
+
4181
+    public function testResendWelcomeMessageSuccessWithFallbackLanguage(): void {
4182
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4183
+            ->disableOriginalConstructor()
4184
+            ->getMock();
4185
+        $targetUser = $this->getMockBuilder(IUser::class)
4186
+            ->disableOriginalConstructor()
4187
+            ->getMock();
4188
+        $loggedInUser
4189
+            ->method('getUID')
4190
+            ->willReturn('logged-user-id');
4191
+        $targetUser
4192
+            ->method('getUID')
4193
+            ->willReturn('user-id');
4194
+        $this->userSession
4195
+            ->expects($this->once())
4196
+            ->method('getUser')
4197
+            ->willReturn($loggedInUser);
4198
+        $this->userManager
4199
+            ->expects($this->once())
4200
+            ->method('get')
4201
+            ->with('UserToGet')
4202
+            ->willReturn($targetUser);
4203
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4204
+            ->disableOriginalConstructor()
4205
+            ->getMock();
4206
+        $subAdminManager
4207
+            ->expects($this->once())
4208
+            ->method('isUserAccessible')
4209
+            ->with($loggedInUser, $targetUser)
4210
+            ->willReturn(true);
4211
+        $this->groupManager
4212
+            ->expects($this->once())
4213
+            ->method('getSubAdmin')
4214
+            ->willReturn($subAdminManager);
4215
+        $targetUser
4216
+            ->expects($this->once())
4217
+            ->method('getEmailAddress')
4218
+            ->willReturn('[email protected]');
4219
+        $emailTemplate = $this->createMock(IEMailTemplate::class);
4220
+        $this->newUserMailHelper
4221
+            ->expects($this->once())
4222
+            ->method('generateTemplate')
4223
+            ->willReturn($emailTemplate);
4224
+        $this->newUserMailHelper
4225
+            ->expects($this->once())
4226
+            ->method('sendMail')
4227
+            ->with($targetUser, $emailTemplate);
4228
+
4229
+        $this->api->resendWelcomeMessage('UserToGet');
4230
+    }
4231
+
4232
+
4233
+    public function testResendWelcomeMessageFailed(): void {
4234
+        $this->expectException(OCSException::class);
4235
+        $this->expectExceptionMessage('Sending email failed');
4236
+        $this->expectExceptionCode(102);
4237
+
4238
+        $loggedInUser = $this->getMockBuilder(IUser::class)
4239
+            ->disableOriginalConstructor()
4240
+            ->getMock();
4241
+        $targetUser = $this->getMockBuilder(IUser::class)
4242
+            ->disableOriginalConstructor()
4243
+            ->getMock();
4244
+        $loggedInUser
4245
+            ->expects($this->exactly(2))
4246
+            ->method('getUID')
4247
+            ->willReturn('logged-user-id');
4248
+        $targetUser
4249
+            ->method('getUID')
4250
+            ->willReturn('user-id');
4251
+        $this->userSession
4252
+            ->expects($this->once())
4253
+            ->method('getUser')
4254
+            ->willReturn($loggedInUser);
4255
+        $this->userManager
4256
+            ->expects($this->once())
4257
+            ->method('get')
4258
+            ->with('UserToGet')
4259
+            ->willReturn($targetUser);
4260
+        $subAdminManager = $this->getMockBuilder('OC\SubAdmin')
4261
+            ->disableOriginalConstructor()
4262
+            ->getMock();
4263
+        $subAdminManager
4264
+            ->expects($this->once())
4265
+            ->method('isUserAccessible')
4266
+            ->with($loggedInUser, $targetUser)
4267
+            ->willReturn(true);
4268
+        $this->groupManager
4269
+            ->expects($this->once())
4270
+            ->method('getSubAdmin')
4271
+            ->willReturn($subAdminManager);
4272
+        $targetUser
4273
+            ->expects($this->once())
4274
+            ->method('getEmailAddress')
4275
+            ->willReturn('[email protected]');
4276
+        $emailTemplate = $this->createMock(IEMailTemplate::class);
4277
+        $this->newUserMailHelper
4278
+            ->expects($this->once())
4279
+            ->method('generateTemplate')
4280
+            ->willReturn($emailTemplate);
4281
+        $this->newUserMailHelper
4282
+            ->expects($this->once())
4283
+            ->method('sendMail')
4284
+            ->with($targetUser, $emailTemplate)
4285
+            ->willThrowException(new \Exception());
4286
+
4287
+        $this->api->resendWelcomeMessage('UserToGet');
4288
+    }
4289
+
4290
+
4291
+    public static function dataGetEditableFields(): array {
4292
+        return [
4293
+            [false, true, ISetDisplayNameBackend::class, [
4294
+                IAccountManager::PROPERTY_EMAIL,
4295
+                IAccountManager::COLLECTION_EMAIL,
4296
+                IAccountManager::PROPERTY_PHONE,
4297
+                IAccountManager::PROPERTY_ADDRESS,
4298
+                IAccountManager::PROPERTY_WEBSITE,
4299
+                IAccountManager::PROPERTY_TWITTER,
4300
+                IAccountManager::PROPERTY_BLUESKY,
4301
+                IAccountManager::PROPERTY_FEDIVERSE,
4302
+                IAccountManager::PROPERTY_ORGANISATION,
4303
+                IAccountManager::PROPERTY_ROLE,
4304
+                IAccountManager::PROPERTY_HEADLINE,
4305
+                IAccountManager::PROPERTY_BIOGRAPHY,
4306
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4307
+                IAccountManager::PROPERTY_PRONOUNS,
4308
+            ]],
4309
+            [true, false, ISetDisplayNameBackend::class, [
4310
+                IAccountManager::PROPERTY_DISPLAYNAME,
4311
+                IAccountManager::COLLECTION_EMAIL,
4312
+                IAccountManager::PROPERTY_PHONE,
4313
+                IAccountManager::PROPERTY_ADDRESS,
4314
+                IAccountManager::PROPERTY_WEBSITE,
4315
+                IAccountManager::PROPERTY_TWITTER,
4316
+                IAccountManager::PROPERTY_BLUESKY,
4317
+                IAccountManager::PROPERTY_FEDIVERSE,
4318
+                IAccountManager::PROPERTY_ORGANISATION,
4319
+                IAccountManager::PROPERTY_ROLE,
4320
+                IAccountManager::PROPERTY_HEADLINE,
4321
+                IAccountManager::PROPERTY_BIOGRAPHY,
4322
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4323
+                IAccountManager::PROPERTY_PRONOUNS,
4324
+            ]],
4325
+            [true, true, ISetDisplayNameBackend::class, [
4326
+                IAccountManager::PROPERTY_DISPLAYNAME,
4327
+                IAccountManager::PROPERTY_EMAIL,
4328
+                IAccountManager::COLLECTION_EMAIL,
4329
+                IAccountManager::PROPERTY_PHONE,
4330
+                IAccountManager::PROPERTY_ADDRESS,
4331
+                IAccountManager::PROPERTY_WEBSITE,
4332
+                IAccountManager::PROPERTY_TWITTER,
4333
+                IAccountManager::PROPERTY_BLUESKY,
4334
+                IAccountManager::PROPERTY_FEDIVERSE,
4335
+                IAccountManager::PROPERTY_ORGANISATION,
4336
+                IAccountManager::PROPERTY_ROLE,
4337
+                IAccountManager::PROPERTY_HEADLINE,
4338
+                IAccountManager::PROPERTY_BIOGRAPHY,
4339
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4340
+                IAccountManager::PROPERTY_PRONOUNS,
4341
+            ]],
4342
+            [false, false, ISetDisplayNameBackend::class, [
4343
+                IAccountManager::COLLECTION_EMAIL,
4344
+                IAccountManager::PROPERTY_PHONE,
4345
+                IAccountManager::PROPERTY_ADDRESS,
4346
+                IAccountManager::PROPERTY_WEBSITE,
4347
+                IAccountManager::PROPERTY_TWITTER,
4348
+                IAccountManager::PROPERTY_BLUESKY,
4349
+                IAccountManager::PROPERTY_FEDIVERSE,
4350
+                IAccountManager::PROPERTY_ORGANISATION,
4351
+                IAccountManager::PROPERTY_ROLE,
4352
+                IAccountManager::PROPERTY_HEADLINE,
4353
+                IAccountManager::PROPERTY_BIOGRAPHY,
4354
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4355
+                IAccountManager::PROPERTY_PRONOUNS,
4356
+            ]],
4357
+            [false, true, UserInterface::class, [
4358
+                IAccountManager::PROPERTY_EMAIL,
4359
+                IAccountManager::COLLECTION_EMAIL,
4360
+                IAccountManager::PROPERTY_PHONE,
4361
+                IAccountManager::PROPERTY_ADDRESS,
4362
+                IAccountManager::PROPERTY_WEBSITE,
4363
+                IAccountManager::PROPERTY_TWITTER,
4364
+                IAccountManager::PROPERTY_BLUESKY,
4365
+                IAccountManager::PROPERTY_FEDIVERSE,
4366
+                IAccountManager::PROPERTY_ORGANISATION,
4367
+                IAccountManager::PROPERTY_ROLE,
4368
+                IAccountManager::PROPERTY_HEADLINE,
4369
+                IAccountManager::PROPERTY_BIOGRAPHY,
4370
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4371
+                IAccountManager::PROPERTY_PRONOUNS,
4372
+            ]],
4373
+            [true, false, UserInterface::class, [
4374
+                IAccountManager::COLLECTION_EMAIL,
4375
+                IAccountManager::PROPERTY_PHONE,
4376
+                IAccountManager::PROPERTY_ADDRESS,
4377
+                IAccountManager::PROPERTY_WEBSITE,
4378
+                IAccountManager::PROPERTY_TWITTER,
4379
+                IAccountManager::PROPERTY_BLUESKY,
4380
+                IAccountManager::PROPERTY_FEDIVERSE,
4381
+                IAccountManager::PROPERTY_ORGANISATION,
4382
+                IAccountManager::PROPERTY_ROLE,
4383
+                IAccountManager::PROPERTY_HEADLINE,
4384
+                IAccountManager::PROPERTY_BIOGRAPHY,
4385
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4386
+                IAccountManager::PROPERTY_PRONOUNS,
4387
+            ]],
4388
+            [true, true, UserInterface::class, [
4389
+                IAccountManager::PROPERTY_EMAIL,
4390
+                IAccountManager::COLLECTION_EMAIL,
4391
+                IAccountManager::PROPERTY_PHONE,
4392
+                IAccountManager::PROPERTY_ADDRESS,
4393
+                IAccountManager::PROPERTY_WEBSITE,
4394
+                IAccountManager::PROPERTY_TWITTER,
4395
+                IAccountManager::PROPERTY_BLUESKY,
4396
+                IAccountManager::PROPERTY_FEDIVERSE,
4397
+                IAccountManager::PROPERTY_ORGANISATION,
4398
+                IAccountManager::PROPERTY_ROLE,
4399
+                IAccountManager::PROPERTY_HEADLINE,
4400
+                IAccountManager::PROPERTY_BIOGRAPHY,
4401
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4402
+                IAccountManager::PROPERTY_PRONOUNS,
4403
+            ]],
4404
+            [false, false, UserInterface::class, [
4405
+                IAccountManager::COLLECTION_EMAIL,
4406
+                IAccountManager::PROPERTY_PHONE,
4407
+                IAccountManager::PROPERTY_ADDRESS,
4408
+                IAccountManager::PROPERTY_WEBSITE,
4409
+                IAccountManager::PROPERTY_TWITTER,
4410
+                IAccountManager::PROPERTY_BLUESKY,
4411
+                IAccountManager::PROPERTY_FEDIVERSE,
4412
+                IAccountManager::PROPERTY_ORGANISATION,
4413
+                IAccountManager::PROPERTY_ROLE,
4414
+                IAccountManager::PROPERTY_HEADLINE,
4415
+                IAccountManager::PROPERTY_BIOGRAPHY,
4416
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
4417
+                IAccountManager::PROPERTY_PRONOUNS,
4418
+            ]],
4419
+        ];
4420
+    }
4421
+
4422
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataGetEditableFields')]
4423
+    public function testGetEditableFields(bool $allowedToChangeDisplayName, bool $allowedToChangeEmail, string $userBackend, array $expected): void {
4424
+        $this->config->method('getSystemValue')->willReturnCallback(fn (string $key, mixed $default) => match ($key) {
4425
+            'allow_user_to_change_display_name' => $allowedToChangeDisplayName,
4426
+            'allow_user_to_change_email' => $allowedToChangeEmail,
4427
+            default => throw new RuntimeException('Unexpected system config key: ' . $key),
4428
+        });
4429
+
4430
+        $user = $this->createMock(IUser::class);
4431
+        $this->userSession->method('getUser')
4432
+            ->willReturn($user);
4433
+
4434
+        $backend = $this->createMock($userBackend);
4435
+
4436
+        $user->method('getUID')
4437
+            ->willReturn('userId');
4438
+        $user->method('getBackend')
4439
+            ->willReturn($backend);
4440
+
4441
+        $expectedResp = new DataResponse($expected);
4442
+        $this->assertEquals($expectedResp, $this->api->getEditableFields('userId'));
4443
+    }
4444
+
4445
+    private function mockAccount($targetUser, $accountProperties) {
4446
+        $mockedProperties = [];
4447
+
4448
+        foreach ($accountProperties as $propertyName => $data) {
4449
+            $mockedProperty = $this->createMock(IAccountProperty::class);
4450
+            $mockedProperty->method('getValue')->willReturn($data['value'] ?? '');
4451
+            $mockedProperty->method('getScope')->willReturn($data['scope'] ?? '');
4452
+            $mockedProperties[] = [$propertyName, $mockedProperty];
4453
+        }
4454
+
4455
+        $account = $this->createMock(IAccount::class);
4456
+        $account->method('getProperty')
4457
+            ->willReturnMap($mockedProperties);
4458
+
4459
+        $this->accountManager->expects($this->any())->method('getAccount')
4460
+            ->with($targetUser)
4461
+            ->willReturn($account);
4462
+    }
4463 4463
 }
Please login to merge, or discard this patch.
apps/provisioning_api/lib/Controller/AUserDataOCSController.php 1 patch
Indentation   +284 added lines, -284 removed lines patch added patch discarded remove patch
@@ -38,288 +38,288 @@
 block discarded – undo
38 38
  * @psalm-import-type Provisioning_APIUserDetailsQuota from ResponseDefinitions
39 39
  */
40 40
 abstract class AUserDataOCSController extends OCSController {
41
-	public const SCOPE_SUFFIX = 'Scope';
42
-
43
-	public const USER_FIELD_DISPLAYNAME = 'display';
44
-	public const USER_FIELD_LANGUAGE = 'language';
45
-	public const USER_FIELD_LOCALE = 'locale';
46
-	public const USER_FIELD_FIRST_DAY_OF_WEEK = 'first_day_of_week';
47
-	public const USER_FIELD_PASSWORD = 'password';
48
-	public const USER_FIELD_QUOTA = 'quota';
49
-	public const USER_FIELD_MANAGER = 'manager';
50
-	public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email';
51
-
52
-	public function __construct(
53
-		string $appName,
54
-		IRequest $request,
55
-		protected IUserManager $userManager,
56
-		protected IConfig $config,
57
-		protected GroupManager $groupManager,
58
-		protected IUserSession $userSession,
59
-		protected IAccountManager $accountManager,
60
-		protected ISubAdmin $subAdminManager,
61
-		protected IFactory $l10nFactory,
62
-		protected IRootFolder $rootFolder,
63
-	) {
64
-		parent::__construct($appName, $request);
65
-	}
66
-
67
-	/**
68
-	 * creates a array with all user data
69
-	 *
70
-	 * @param string $userId
71
-	 * @param bool $includeScopes
72
-	 * @return Provisioning_APIUserDetails|null
73
-	 * @throws NotFoundException
74
-	 * @throws OCSException
75
-	 * @throws OCSNotFoundException
76
-	 */
77
-	protected function getUserData(string $userId, bool $includeScopes = false): ?array {
78
-		$currentLoggedInUser = $this->userSession->getUser();
79
-		assert($currentLoggedInUser !== null, 'No user logged in');
80
-
81
-		$data = [];
82
-
83
-		// Check if the target user exists
84
-		$targetUserObject = $this->userManager->get($userId);
85
-		if ($targetUserObject === null) {
86
-			throw new OCSNotFoundException('User does not exist');
87
-		}
88
-
89
-		$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
90
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
91
-		if ($isAdmin
92
-			|| $isDelegatedAdmin
93
-			|| $this->groupManager->getSubAdmin()->isUserAccessible($currentLoggedInUser, $targetUserObject)) {
94
-			$data['enabled'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'enabled', 'true') === 'true';
95
-		} else {
96
-			// Check they are looking up themselves
97
-			if ($currentLoggedInUser->getUID() !== $targetUserObject->getUID()) {
98
-				return null;
99
-			}
100
-		}
101
-
102
-		// Get groups data
103
-		$userAccount = $this->accountManager->getAccount($targetUserObject);
104
-		$groups = $this->groupManager->getUserGroups($targetUserObject);
105
-		$gids = [];
106
-		foreach ($groups as $group) {
107
-			$gids[] = $group->getGID();
108
-		}
109
-
110
-		if ($isAdmin || $isDelegatedAdmin) {
111
-			try {
112
-				# might be thrown by LDAP due to handling of users disappears
113
-				# from the external source (reasons unknown to us)
114
-				# cf. https://github.com/nextcloud/server/issues/12991
115
-				$data['storageLocation'] = $targetUserObject->getHome();
116
-			} catch (NoUserException $e) {
117
-				throw new OCSNotFoundException($e->getMessage(), $e);
118
-			}
119
-		}
120
-
121
-		// Find the data
122
-		$data['id'] = $targetUserObject->getUID();
123
-		$data['firstLoginTimestamp'] = $targetUserObject->getFirstLogin();
124
-		$data['lastLoginTimestamp'] = $targetUserObject->getLastLogin();
125
-		$data['lastLogin'] = $targetUserObject->getLastLogin() * 1000;
126
-		$data['backend'] = $targetUserObject->getBackendClassName();
127
-		$data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID());
128
-		$data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject);
129
-		$managers = $this->getManagers($targetUserObject);
130
-		$data[self::USER_FIELD_MANAGER] = empty($managers) ? '' : $managers[0];
131
-
132
-		try {
133
-			if ($includeScopes) {
134
-				$data[IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR)->getScope();
135
-			}
136
-
137
-			$data[IAccountManager::PROPERTY_EMAIL] = $targetUserObject->getSystemEMailAddress();
138
-			if ($includeScopes) {
139
-				$data[IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope();
140
-			}
141
-
142
-			$additionalEmails = $additionalEmailScopes = [];
143
-			$emailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
144
-			foreach ($emailCollection->getProperties() as $property) {
145
-				$email = mb_strtolower(trim($property->getValue()));
146
-				$additionalEmails[] = $email;
147
-				if ($includeScopes) {
148
-					$additionalEmailScopes[] = $property->getScope();
149
-				}
150
-			}
151
-			$data[IAccountManager::COLLECTION_EMAIL] = $additionalEmails;
152
-			if ($includeScopes) {
153
-				$data[IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX] = $additionalEmailScopes;
154
-			}
155
-
156
-			$data[IAccountManager::PROPERTY_DISPLAYNAME] = $targetUserObject->getDisplayName();
157
-			$data[IAccountManager::PROPERTY_DISPLAYNAME_LEGACY] = $data[IAccountManager::PROPERTY_DISPLAYNAME];
158
-			if ($includeScopes) {
159
-				$data[IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope();
160
-			}
161
-
162
-			foreach ([
163
-				IAccountManager::PROPERTY_PHONE,
164
-				IAccountManager::PROPERTY_ADDRESS,
165
-				IAccountManager::PROPERTY_WEBSITE,
166
-				IAccountManager::PROPERTY_TWITTER,
167
-				IAccountManager::PROPERTY_BLUESKY,
168
-				IAccountManager::PROPERTY_FEDIVERSE,
169
-				IAccountManager::PROPERTY_ORGANISATION,
170
-				IAccountManager::PROPERTY_ROLE,
171
-				IAccountManager::PROPERTY_HEADLINE,
172
-				IAccountManager::PROPERTY_BIOGRAPHY,
173
-				IAccountManager::PROPERTY_PROFILE_ENABLED,
174
-				IAccountManager::PROPERTY_PRONOUNS,
175
-			] as $propertyName) {
176
-				$property = $userAccount->getProperty($propertyName);
177
-				$data[$propertyName] = $property->getValue();
178
-				if ($includeScopes) {
179
-					$data[$propertyName . self::SCOPE_SUFFIX] = $property->getScope();
180
-				}
181
-			}
182
-		} catch (PropertyDoesNotExistException $e) {
183
-			// hard coded properties should exist
184
-			throw new OCSException($e->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR, $e);
185
-		}
186
-
187
-		$data['groups'] = $gids;
188
-		$data[self::USER_FIELD_LANGUAGE] = $this->l10nFactory->getUserLanguage($targetUserObject);
189
-		$data[self::USER_FIELD_LOCALE] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale');
190
-		$data[self::USER_FIELD_NOTIFICATION_EMAIL] = $targetUserObject->getPrimaryEMailAddress();
191
-
192
-		$backend = $targetUserObject->getBackend();
193
-		$data['backendCapabilities'] = [
194
-			'setDisplayName' => $backend instanceof ISetDisplayNameBackend || $backend->implementsActions(Backend::SET_DISPLAYNAME),
195
-			'setPassword' => $backend instanceof ISetPasswordBackend || $backend->implementsActions(Backend::SET_PASSWORD),
196
-		];
197
-
198
-		return $data;
199
-	}
200
-
201
-	/**
202
-	 * @return string[]
203
-	 */
204
-	protected function getManagers(IUser $user): array {
205
-		$currentLoggedInUser = $this->userSession->getUser();
206
-
207
-		$managerUids = $user->getManagerUids();
208
-		if ($this->groupManager->isAdmin($currentLoggedInUser->getUID()) || $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())) {
209
-			return $managerUids;
210
-		}
211
-
212
-		if ($this->subAdminManager->isSubAdmin($currentLoggedInUser)) {
213
-			$accessibleManagerUids = array_values(array_filter(
214
-				$managerUids,
215
-				function (string $managerUid) use ($currentLoggedInUser) {
216
-					$manager = $this->userManager->get($managerUid);
217
-					if (!($manager instanceof IUser)) {
218
-						return false;
219
-					}
220
-					return $this->subAdminManager->isUserAccessible($currentLoggedInUser, $manager);
221
-				},
222
-			));
223
-			return $accessibleManagerUids;
224
-		}
225
-
226
-		return [];
227
-	}
228
-
229
-	/**
230
-	 * Get the groups a user is a subadmin of
231
-	 *
232
-	 * @param string $userId
233
-	 * @return list<string>
234
-	 * @throws OCSException
235
-	 */
236
-	protected function getUserSubAdminGroupsData(string $userId): array {
237
-		$user = $this->userManager->get($userId);
238
-		// Check if the user exists
239
-		if ($user === null) {
240
-			throw new OCSNotFoundException('User does not exist');
241
-		}
242
-
243
-		// Get the subadmin groups
244
-		$subAdminGroups = $this->groupManager->getSubAdmin()->getSubAdminsGroups($user);
245
-		$groups = [];
246
-		foreach ($subAdminGroups as $key => $group) {
247
-			$groups[] = $group->getGID();
248
-		}
249
-
250
-		return $groups;
251
-	}
252
-
253
-	/**
254
-	 * @param IUser $user
255
-	 * @return Provisioning_APIUserDetailsQuota
256
-	 * @throws OCSException
257
-	 */
258
-	protected function fillStorageInfo(IUser $user): array {
259
-		$includeExternal = $this->config->getSystemValueBool('quota_include_external_storage');
260
-		$userId = $user->getUID();
261
-
262
-		$quota = $user->getQuota();
263
-		if ($quota === 'none') {
264
-			$quota = FileInfo::SPACE_UNLIMITED;
265
-		} else {
266
-			$quota = Util::computerFileSize($quota);
267
-			if ($quota === false) {
268
-				$quota = FileInfo::SPACE_UNLIMITED;
269
-			}
270
-		}
271
-
272
-		try {
273
-			if ($includeExternal) {
274
-				\OC_Util::tearDownFS();
275
-				\OC_Util::setupFS($user->getUID());
276
-				$storage = \OC_Helper::getStorageInfo('/', null, true, false);
277
-				$data = [
278
-					'free' => $storage['free'],
279
-					'used' => $storage['used'],
280
-					'total' => $storage['total'],
281
-					'relative' => $storage['relative'],
282
-					self::USER_FIELD_QUOTA => $storage['quota'],
283
-				];
284
-			} else {
285
-				$userFileInfo = $this->rootFolder->getUserFolder($userId)->getStorage()->getCache()->get('');
286
-				$used = $userFileInfo->getSize();
287
-
288
-				if ($quota > 0) {
289
-					// prevent division by zero or error codes (negative values)
290
-					$relative = round(($used / $quota) * 10000) / 100;
291
-					$free = $quota - $used;
292
-					$total = $quota;
293
-				} else {
294
-					$relative = 0;
295
-					$free = FileInfo::SPACE_UNLIMITED;
296
-					$total = FileInfo::SPACE_UNLIMITED;
297
-				}
298
-
299
-				$data = [
300
-					'free' => $free,
301
-					'used' => $used,
302
-					'total' => $total,
303
-					'relative' => $relative,
304
-					self::USER_FIELD_QUOTA => $quota,
305
-				];
306
-			}
307
-		} catch (NotFoundException $ex) {
308
-			$data = [
309
-				self::USER_FIELD_QUOTA => $quota >= 0 ? $quota : 'none',
310
-				'used' => 0
311
-			];
312
-		} catch (\Exception $e) {
313
-			Server::get(\Psr\Log\LoggerInterface::class)->error(
314
-				'Could not load storage info for {user}',
315
-				[
316
-					'app' => 'provisioning_api',
317
-					'user' => $userId,
318
-					'exception' => $e,
319
-				]
320
-			);
321
-			return [];
322
-		}
323
-		return $data;
324
-	}
41
+    public const SCOPE_SUFFIX = 'Scope';
42
+
43
+    public const USER_FIELD_DISPLAYNAME = 'display';
44
+    public const USER_FIELD_LANGUAGE = 'language';
45
+    public const USER_FIELD_LOCALE = 'locale';
46
+    public const USER_FIELD_FIRST_DAY_OF_WEEK = 'first_day_of_week';
47
+    public const USER_FIELD_PASSWORD = 'password';
48
+    public const USER_FIELD_QUOTA = 'quota';
49
+    public const USER_FIELD_MANAGER = 'manager';
50
+    public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email';
51
+
52
+    public function __construct(
53
+        string $appName,
54
+        IRequest $request,
55
+        protected IUserManager $userManager,
56
+        protected IConfig $config,
57
+        protected GroupManager $groupManager,
58
+        protected IUserSession $userSession,
59
+        protected IAccountManager $accountManager,
60
+        protected ISubAdmin $subAdminManager,
61
+        protected IFactory $l10nFactory,
62
+        protected IRootFolder $rootFolder,
63
+    ) {
64
+        parent::__construct($appName, $request);
65
+    }
66
+
67
+    /**
68
+     * creates a array with all user data
69
+     *
70
+     * @param string $userId
71
+     * @param bool $includeScopes
72
+     * @return Provisioning_APIUserDetails|null
73
+     * @throws NotFoundException
74
+     * @throws OCSException
75
+     * @throws OCSNotFoundException
76
+     */
77
+    protected function getUserData(string $userId, bool $includeScopes = false): ?array {
78
+        $currentLoggedInUser = $this->userSession->getUser();
79
+        assert($currentLoggedInUser !== null, 'No user logged in');
80
+
81
+        $data = [];
82
+
83
+        // Check if the target user exists
84
+        $targetUserObject = $this->userManager->get($userId);
85
+        if ($targetUserObject === null) {
86
+            throw new OCSNotFoundException('User does not exist');
87
+        }
88
+
89
+        $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
90
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
91
+        if ($isAdmin
92
+            || $isDelegatedAdmin
93
+            || $this->groupManager->getSubAdmin()->isUserAccessible($currentLoggedInUser, $targetUserObject)) {
94
+            $data['enabled'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'enabled', 'true') === 'true';
95
+        } else {
96
+            // Check they are looking up themselves
97
+            if ($currentLoggedInUser->getUID() !== $targetUserObject->getUID()) {
98
+                return null;
99
+            }
100
+        }
101
+
102
+        // Get groups data
103
+        $userAccount = $this->accountManager->getAccount($targetUserObject);
104
+        $groups = $this->groupManager->getUserGroups($targetUserObject);
105
+        $gids = [];
106
+        foreach ($groups as $group) {
107
+            $gids[] = $group->getGID();
108
+        }
109
+
110
+        if ($isAdmin || $isDelegatedAdmin) {
111
+            try {
112
+                # might be thrown by LDAP due to handling of users disappears
113
+                # from the external source (reasons unknown to us)
114
+                # cf. https://github.com/nextcloud/server/issues/12991
115
+                $data['storageLocation'] = $targetUserObject->getHome();
116
+            } catch (NoUserException $e) {
117
+                throw new OCSNotFoundException($e->getMessage(), $e);
118
+            }
119
+        }
120
+
121
+        // Find the data
122
+        $data['id'] = $targetUserObject->getUID();
123
+        $data['firstLoginTimestamp'] = $targetUserObject->getFirstLogin();
124
+        $data['lastLoginTimestamp'] = $targetUserObject->getLastLogin();
125
+        $data['lastLogin'] = $targetUserObject->getLastLogin() * 1000;
126
+        $data['backend'] = $targetUserObject->getBackendClassName();
127
+        $data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID());
128
+        $data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject);
129
+        $managers = $this->getManagers($targetUserObject);
130
+        $data[self::USER_FIELD_MANAGER] = empty($managers) ? '' : $managers[0];
131
+
132
+        try {
133
+            if ($includeScopes) {
134
+                $data[IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_AVATAR)->getScope();
135
+            }
136
+
137
+            $data[IAccountManager::PROPERTY_EMAIL] = $targetUserObject->getSystemEMailAddress();
138
+            if ($includeScopes) {
139
+                $data[IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope();
140
+            }
141
+
142
+            $additionalEmails = $additionalEmailScopes = [];
143
+            $emailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
144
+            foreach ($emailCollection->getProperties() as $property) {
145
+                $email = mb_strtolower(trim($property->getValue()));
146
+                $additionalEmails[] = $email;
147
+                if ($includeScopes) {
148
+                    $additionalEmailScopes[] = $property->getScope();
149
+                }
150
+            }
151
+            $data[IAccountManager::COLLECTION_EMAIL] = $additionalEmails;
152
+            if ($includeScopes) {
153
+                $data[IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX] = $additionalEmailScopes;
154
+            }
155
+
156
+            $data[IAccountManager::PROPERTY_DISPLAYNAME] = $targetUserObject->getDisplayName();
157
+            $data[IAccountManager::PROPERTY_DISPLAYNAME_LEGACY] = $data[IAccountManager::PROPERTY_DISPLAYNAME];
158
+            if ($includeScopes) {
159
+                $data[IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX] = $userAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope();
160
+            }
161
+
162
+            foreach ([
163
+                IAccountManager::PROPERTY_PHONE,
164
+                IAccountManager::PROPERTY_ADDRESS,
165
+                IAccountManager::PROPERTY_WEBSITE,
166
+                IAccountManager::PROPERTY_TWITTER,
167
+                IAccountManager::PROPERTY_BLUESKY,
168
+                IAccountManager::PROPERTY_FEDIVERSE,
169
+                IAccountManager::PROPERTY_ORGANISATION,
170
+                IAccountManager::PROPERTY_ROLE,
171
+                IAccountManager::PROPERTY_HEADLINE,
172
+                IAccountManager::PROPERTY_BIOGRAPHY,
173
+                IAccountManager::PROPERTY_PROFILE_ENABLED,
174
+                IAccountManager::PROPERTY_PRONOUNS,
175
+            ] as $propertyName) {
176
+                $property = $userAccount->getProperty($propertyName);
177
+                $data[$propertyName] = $property->getValue();
178
+                if ($includeScopes) {
179
+                    $data[$propertyName . self::SCOPE_SUFFIX] = $property->getScope();
180
+                }
181
+            }
182
+        } catch (PropertyDoesNotExistException $e) {
183
+            // hard coded properties should exist
184
+            throw new OCSException($e->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR, $e);
185
+        }
186
+
187
+        $data['groups'] = $gids;
188
+        $data[self::USER_FIELD_LANGUAGE] = $this->l10nFactory->getUserLanguage($targetUserObject);
189
+        $data[self::USER_FIELD_LOCALE] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale');
190
+        $data[self::USER_FIELD_NOTIFICATION_EMAIL] = $targetUserObject->getPrimaryEMailAddress();
191
+
192
+        $backend = $targetUserObject->getBackend();
193
+        $data['backendCapabilities'] = [
194
+            'setDisplayName' => $backend instanceof ISetDisplayNameBackend || $backend->implementsActions(Backend::SET_DISPLAYNAME),
195
+            'setPassword' => $backend instanceof ISetPasswordBackend || $backend->implementsActions(Backend::SET_PASSWORD),
196
+        ];
197
+
198
+        return $data;
199
+    }
200
+
201
+    /**
202
+     * @return string[]
203
+     */
204
+    protected function getManagers(IUser $user): array {
205
+        $currentLoggedInUser = $this->userSession->getUser();
206
+
207
+        $managerUids = $user->getManagerUids();
208
+        if ($this->groupManager->isAdmin($currentLoggedInUser->getUID()) || $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())) {
209
+            return $managerUids;
210
+        }
211
+
212
+        if ($this->subAdminManager->isSubAdmin($currentLoggedInUser)) {
213
+            $accessibleManagerUids = array_values(array_filter(
214
+                $managerUids,
215
+                function (string $managerUid) use ($currentLoggedInUser) {
216
+                    $manager = $this->userManager->get($managerUid);
217
+                    if (!($manager instanceof IUser)) {
218
+                        return false;
219
+                    }
220
+                    return $this->subAdminManager->isUserAccessible($currentLoggedInUser, $manager);
221
+                },
222
+            ));
223
+            return $accessibleManagerUids;
224
+        }
225
+
226
+        return [];
227
+    }
228
+
229
+    /**
230
+     * Get the groups a user is a subadmin of
231
+     *
232
+     * @param string $userId
233
+     * @return list<string>
234
+     * @throws OCSException
235
+     */
236
+    protected function getUserSubAdminGroupsData(string $userId): array {
237
+        $user = $this->userManager->get($userId);
238
+        // Check if the user exists
239
+        if ($user === null) {
240
+            throw new OCSNotFoundException('User does not exist');
241
+        }
242
+
243
+        // Get the subadmin groups
244
+        $subAdminGroups = $this->groupManager->getSubAdmin()->getSubAdminsGroups($user);
245
+        $groups = [];
246
+        foreach ($subAdminGroups as $key => $group) {
247
+            $groups[] = $group->getGID();
248
+        }
249
+
250
+        return $groups;
251
+    }
252
+
253
+    /**
254
+     * @param IUser $user
255
+     * @return Provisioning_APIUserDetailsQuota
256
+     * @throws OCSException
257
+     */
258
+    protected function fillStorageInfo(IUser $user): array {
259
+        $includeExternal = $this->config->getSystemValueBool('quota_include_external_storage');
260
+        $userId = $user->getUID();
261
+
262
+        $quota = $user->getQuota();
263
+        if ($quota === 'none') {
264
+            $quota = FileInfo::SPACE_UNLIMITED;
265
+        } else {
266
+            $quota = Util::computerFileSize($quota);
267
+            if ($quota === false) {
268
+                $quota = FileInfo::SPACE_UNLIMITED;
269
+            }
270
+        }
271
+
272
+        try {
273
+            if ($includeExternal) {
274
+                \OC_Util::tearDownFS();
275
+                \OC_Util::setupFS($user->getUID());
276
+                $storage = \OC_Helper::getStorageInfo('/', null, true, false);
277
+                $data = [
278
+                    'free' => $storage['free'],
279
+                    'used' => $storage['used'],
280
+                    'total' => $storage['total'],
281
+                    'relative' => $storage['relative'],
282
+                    self::USER_FIELD_QUOTA => $storage['quota'],
283
+                ];
284
+            } else {
285
+                $userFileInfo = $this->rootFolder->getUserFolder($userId)->getStorage()->getCache()->get('');
286
+                $used = $userFileInfo->getSize();
287
+
288
+                if ($quota > 0) {
289
+                    // prevent division by zero or error codes (negative values)
290
+                    $relative = round(($used / $quota) * 10000) / 100;
291
+                    $free = $quota - $used;
292
+                    $total = $quota;
293
+                } else {
294
+                    $relative = 0;
295
+                    $free = FileInfo::SPACE_UNLIMITED;
296
+                    $total = FileInfo::SPACE_UNLIMITED;
297
+                }
298
+
299
+                $data = [
300
+                    'free' => $free,
301
+                    'used' => $used,
302
+                    'total' => $total,
303
+                    'relative' => $relative,
304
+                    self::USER_FIELD_QUOTA => $quota,
305
+                ];
306
+            }
307
+        } catch (NotFoundException $ex) {
308
+            $data = [
309
+                self::USER_FIELD_QUOTA => $quota >= 0 ? $quota : 'none',
310
+                'used' => 0
311
+            ];
312
+        } catch (\Exception $e) {
313
+            Server::get(\Psr\Log\LoggerInterface::class)->error(
314
+                'Could not load storage info for {user}',
315
+                [
316
+                    'app' => 'provisioning_api',
317
+                    'user' => $userId,
318
+                    'exception' => $e,
319
+                ]
320
+            );
321
+            return [];
322
+        }
323
+        return $data;
324
+    }
325 325
 }
Please login to merge, or discard this patch.
apps/provisioning_api/lib/Controller/UsersController.php 1 patch
Indentation   +1755 added lines, -1755 removed lines patch added patch discarded remove patch
@@ -59,1759 +59,1759 @@
 block discarded – undo
59 59
  */
60 60
 class UsersController extends AUserDataOCSController {
61 61
 
62
-	private IL10N $l10n;
63
-
64
-	public function __construct(
65
-		string $appName,
66
-		IRequest $request,
67
-		IUserManager $userManager,
68
-		IConfig $config,
69
-		IGroupManager $groupManager,
70
-		IUserSession $userSession,
71
-		IAccountManager $accountManager,
72
-		ISubAdmin $subAdminManager,
73
-		IFactory $l10nFactory,
74
-		IRootFolder $rootFolder,
75
-		private IURLGenerator $urlGenerator,
76
-		private LoggerInterface $logger,
77
-		private NewUserMailHelper $newUserMailHelper,
78
-		private ISecureRandom $secureRandom,
79
-		private RemoteWipe $remoteWipe,
80
-		private KnownUserService $knownUserService,
81
-		private IEventDispatcher $eventDispatcher,
82
-		private IPhoneNumberUtil $phoneNumberUtil,
83
-		private IAppManager $appManager,
84
-	) {
85
-		parent::__construct(
86
-			$appName,
87
-			$request,
88
-			$userManager,
89
-			$config,
90
-			$groupManager,
91
-			$userSession,
92
-			$accountManager,
93
-			$subAdminManager,
94
-			$l10nFactory,
95
-			$rootFolder,
96
-		);
97
-
98
-		$this->l10n = $l10nFactory->get($appName);
99
-	}
100
-
101
-	/**
102
-	 * Get a list of users
103
-	 *
104
-	 * @param string $search Text to search for
105
-	 * @param int|null $limit Limit the amount of groups returned
106
-	 * @param int $offset Offset for searching for groups
107
-	 * @return DataResponse<Http::STATUS_OK, array{users: list<string>}, array{}>
108
-	 *
109
-	 * 200: Users returned
110
-	 */
111
-	#[NoAdminRequired]
112
-	public function getUsers(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
113
-		$user = $this->userSession->getUser();
114
-		$users = [];
115
-
116
-		// Admin? Or SubAdmin?
117
-		$uid = $user->getUID();
118
-		$subAdminManager = $this->groupManager->getSubAdmin();
119
-		$isAdmin = $this->groupManager->isAdmin($uid);
120
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
121
-		if ($isAdmin || $isDelegatedAdmin) {
122
-			$users = $this->userManager->search($search, $limit, $offset);
123
-		} elseif ($subAdminManager->isSubAdmin($user)) {
124
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
125
-			foreach ($subAdminOfGroups as $key => $group) {
126
-				$subAdminOfGroups[$key] = $group->getGID();
127
-			}
128
-
129
-			$users = [];
130
-			foreach ($subAdminOfGroups as $group) {
131
-				$users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
132
-			}
133
-		}
134
-
135
-		/** @var list<string> $users */
136
-		$users = array_keys($users);
137
-
138
-		return new DataResponse([
139
-			'users' => $users
140
-		]);
141
-	}
142
-
143
-	/**
144
-	 * Get a list of users and their details
145
-	 *
146
-	 * @param string $search Text to search for
147
-	 * @param int|null $limit Limit the amount of groups returned
148
-	 * @param int $offset Offset for searching for groups
149
-	 * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
150
-	 *
151
-	 * 200: Users details returned
152
-	 */
153
-	#[NoAdminRequired]
154
-	public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
155
-		$currentUser = $this->userSession->getUser();
156
-		$users = [];
157
-
158
-		// Admin? Or SubAdmin?
159
-		$uid = $currentUser->getUID();
160
-		$subAdminManager = $this->groupManager->getSubAdmin();
161
-		$isAdmin = $this->groupManager->isAdmin($uid);
162
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
163
-		if ($isAdmin || $isDelegatedAdmin) {
164
-			$users = $this->userManager->search($search, $limit, $offset);
165
-			$users = array_keys($users);
166
-		} elseif ($subAdminManager->isSubAdmin($currentUser)) {
167
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
168
-			foreach ($subAdminOfGroups as $key => $group) {
169
-				$subAdminOfGroups[$key] = $group->getGID();
170
-			}
171
-
172
-			$users = [];
173
-			foreach ($subAdminOfGroups as $group) {
174
-				$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
175
-			}
176
-			$users = array_merge(...$users);
177
-		}
178
-
179
-		$usersDetails = [];
180
-		foreach ($users as $userId) {
181
-			$userId = (string)$userId;
182
-			try {
183
-				$userData = $this->getUserData($userId);
184
-			} catch (OCSNotFoundException $e) {
185
-				// We still want to return all other accounts, but this one was removed from the backends
186
-				// yet they are still in our database. Might be a LDAP remnant.
187
-				$userData = null;
188
-				$this->logger->warning('Found one enabled account that is removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
189
-			}
190
-			// Do not insert empty entry
191
-			if ($userData !== null) {
192
-				$usersDetails[$userId] = $userData;
193
-			} else {
194
-				// Logged user does not have permissions to see this user
195
-				// only showing its id
196
-				$usersDetails[$userId] = ['id' => $userId];
197
-			}
198
-		}
199
-
200
-		return new DataResponse([
201
-			'users' => $usersDetails
202
-		]);
203
-	}
204
-
205
-	/**
206
-	 * Get the list of disabled users and their details
207
-	 *
208
-	 * @param string $search Text to search for
209
-	 * @param ?int $limit Limit the amount of users returned
210
-	 * @param int $offset Offset
211
-	 * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
212
-	 *
213
-	 * 200: Disabled users details returned
214
-	 */
215
-	#[NoAdminRequired]
216
-	public function getDisabledUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
217
-		$currentUser = $this->userSession->getUser();
218
-		if ($currentUser === null) {
219
-			return new DataResponse(['users' => []]);
220
-		}
221
-		if ($limit !== null && $limit < 0) {
222
-			throw new InvalidArgumentException("Invalid limit value: $limit");
223
-		}
224
-		if ($offset < 0) {
225
-			throw new InvalidArgumentException("Invalid offset value: $offset");
226
-		}
227
-
228
-		$users = [];
229
-
230
-		// Admin? Or SubAdmin?
231
-		$uid = $currentUser->getUID();
232
-		$subAdminManager = $this->groupManager->getSubAdmin();
233
-		$isAdmin = $this->groupManager->isAdmin($uid);
234
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
235
-		if ($isAdmin || $isDelegatedAdmin) {
236
-			$users = $this->userManager->getDisabledUsers($limit, $offset, $search);
237
-			$users = array_map(fn (IUser $user): string => $user->getUID(), $users);
238
-		} elseif ($subAdminManager->isSubAdmin($currentUser)) {
239
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
240
-
241
-			$users = [];
242
-			/* We have to handle offset ourselve for correctness */
243
-			$tempLimit = ($limit === null ? null : $limit + $offset);
244
-			foreach ($subAdminOfGroups as $group) {
245
-				$users = array_unique(array_merge(
246
-					$users,
247
-					array_map(
248
-						fn (IUser $user): string => $user->getUID(),
249
-						array_filter(
250
-							$group->searchUsers($search),
251
-							fn (IUser $user): bool => !$user->isEnabled()
252
-						)
253
-					)
254
-				));
255
-				if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
256
-					break;
257
-				}
258
-			}
259
-			$users = array_slice($users, $offset, $limit);
260
-		}
261
-
262
-		$usersDetails = [];
263
-		foreach ($users as $userId) {
264
-			try {
265
-				$userData = $this->getUserData($userId);
266
-			} catch (OCSNotFoundException $e) {
267
-				// We still want to return all other accounts, but this one was removed from the backends
268
-				// yet they are still in our database. Might be a LDAP remnant.
269
-				$userData = null;
270
-				$this->logger->warning('Found one disabled account that was removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
271
-			}
272
-			// Do not insert empty entry
273
-			if ($userData !== null) {
274
-				$usersDetails[$userId] = $userData;
275
-			} else {
276
-				// Currently logged in user does not have permissions to see this user
277
-				// only showing its id
278
-				$usersDetails[$userId] = ['id' => $userId];
279
-			}
280
-		}
281
-
282
-		return new DataResponse([
283
-			'users' => $usersDetails
284
-		]);
285
-	}
286
-
287
-	/**
288
-	 * Gets the list of users sorted by lastLogin, from most recent to least recent
289
-	 *
290
-	 * @param string $search Text to search for
291
-	 * @param ?int $limit Limit the amount of users returned
292
-	 * @param int $offset Offset
293
-	 * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
294
-	 *
295
-	 * 200: Users details returned based on last logged in information
296
-	 */
297
-	#[AuthorizedAdminSetting(settings:Users::class)]
298
-	public function getLastLoggedInUsers(string $search = '',
299
-		?int $limit = null,
300
-		int $offset = 0,
301
-	): DataResponse {
302
-		$currentUser = $this->userSession->getUser();
303
-		if ($currentUser === null) {
304
-			return new DataResponse(['users' => []]);
305
-		}
306
-		if ($limit !== null && $limit < 0) {
307
-			throw new InvalidArgumentException("Invalid limit value: $limit");
308
-		}
309
-		if ($offset < 0) {
310
-			throw new InvalidArgumentException("Invalid offset value: $offset");
311
-		}
312
-
313
-		$users = [];
314
-
315
-		// For Admin alone user sorting based on lastLogin. For sub admin and groups this is not supported
316
-		$users = $this->userManager->getLastLoggedInUsers($limit, $offset, $search);
317
-
318
-		$usersDetails = [];
319
-		foreach ($users as $userId) {
320
-			try {
321
-				$userData = $this->getUserData($userId);
322
-			} catch (OCSNotFoundException $e) {
323
-				// We still want to return all other accounts, but this one was removed from the backends
324
-				// yet they are still in our database. Might be a LDAP remnant.
325
-				$userData = null;
326
-				$this->logger->warning('Found one account that was removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
327
-			}
328
-			// Do not insert empty entry
329
-			if ($userData !== null) {
330
-				$usersDetails[$userId] = $userData;
331
-			} else {
332
-				// Currently logged-in user does not have permissions to see this user
333
-				// only showing its id
334
-				$usersDetails[$userId] = ['id' => $userId];
335
-			}
336
-		}
337
-
338
-		return new DataResponse([
339
-			'users' => $usersDetails
340
-		]);
341
-	}
342
-
343
-
344
-
345
-	/**
346
-	 * @NoSubAdminRequired
347
-	 *
348
-	 * Search users by their phone numbers
349
-	 *
350
-	 * @param string $location Location of the phone number (for country code)
351
-	 * @param array<string, list<string>> $search Phone numbers to search for
352
-	 * @return DataResponse<Http::STATUS_OK, array<string, string>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, list<empty>, array{}>
353
-	 *
354
-	 * 200: Users returned
355
-	 * 400: Invalid location
356
-	 */
357
-	#[NoAdminRequired]
358
-	public function searchByPhoneNumbers(string $location, array $search): DataResponse {
359
-		if ($this->phoneNumberUtil->getCountryCodeForRegion($location) === null) {
360
-			// Not a valid region code
361
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
362
-		}
363
-
364
-		/** @var IUser $user */
365
-		$user = $this->userSession->getUser();
366
-		$knownTo = $user->getUID();
367
-		$defaultPhoneRegion = $this->config->getSystemValueString('default_phone_region');
368
-
369
-		$normalizedNumberToKey = [];
370
-		foreach ($search as $key => $phoneNumbers) {
371
-			foreach ($phoneNumbers as $phone) {
372
-				$normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $location);
373
-				if ($normalizedNumber !== null) {
374
-					$normalizedNumberToKey[$normalizedNumber] = (string)$key;
375
-				}
376
-
377
-				if ($defaultPhoneRegion !== '' && $defaultPhoneRegion !== $location && str_starts_with($phone, '0')) {
378
-					// If the number has a leading zero (no country code),
379
-					// we also check the default phone region of the instance,
380
-					// when it's different to the user's given region.
381
-					$normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $defaultPhoneRegion);
382
-					if ($normalizedNumber !== null) {
383
-						$normalizedNumberToKey[$normalizedNumber] = (string)$key;
384
-					}
385
-				}
386
-			}
387
-		}
388
-
389
-		$phoneNumbers = array_keys($normalizedNumberToKey);
390
-
391
-		if (empty($phoneNumbers)) {
392
-			return new DataResponse();
393
-		}
394
-
395
-		// Cleanup all previous entries and only allow new matches
396
-		$this->knownUserService->deleteKnownTo($knownTo);
397
-
398
-		$userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers);
399
-
400
-		if (empty($userMatches)) {
401
-			return new DataResponse();
402
-		}
403
-
404
-		$cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
405
-		if (strpos($cloudUrl, 'http://') === 0) {
406
-			$cloudUrl = substr($cloudUrl, strlen('http://'));
407
-		} elseif (strpos($cloudUrl, 'https://') === 0) {
408
-			$cloudUrl = substr($cloudUrl, strlen('https://'));
409
-		}
410
-
411
-		$matches = [];
412
-		foreach ($userMatches as $phone => $userId) {
413
-			// Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
414
-			$matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
415
-			$this->knownUserService->storeIsKnownToUser($knownTo, $userId);
416
-		}
417
-
418
-		return new DataResponse($matches);
419
-	}
420
-
421
-	/**
422
-	 * @throws OCSException
423
-	 */
424
-	private function createNewUserId(): string {
425
-		$attempts = 0;
426
-		do {
427
-			$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
428
-			if (!$this->userManager->userExists($uidCandidate)) {
429
-				return $uidCandidate;
430
-			}
431
-			$attempts++;
432
-		} while ($attempts < 10);
433
-		throw new OCSException($this->l10n->t('Could not create non-existing user ID'), 111);
434
-	}
435
-
436
-	/**
437
-	 * Create a new user
438
-	 *
439
-	 * @param string $userid ID of the user
440
-	 * @param string $password Password of the user
441
-	 * @param string $displayName Display name of the user
442
-	 * @param string $email Email of the user
443
-	 * @param list<string> $groups Groups of the user
444
-	 * @param list<string> $subadmin Groups where the user is subadmin
445
-	 * @param string $quota Quota of the user
446
-	 * @param string $language Language of the user
447
-	 * @param ?string $manager Manager of the user
448
-	 * @return DataResponse<Http::STATUS_OK, array{id: string}, array{}>
449
-	 * @throws OCSException
450
-	 * @throws OCSForbiddenException Missing permissions to make user subadmin
451
-	 *
452
-	 * 200: User added successfully
453
-	 */
454
-	#[PasswordConfirmationRequired]
455
-	#[NoAdminRequired]
456
-	public function addUser(
457
-		string $userid,
458
-		string $password = '',
459
-		string $displayName = '',
460
-		string $email = '',
461
-		array $groups = [],
462
-		array $subadmin = [],
463
-		string $quota = '',
464
-		string $language = '',
465
-		?string $manager = null,
466
-	): DataResponse {
467
-		$user = $this->userSession->getUser();
468
-		$isAdmin = $this->groupManager->isAdmin($user->getUID());
469
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($user->getUID());
470
-		$subAdminManager = $this->groupManager->getSubAdmin();
471
-
472
-		if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
473
-			$userid = $this->createNewUserId();
474
-		}
475
-
476
-		if ($this->userManager->userExists($userid)) {
477
-			$this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
478
-			throw new OCSException($this->l10n->t('User already exists'), 102);
479
-		}
480
-
481
-		if ($groups !== []) {
482
-			foreach ($groups as $group) {
483
-				if (!$this->groupManager->groupExists($group)) {
484
-					throw new OCSException($this->l10n->t('Group %1$s does not exist', [$group]), 104);
485
-				}
486
-				if (!$isAdmin && !($isDelegatedAdmin && $group !== 'admin') && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
487
-					throw new OCSException($this->l10n->t('Insufficient privileges for group %1$s', [$group]), 105);
488
-				}
489
-			}
490
-		} else {
491
-			if (!$isAdmin && !$isDelegatedAdmin) {
492
-				throw new OCSException($this->l10n->t('No group specified (required for sub-admins)'), 106);
493
-			}
494
-		}
495
-
496
-		$subadminGroups = [];
497
-		if ($subadmin !== []) {
498
-			foreach ($subadmin as $groupid) {
499
-				$group = $this->groupManager->get($groupid);
500
-				// Check if group exists
501
-				if ($group === null) {
502
-					throw new OCSException($this->l10n->t('Sub-admin group does not exist'), 109);
503
-				}
504
-				// Check if trying to make subadmin of admin group
505
-				if ($group->getGID() === 'admin') {
506
-					throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103);
507
-				}
508
-				// Check if has permission to promote subadmins
509
-				if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin && !$isDelegatedAdmin) {
510
-					throw new OCSForbiddenException($this->l10n->t('No permissions to promote sub-admins'));
511
-				}
512
-				$subadminGroups[] = $group;
513
-			}
514
-		}
515
-
516
-		$generatePasswordResetToken = false;
517
-		if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
518
-			throw new OCSException($this->l10n->t('Invalid password value'), 101);
519
-		}
520
-		if ($password === '') {
521
-			if ($email === '') {
522
-				throw new OCSException($this->l10n->t('An email address is required, to send a password link to the user.'), 108);
523
-			}
524
-
525
-			$passwordEvent = new GenerateSecurePasswordEvent();
526
-			$this->eventDispatcher->dispatchTyped($passwordEvent);
527
-
528
-			$password = $passwordEvent->getPassword();
529
-			if ($password === null) {
530
-				// Fallback: ensure to pass password_policy in any case
531
-				$password = $this->secureRandom->generate(10)
532
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_UPPER)
533
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_LOWER)
534
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_DIGITS)
535
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_SYMBOLS);
536
-			}
537
-			$generatePasswordResetToken = true;
538
-		}
539
-
540
-		$email = mb_strtolower(trim($email));
541
-		if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
542
-			throw new OCSException($this->l10n->t('Required email address was not provided'), 110);
543
-		}
544
-
545
-		// Create the user
546
-		try {
547
-			$newUser = $this->userManager->createUser($userid, $password);
548
-			if (!$newUser instanceof IUser) {
549
-				// If the user is not an instance of IUser, it means the user creation failed
550
-				$this->logger->error('Failed addUser attempt: User creation failed.', ['app' => 'ocs_api']);
551
-				throw new OCSException($this->l10n->t('User creation failed'), 111);
552
-			}
553
-
554
-			$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
555
-			foreach ($groups as $group) {
556
-				$this->groupManager->get($group)->addUser($newUser);
557
-				$this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
558
-			}
559
-			foreach ($subadminGroups as $group) {
560
-				$subAdminManager->createSubAdmin($newUser, $group);
561
-			}
562
-
563
-			if ($displayName !== '') {
564
-				try {
565
-					$this->editUser($userid, self::USER_FIELD_DISPLAYNAME, $displayName);
566
-				} catch (OCSException $e) {
567
-					if ($newUser instanceof IUser) {
568
-						$newUser->delete();
569
-					}
570
-					throw $e;
571
-				}
572
-			}
573
-
574
-			if ($quota !== '') {
575
-				$this->editUser($userid, self::USER_FIELD_QUOTA, $quota);
576
-			}
577
-
578
-			if ($language !== '') {
579
-				$this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
580
-			}
581
-
582
-			/**
583
-			 * null -> nothing sent
584
-			 * '' -> unset manager
585
-			 * else -> set manager
586
-			 */
587
-			if ($manager !== null) {
588
-				$this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
589
-			}
590
-
591
-			// Send new user mail only if a mail is set
592
-			if ($email !== '') {
593
-				$newUser->setSystemEMailAddress($email);
594
-				if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
595
-					try {
596
-						$emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
597
-						$this->newUserMailHelper->sendMail($newUser, $emailTemplate);
598
-					} catch (\Exception $e) {
599
-						// Mail could be failing hard or just be plain not configured
600
-						// Logging error as it is the hardest of the two
601
-						$this->logger->error(
602
-							"Unable to send the invitation mail to $email",
603
-							[
604
-								'app' => 'ocs_api',
605
-								'exception' => $e,
606
-							]
607
-						);
608
-					}
609
-				}
610
-			}
611
-
612
-			return new DataResponse(['id' => $userid]);
613
-		} catch (HintException $e) {
614
-			$this->logger->warning(
615
-				'Failed addUser attempt with hint exception.',
616
-				[
617
-					'app' => 'ocs_api',
618
-					'exception' => $e,
619
-				]
620
-			);
621
-			throw new OCSException($e->getHint(), 107);
622
-		} catch (OCSException $e) {
623
-			$this->logger->warning(
624
-				'Failed addUser attempt with ocs exception.',
625
-				[
626
-					'app' => 'ocs_api',
627
-					'exception' => $e,
628
-				]
629
-			);
630
-			throw $e;
631
-		} catch (InvalidArgumentException $e) {
632
-			$this->logger->error(
633
-				'Failed addUser attempt with invalid argument exception.',
634
-				[
635
-					'app' => 'ocs_api',
636
-					'exception' => $e,
637
-				]
638
-			);
639
-			throw new OCSException($e->getMessage(), 101);
640
-		} catch (\Exception $e) {
641
-			$this->logger->error(
642
-				'Failed addUser attempt with exception.',
643
-				[
644
-					'app' => 'ocs_api',
645
-					'exception' => $e
646
-				]
647
-			);
648
-			throw new OCSException('Bad request', 101);
649
-		}
650
-	}
651
-
652
-	/**
653
-	 * @NoSubAdminRequired
654
-	 *
655
-	 * Get the details of a user
656
-	 *
657
-	 * @param string $userId ID of the user
658
-	 * @return DataResponse<Http::STATUS_OK, Provisioning_APIUserDetails, array{}>
659
-	 * @throws OCSException
660
-	 *
661
-	 * 200: User returned
662
-	 */
663
-	#[NoAdminRequired]
664
-	public function getUser(string $userId): DataResponse {
665
-		$includeScopes = false;
666
-		$currentUser = $this->userSession->getUser();
667
-		if ($currentUser && $currentUser->getUID() === $userId) {
668
-			$includeScopes = true;
669
-		}
670
-
671
-		$data = $this->getUserData($userId, $includeScopes);
672
-		// getUserData returns null if not enough permissions
673
-		if ($data === null) {
674
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
675
-		}
676
-		return new DataResponse($data);
677
-	}
678
-
679
-	/**
680
-	 * @NoSubAdminRequired
681
-	 *
682
-	 * Get the details of the current user
683
-	 *
684
-	 * @return DataResponse<Http::STATUS_OK, Provisioning_APIUserDetails, array{}>
685
-	 * @throws OCSException
686
-	 *
687
-	 * 200: Current user returned
688
-	 */
689
-	#[NoAdminRequired]
690
-	public function getCurrentUser(): DataResponse {
691
-		$user = $this->userSession->getUser();
692
-		if ($user) {
693
-			/** @var Provisioning_APIUserDetails $data */
694
-			$data = $this->getUserData($user->getUID(), true);
695
-			return new DataResponse($data);
696
-		}
697
-
698
-		throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
699
-	}
700
-
701
-	/**
702
-	 * @NoSubAdminRequired
703
-	 *
704
-	 * Get a list of fields that are editable for the current user
705
-	 *
706
-	 * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
707
-	 * @throws OCSException
708
-	 *
709
-	 * 200: Editable fields returned
710
-	 */
711
-	#[NoAdminRequired]
712
-	public function getEditableFields(): DataResponse {
713
-		$currentLoggedInUser = $this->userSession->getUser();
714
-		if (!$currentLoggedInUser instanceof IUser) {
715
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
716
-		}
717
-
718
-		return $this->getEditableFieldsForUser($currentLoggedInUser->getUID());
719
-	}
720
-
721
-	/**
722
-	 * Get a list of enabled apps for the current user
723
-	 *
724
-	 * @return DataResponse<Http::STATUS_OK, array{apps: list<string>}, array{}>
725
-	 *
726
-	 * 200: Enabled apps returned
727
-	 */
728
-	#[NoAdminRequired]
729
-	public function getEnabledApps(): DataResponse {
730
-		$currentLoggedInUser = $this->userSession->getUser();
731
-		return new DataResponse(['apps' => $this->appManager->getEnabledAppsForUser($currentLoggedInUser)]);
732
-	}
733
-
734
-	/**
735
-	 * @NoSubAdminRequired
736
-	 *
737
-	 * Get a list of fields that are editable for a user
738
-	 *
739
-	 * @param string $userId ID of the user
740
-	 * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
741
-	 * @throws OCSException
742
-	 *
743
-	 * 200: Editable fields for user returned
744
-	 */
745
-	#[NoAdminRequired]
746
-	public function getEditableFieldsForUser(string $userId): DataResponse {
747
-		$currentLoggedInUser = $this->userSession->getUser();
748
-		if (!$currentLoggedInUser instanceof IUser) {
749
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
750
-		}
751
-
752
-		$permittedFields = [];
753
-
754
-		if ($userId !== $currentLoggedInUser->getUID()) {
755
-			$targetUser = $this->userManager->get($userId);
756
-			if (!$targetUser instanceof IUser) {
757
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
758
-			}
759
-
760
-			$subAdminManager = $this->groupManager->getSubAdmin();
761
-			$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
762
-			$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
763
-			if (
764
-				!($isAdmin || $isDelegatedAdmin)
765
-				&& !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
766
-			) {
767
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
768
-			}
769
-		} else {
770
-			$targetUser = $currentLoggedInUser;
771
-		}
772
-
773
-		$allowDisplayNameChange = $this->config->getSystemValue('allow_user_to_change_display_name', true);
774
-		if ($allowDisplayNameChange === true && (
775
-			$targetUser->getBackend() instanceof ISetDisplayNameBackend
776
-			|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
777
-		)) {
778
-			$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
779
-		}
780
-
781
-		// Fallback to display name value to avoid changing behavior with the new option.
782
-		if ($this->config->getSystemValue('allow_user_to_change_email', $allowDisplayNameChange)) {
783
-			$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
784
-		}
785
-
786
-		$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
787
-		$permittedFields[] = IAccountManager::PROPERTY_PHONE;
788
-		$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
789
-		$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
790
-		$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
791
-		$permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
792
-		$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
793
-		$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
794
-		$permittedFields[] = IAccountManager::PROPERTY_ROLE;
795
-		$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
796
-		$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
797
-		$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
798
-		$permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
799
-
800
-		return new DataResponse($permittedFields);
801
-	}
802
-
803
-	/**
804
-	 * @NoSubAdminRequired
805
-	 *
806
-	 * Update multiple values of the user's details
807
-	 *
808
-	 * @param string $userId ID of the user
809
-	 * @param string $collectionName Collection to update
810
-	 * @param string $key Key that will be updated
811
-	 * @param string $value New value for the key
812
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
813
-	 * @throws OCSException
814
-	 *
815
-	 * 200: User values edited successfully
816
-	 */
817
-	#[PasswordConfirmationRequired]
818
-	#[NoAdminRequired]
819
-	#[UserRateLimit(limit: 5, period: 60)]
820
-	public function editUserMultiValue(
821
-		string $userId,
822
-		string $collectionName,
823
-		string $key,
824
-		string $value,
825
-	): DataResponse {
826
-		$currentLoggedInUser = $this->userSession->getUser();
827
-		if ($currentLoggedInUser === null) {
828
-			throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
829
-		}
830
-
831
-		$targetUser = $this->userManager->get($userId);
832
-		if ($targetUser === null) {
833
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
834
-		}
835
-
836
-		$subAdminManager = $this->groupManager->getSubAdmin();
837
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
838
-		$isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID())
839
-			|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
840
-
841
-		$permittedFields = [];
842
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
843
-			// Editing self (display, email)
844
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
845
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
846
-		} else {
847
-			// Check if admin / subadmin
848
-			if ($isAdminOrSubadmin || $isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) {
849
-				// They have permissions over the user
850
-				$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
851
-			} else {
852
-				// No rights
853
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
854
-			}
855
-		}
856
-
857
-		// Check if permitted to edit this field
858
-		if (!in_array($collectionName, $permittedFields)) {
859
-			throw new OCSException('', 103);
860
-		}
861
-
862
-		switch ($collectionName) {
863
-			case IAccountManager::COLLECTION_EMAIL:
864
-				$userAccount = $this->accountManager->getAccount($targetUser);
865
-				$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
866
-				$mailCollection->removePropertyByValue($key);
867
-				if ($value !== '') {
868
-					$value = mb_strtolower(trim($value));
869
-					$mailCollection->addPropertyWithDefaults($value);
870
-					$property = $mailCollection->getPropertyByValue($key);
871
-					if ($isAdminOrSubadmin && $property) {
872
-						// admin set mails are auto-verified
873
-						$property->setLocallyVerified(IAccountManager::VERIFIED);
874
-					}
875
-				}
876
-				$this->accountManager->updateAccount($userAccount);
877
-				if ($value === '' && $key === $targetUser->getPrimaryEMailAddress()) {
878
-					$targetUser->setPrimaryEMailAddress('');
879
-				}
880
-				break;
881
-
882
-			case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX:
883
-				$userAccount = $this->accountManager->getAccount($targetUser);
884
-				$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
885
-				$targetProperty = null;
886
-				foreach ($mailCollection->getProperties() as $property) {
887
-					if ($property->getValue() === $key) {
888
-						$targetProperty = $property;
889
-						break;
890
-					}
891
-				}
892
-				if ($targetProperty instanceof IAccountProperty) {
893
-					try {
894
-						$targetProperty->setScope($value);
895
-						$this->accountManager->updateAccount($userAccount);
896
-					} catch (InvalidArgumentException $e) {
897
-						throw new OCSException('', 102);
898
-					}
899
-				} else {
900
-					throw new OCSException('', 102);
901
-				}
902
-				break;
903
-
904
-			default:
905
-				throw new OCSException('', 103);
906
-		}
907
-		return new DataResponse();
908
-	}
909
-
910
-	/**
911
-	 * @NoSubAdminRequired
912
-	 *
913
-	 * Update a value of the user's details
914
-	 *
915
-	 * @param string $userId ID of the user
916
-	 * @param string $key Key that will be updated
917
-	 * @param string $value New value for the key
918
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
919
-	 * @throws OCSException
920
-	 *
921
-	 * 200: User value edited successfully
922
-	 */
923
-	#[PasswordConfirmationRequired]
924
-	#[NoAdminRequired]
925
-	#[UserRateLimit(limit: 50, period: 600)]
926
-	public function editUser(string $userId, string $key, string $value): DataResponse {
927
-		$currentLoggedInUser = $this->userSession->getUser();
928
-
929
-		$targetUser = $this->userManager->get($userId);
930
-		if ($targetUser === null) {
931
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
932
-		}
933
-
934
-		$permittedFields = [];
935
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
936
-			$allowDisplayNameChange = $this->config->getSystemValue('allow_user_to_change_display_name', true);
937
-			if ($allowDisplayNameChange !== false && (
938
-				$targetUser->getBackend() instanceof ISetDisplayNameBackend
939
-				|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
940
-			)) {
941
-				$permittedFields[] = self::USER_FIELD_DISPLAYNAME;
942
-				$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
943
-			}
944
-
945
-			// Fallback to display name value to avoid changing behavior with the new option.
946
-			if ($this->config->getSystemValue('allow_user_to_change_email', $allowDisplayNameChange)) {
947
-				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
948
-			}
949
-
950
-			$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX;
951
-			$permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX;
952
-
953
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
954
-
955
-			$permittedFields[] = self::USER_FIELD_PASSWORD;
956
-			$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
957
-			if (
958
-				$this->config->getSystemValue('force_language', false) === false
959
-				|| $this->groupManager->isAdmin($currentLoggedInUser->getUID())
960
-				|| $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
961
-			) {
962
-				$permittedFields[] = self::USER_FIELD_LANGUAGE;
963
-			}
964
-
965
-			if (
966
-				$this->config->getSystemValue('force_locale', false) === false
967
-				|| $this->groupManager->isAdmin($currentLoggedInUser->getUID())
968
-				|| $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
969
-			) {
970
-				$permittedFields[] = self::USER_FIELD_LOCALE;
971
-				$permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
972
-			}
973
-
974
-			$permittedFields[] = IAccountManager::PROPERTY_PHONE;
975
-			$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
976
-			$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
977
-			$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
978
-			$permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
979
-			$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
980
-			$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
981
-			$permittedFields[] = IAccountManager::PROPERTY_ROLE;
982
-			$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
983
-			$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
984
-			$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
985
-			$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE;
986
-			$permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
987
-
988
-			$permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
989
-			$permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
990
-			$permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
991
-			$permittedFields[] = IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX;
992
-			$permittedFields[] = IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX;
993
-			$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX;
994
-			$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX;
995
-			$permittedFields[] = IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX;
996
-			$permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
997
-			$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
998
-			$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
999
-			$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX;
1000
-			$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
1001
-			$permittedFields[] = IAccountManager::PROPERTY_PRONOUNS . self::SCOPE_SUFFIX;
1002
-
1003
-			// If admin they can edit their own quota and manager
1004
-			$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1005
-			$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1006
-			if ($isAdmin || $isDelegatedAdmin) {
1007
-				$permittedFields[] = self::USER_FIELD_QUOTA;
1008
-				$permittedFields[] = self::USER_FIELD_MANAGER;
1009
-			}
1010
-		} else {
1011
-			// Check if admin / subadmin
1012
-			$subAdminManager = $this->groupManager->getSubAdmin();
1013
-			if (
1014
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())
1015
-				|| $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID()) && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')
1016
-				|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1017
-			) {
1018
-				// They have permissions over the user
1019
-				if (
1020
-					$targetUser->getBackend() instanceof ISetDisplayNameBackend
1021
-					|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
1022
-				) {
1023
-					$permittedFields[] = self::USER_FIELD_DISPLAYNAME;
1024
-					$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
1025
-				}
1026
-				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
1027
-				$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
1028
-				$permittedFields[] = self::USER_FIELD_PASSWORD;
1029
-				$permittedFields[] = self::USER_FIELD_LANGUAGE;
1030
-				$permittedFields[] = self::USER_FIELD_LOCALE;
1031
-				$permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
1032
-				$permittedFields[] = IAccountManager::PROPERTY_PHONE;
1033
-				$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
1034
-				$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
1035
-				$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
1036
-				$permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
1037
-				$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
1038
-				$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
1039
-				$permittedFields[] = IAccountManager::PROPERTY_ROLE;
1040
-				$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
1041
-				$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
1042
-				$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
1043
-				$permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
1044
-				$permittedFields[] = self::USER_FIELD_QUOTA;
1045
-				$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
1046
-				$permittedFields[] = self::USER_FIELD_MANAGER;
1047
-			} else {
1048
-				// No rights
1049
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1050
-			}
1051
-		}
1052
-		// Check if permitted to edit this field
1053
-		if (!in_array($key, $permittedFields)) {
1054
-			throw new OCSException('', 113);
1055
-		}
1056
-		// Process the edit
1057
-		switch ($key) {
1058
-			case self::USER_FIELD_DISPLAYNAME:
1059
-			case IAccountManager::PROPERTY_DISPLAYNAME:
1060
-				try {
1061
-					$targetUser->setDisplayName($value);
1062
-				} catch (InvalidArgumentException $e) {
1063
-					throw new OCSException($e->getMessage(), 101);
1064
-				}
1065
-				break;
1066
-			case self::USER_FIELD_QUOTA:
1067
-				$quota = $value;
1068
-				if ($quota !== 'none' && $quota !== 'default') {
1069
-					if (is_numeric($quota)) {
1070
-						$quota = (float)$quota;
1071
-					} else {
1072
-						$quota = Util::computerFileSize($quota);
1073
-					}
1074
-					if ($quota === false) {
1075
-						throw new OCSException($this->l10n->t('Invalid quota value: %1$s', [$value]), 101);
1076
-					}
1077
-					if ($quota === -1) {
1078
-						$quota = 'none';
1079
-					} else {
1080
-						$maxQuota = (int)$this->config->getAppValue('files', 'max_quota', '-1');
1081
-						if ($maxQuota !== -1 && $quota > $maxQuota) {
1082
-							throw new OCSException($this->l10n->t('Invalid quota value. %1$s is exceeding the maximum quota', [$value]), 101);
1083
-						}
1084
-						$quota = Util::humanFileSize($quota);
1085
-					}
1086
-				}
1087
-				// no else block because quota can be set to 'none' in previous if
1088
-				if ($quota === 'none') {
1089
-					$allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
1090
-					if (!$allowUnlimitedQuota) {
1091
-						throw new OCSException($this->l10n->t('Unlimited quota is forbidden on this instance'), 101);
1092
-					}
1093
-				}
1094
-				$targetUser->setQuota($quota);
1095
-				break;
1096
-			case self::USER_FIELD_MANAGER:
1097
-				$targetUser->setManagerUids([$value]);
1098
-				break;
1099
-			case self::USER_FIELD_PASSWORD:
1100
-				try {
1101
-					if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {
1102
-						throw new OCSException($this->l10n->t('Invalid password value'), 101);
1103
-					}
1104
-					if (!$targetUser->canChangePassword()) {
1105
-						throw new OCSException($this->l10n->t('Setting the password is not supported by the users backend'), 112);
1106
-					}
1107
-					$targetUser->setPassword($value);
1108
-				} catch (HintException $e) { // password policy error
1109
-					throw new OCSException($e->getHint(), 107);
1110
-				}
1111
-				break;
1112
-			case self::USER_FIELD_LANGUAGE:
1113
-				$languagesCodes = $this->l10nFactory->findAvailableLanguages();
1114
-				if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
1115
-					throw new OCSException($this->l10n->t('Invalid language'), 101);
1116
-				}
1117
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
1118
-				break;
1119
-			case self::USER_FIELD_LOCALE:
1120
-				if (!$this->l10nFactory->localeExists($value)) {
1121
-					throw new OCSException($this->l10n->t('Invalid locale'), 101);
1122
-				}
1123
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
1124
-				break;
1125
-			case self::USER_FIELD_FIRST_DAY_OF_WEEK:
1126
-				$intValue = (int)$value;
1127
-				if ($intValue < -1 || $intValue > 6) {
1128
-					throw new OCSException($this->l10n->t('Invalid first day of week'), 101);
1129
-				}
1130
-				if ($intValue === -1) {
1131
-					$this->config->deleteUserValue($targetUser->getUID(), 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK);
1132
-				} else {
1133
-					$this->config->setUserValue($targetUser->getUID(), 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK, $value);
1134
-				}
1135
-				break;
1136
-			case self::USER_FIELD_NOTIFICATION_EMAIL:
1137
-				$success = false;
1138
-				if ($value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)) {
1139
-					try {
1140
-						$targetUser->setPrimaryEMailAddress($value);
1141
-						$success = true;
1142
-					} catch (InvalidArgumentException $e) {
1143
-						$this->logger->info(
1144
-							'Cannot set primary email, because provided address is not verified',
1145
-							[
1146
-								'app' => 'provisioning_api',
1147
-								'exception' => $e,
1148
-							]
1149
-						);
1150
-					}
1151
-				}
1152
-				if (!$success) {
1153
-					throw new OCSException('', 101);
1154
-				}
1155
-				break;
1156
-			case IAccountManager::PROPERTY_EMAIL:
1157
-				$value = mb_strtolower(trim($value));
1158
-				if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
1159
-					$targetUser->setSystemEMailAddress($value);
1160
-				} else {
1161
-					throw new OCSException('', 101);
1162
-				}
1163
-				break;
1164
-			case IAccountManager::COLLECTION_EMAIL:
1165
-				$value = mb_strtolower(trim($value));
1166
-				if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getSystemEMailAddress()) {
1167
-					$userAccount = $this->accountManager->getAccount($targetUser);
1168
-					$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
1169
-
1170
-					if ($mailCollection->getPropertyByValue($value)) {
1171
-						throw new OCSException('', 101);
1172
-					}
1173
-
1174
-					$mailCollection->addPropertyWithDefaults($value);
1175
-					$this->accountManager->updateAccount($userAccount);
1176
-				} else {
1177
-					throw new OCSException('', 101);
1178
-				}
1179
-				break;
1180
-			case IAccountManager::PROPERTY_PHONE:
1181
-			case IAccountManager::PROPERTY_ADDRESS:
1182
-			case IAccountManager::PROPERTY_WEBSITE:
1183
-			case IAccountManager::PROPERTY_TWITTER:
1184
-			case IAccountManager::PROPERTY_BLUESKY:
1185
-			case IAccountManager::PROPERTY_FEDIVERSE:
1186
-			case IAccountManager::PROPERTY_ORGANISATION:
1187
-			case IAccountManager::PROPERTY_ROLE:
1188
-			case IAccountManager::PROPERTY_HEADLINE:
1189
-			case IAccountManager::PROPERTY_BIOGRAPHY:
1190
-			case IAccountManager::PROPERTY_BIRTHDATE:
1191
-			case IAccountManager::PROPERTY_PRONOUNS:
1192
-				$userAccount = $this->accountManager->getAccount($targetUser);
1193
-				try {
1194
-					$userProperty = $userAccount->getProperty($key);
1195
-					if ($userProperty->getValue() !== $value) {
1196
-						try {
1197
-							$userProperty->setValue($value);
1198
-							if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
1199
-								$this->knownUserService->deleteByContactUserId($targetUser->getUID());
1200
-							}
1201
-						} catch (InvalidArgumentException $e) {
1202
-							throw new OCSException('Invalid ' . $e->getMessage(), 101);
1203
-						}
1204
-					}
1205
-				} catch (PropertyDoesNotExistException $e) {
1206
-					$userAccount->setProperty($key, $value, IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED);
1207
-				}
1208
-				try {
1209
-					$this->accountManager->updateAccount($userAccount);
1210
-				} catch (InvalidArgumentException $e) {
1211
-					throw new OCSException('Invalid ' . $e->getMessage(), 101);
1212
-				}
1213
-				break;
1214
-			case IAccountManager::PROPERTY_PROFILE_ENABLED:
1215
-				$userAccount = $this->accountManager->getAccount($targetUser);
1216
-				try {
1217
-					$userProperty = $userAccount->getProperty($key);
1218
-					if ($userProperty->getValue() !== $value) {
1219
-						$userProperty->setValue($value);
1220
-					}
1221
-				} catch (PropertyDoesNotExistException $e) {
1222
-					$userAccount->setProperty($key, $value, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
1223
-				}
1224
-				$this->accountManager->updateAccount($userAccount);
1225
-				break;
1226
-			case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX:
1227
-			case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX:
1228
-			case IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX:
1229
-			case IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX:
1230
-			case IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX:
1231
-			case IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX:
1232
-			case IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX:
1233
-			case IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX:
1234
-			case IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX:
1235
-			case IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX:
1236
-			case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
1237
-			case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
1238
-			case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
1239
-			case IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX:
1240
-			case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
1241
-			case IAccountManager::PROPERTY_PRONOUNS . self::SCOPE_SUFFIX:
1242
-				$propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
1243
-				$userAccount = $this->accountManager->getAccount($targetUser);
1244
-				$userProperty = $userAccount->getProperty($propertyName);
1245
-				if ($userProperty->getScope() !== $value) {
1246
-					try {
1247
-						$userProperty->setScope($value);
1248
-						$this->accountManager->updateAccount($userAccount);
1249
-					} catch (InvalidArgumentException $e) {
1250
-						throw new OCSException('Invalid ' . $e->getMessage(), 101);
1251
-					}
1252
-				}
1253
-				break;
1254
-			default:
1255
-				throw new OCSException('', 113);
1256
-		}
1257
-		return new DataResponse();
1258
-	}
1259
-
1260
-	/**
1261
-	 * Wipe all devices of a user
1262
-	 *
1263
-	 * @param string $userId ID of the user
1264
-	 *
1265
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1266
-	 *
1267
-	 * @throws OCSException
1268
-	 *
1269
-	 * 200: Wiped all user devices successfully
1270
-	 */
1271
-	#[PasswordConfirmationRequired]
1272
-	#[NoAdminRequired]
1273
-	public function wipeUserDevices(string $userId): DataResponse {
1274
-		/** @var IUser $currentLoggedInUser */
1275
-		$currentLoggedInUser = $this->userSession->getUser();
1276
-
1277
-		$targetUser = $this->userManager->get($userId);
1278
-
1279
-		if ($targetUser === null) {
1280
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1281
-		}
1282
-
1283
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1284
-			throw new OCSException('', 101);
1285
-		}
1286
-
1287
-		// If not permitted
1288
-		$subAdminManager = $this->groupManager->getSubAdmin();
1289
-		$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1290
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1291
-		if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1292
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1293
-		}
1294
-
1295
-		$this->remoteWipe->markAllTokensForWipe($targetUser);
1296
-
1297
-		return new DataResponse();
1298
-	}
1299
-
1300
-	/**
1301
-	 * Delete a user
1302
-	 *
1303
-	 * @param string $userId ID of the user
1304
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1305
-	 * @throws OCSException
1306
-	 *
1307
-	 * 200: User deleted successfully
1308
-	 */
1309
-	#[PasswordConfirmationRequired]
1310
-	#[NoAdminRequired]
1311
-	public function deleteUser(string $userId): DataResponse {
1312
-		$currentLoggedInUser = $this->userSession->getUser();
1313
-
1314
-		$targetUser = $this->userManager->get($userId);
1315
-
1316
-		if ($targetUser === null) {
1317
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1318
-		}
1319
-
1320
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1321
-			throw new OCSException('', 101);
1322
-		}
1323
-
1324
-		// If not permitted
1325
-		$subAdminManager = $this->groupManager->getSubAdmin();
1326
-		$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1327
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1328
-		if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1329
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1330
-		}
1331
-
1332
-		// Go ahead with the delete
1333
-		if ($targetUser->delete()) {
1334
-			return new DataResponse();
1335
-		} else {
1336
-			throw new OCSException('', 101);
1337
-		}
1338
-	}
1339
-
1340
-	/**
1341
-	 * Disable a user
1342
-	 *
1343
-	 * @param string $userId ID of the user
1344
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1345
-	 * @throws OCSException
1346
-	 *
1347
-	 * 200: User disabled successfully
1348
-	 */
1349
-	#[PasswordConfirmationRequired]
1350
-	#[NoAdminRequired]
1351
-	public function disableUser(string $userId): DataResponse {
1352
-		return $this->setEnabled($userId, false);
1353
-	}
1354
-
1355
-	/**
1356
-	 * Enable a user
1357
-	 *
1358
-	 * @param string $userId ID of the user
1359
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1360
-	 * @throws OCSException
1361
-	 *
1362
-	 * 200: User enabled successfully
1363
-	 */
1364
-	#[PasswordConfirmationRequired]
1365
-	#[NoAdminRequired]
1366
-	public function enableUser(string $userId): DataResponse {
1367
-		return $this->setEnabled($userId, true);
1368
-	}
1369
-
1370
-	/**
1371
-	 * @param string $userId
1372
-	 * @param bool $value
1373
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1374
-	 * @throws OCSException
1375
-	 */
1376
-	private function setEnabled(string $userId, bool $value): DataResponse {
1377
-		$currentLoggedInUser = $this->userSession->getUser();
1378
-
1379
-		$targetUser = $this->userManager->get($userId);
1380
-		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
1381
-			throw new OCSException('', 101);
1382
-		}
1383
-
1384
-		// If not permitted
1385
-		$subAdminManager = $this->groupManager->getSubAdmin();
1386
-		$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1387
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1388
-		if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1389
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1390
-		}
1391
-
1392
-		// enable/disable the user now
1393
-		$targetUser->setEnabled($value);
1394
-		return new DataResponse();
1395
-	}
1396
-
1397
-	/**
1398
-	 * @NoSubAdminRequired
1399
-	 *
1400
-	 * Get a list of groups the user belongs to
1401
-	 *
1402
-	 * @param string $userId ID of the user
1403
-	 * @return DataResponse<Http::STATUS_OK, array{groups: list<string>}, array{}>
1404
-	 * @throws OCSException
1405
-	 *
1406
-	 * 200: Users groups returned
1407
-	 */
1408
-	#[NoAdminRequired]
1409
-	public function getUsersGroups(string $userId): DataResponse {
1410
-		$loggedInUser = $this->userSession->getUser();
1411
-
1412
-		$targetUser = $this->userManager->get($userId);
1413
-		if ($targetUser === null) {
1414
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1415
-		}
1416
-
1417
-		$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1418
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1419
-		if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1420
-			// Self lookup or admin lookup
1421
-			return new DataResponse([
1422
-				'groups' => $this->groupManager->getUserGroupIds($targetUser)
1423
-			]);
1424
-		} else {
1425
-			$subAdminManager = $this->groupManager->getSubAdmin();
1426
-
1427
-			// Looking up someone else
1428
-			if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1429
-				// Return the group that the method caller is subadmin of for the user in question
1430
-				$groups = array_values(array_intersect(
1431
-					array_map(static fn (IGroup $group) => $group->getGID(), $subAdminManager->getSubAdminsGroups($loggedInUser)),
1432
-					$this->groupManager->getUserGroupIds($targetUser)
1433
-				));
1434
-				return new DataResponse(['groups' => $groups]);
1435
-			} else {
1436
-				// Not permitted
1437
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1438
-			}
1439
-		}
1440
-	}
1441
-
1442
-	/**
1443
-	 * @NoSubAdminRequired
1444
-	 *
1445
-	 * Get a list of groups with details
1446
-	 *
1447
-	 * @param string $userId ID of the user
1448
-	 * @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
1449
-	 * @throws OCSException
1450
-	 *
1451
-	 * 200: Users groups returned
1452
-	 */
1453
-	#[NoAdminRequired]
1454
-	public function getUsersGroupsDetails(string $userId): DataResponse {
1455
-		$loggedInUser = $this->userSession->getUser();
1456
-
1457
-		$targetUser = $this->userManager->get($userId);
1458
-		if ($targetUser === null) {
1459
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1460
-		}
1461
-
1462
-		$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1463
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1464
-		if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1465
-			// Self lookup or admin lookup
1466
-			$groups = array_map(
1467
-				function (Group $group) {
1468
-					return [
1469
-						'id' => $group->getGID(),
1470
-						'displayname' => $group->getDisplayName(),
1471
-						'usercount' => $group->count(),
1472
-						'disabled' => $group->countDisabled(),
1473
-						'canAdd' => $group->canAddUser(),
1474
-						'canRemove' => $group->canRemoveUser(),
1475
-					];
1476
-				},
1477
-				array_values($this->groupManager->getUserGroups($targetUser)),
1478
-			);
1479
-			return new DataResponse([
1480
-				'groups' => $groups,
1481
-			]);
1482
-		} else {
1483
-			$subAdminManager = $this->groupManager->getSubAdmin();
1484
-
1485
-			// Looking up someone else
1486
-			if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1487
-				// Return the group that the method caller is subadmin of for the user in question
1488
-				$gids = array_values(array_intersect(
1489
-					array_map(
1490
-						static fn (IGroup $group) => $group->getGID(),
1491
-						$subAdminManager->getSubAdminsGroups($loggedInUser),
1492
-					),
1493
-					$this->groupManager->getUserGroupIds($targetUser)
1494
-				));
1495
-				$groups = array_map(
1496
-					function (string $gid) {
1497
-						$group = $this->groupManager->get($gid);
1498
-						return [
1499
-							'id' => $group->getGID(),
1500
-							'displayname' => $group->getDisplayName(),
1501
-							'usercount' => $group->count(),
1502
-							'disabled' => $group->countDisabled(),
1503
-							'canAdd' => $group->canAddUser(),
1504
-							'canRemove' => $group->canRemoveUser(),
1505
-						];
1506
-					},
1507
-					$gids,
1508
-				);
1509
-				return new DataResponse([
1510
-					'groups' => $groups,
1511
-				]);
1512
-			} else {
1513
-				// Not permitted
1514
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1515
-			}
1516
-		}
1517
-	}
1518
-
1519
-	/**
1520
-	 * @NoSubAdminRequired
1521
-	 *
1522
-	 * Get a list of the groups the user is a subadmin of, with details
1523
-	 *
1524
-	 * @param string $userId ID of the user
1525
-	 * @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
1526
-	 * @throws OCSException
1527
-	 *
1528
-	 * 200: Users subadmin groups returned
1529
-	 */
1530
-	#[NoAdminRequired]
1531
-	public function getUserSubAdminGroupsDetails(string $userId): DataResponse {
1532
-		$loggedInUser = $this->userSession->getUser();
1533
-
1534
-		$targetUser = $this->userManager->get($userId);
1535
-		if ($targetUser === null) {
1536
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1537
-		}
1538
-
1539
-		$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1540
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1541
-		if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1542
-			$subAdminManager = $this->groupManager->getSubAdmin();
1543
-			$groups = array_map(
1544
-				function (IGroup $group) {
1545
-					return [
1546
-						'id' => $group->getGID(),
1547
-						'displayname' => $group->getDisplayName(),
1548
-						'usercount' => $group->count(),
1549
-						'disabled' => $group->countDisabled(),
1550
-						'canAdd' => $group->canAddUser(),
1551
-						'canRemove' => $group->canRemoveUser(),
1552
-					];
1553
-				},
1554
-				array_values($subAdminManager->getSubAdminsGroups($targetUser)),
1555
-			);
1556
-			return new DataResponse([
1557
-				'groups' => $groups,
1558
-			]);
1559
-		}
1560
-		throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1561
-	}
1562
-
1563
-	/**
1564
-	 * Add a user to a group
1565
-	 *
1566
-	 * @param string $userId ID of the user
1567
-	 * @param string $groupid ID of the group
1568
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1569
-	 * @throws OCSException
1570
-	 *
1571
-	 * 200: User added to group successfully
1572
-	 */
1573
-	#[PasswordConfirmationRequired]
1574
-	#[NoAdminRequired]
1575
-	public function addToGroup(string $userId, string $groupid = ''): DataResponse {
1576
-		if ($groupid === '') {
1577
-			throw new OCSException('', 101);
1578
-		}
1579
-
1580
-		$group = $this->groupManager->get($groupid);
1581
-		$targetUser = $this->userManager->get($userId);
1582
-		if ($group === null) {
1583
-			throw new OCSException('', 102);
1584
-		}
1585
-		if ($targetUser === null) {
1586
-			throw new OCSException('', 103);
1587
-		}
1588
-
1589
-		// If they're not an admin, check they are a subadmin of the group in question
1590
-		$loggedInUser = $this->userSession->getUser();
1591
-		$subAdminManager = $this->groupManager->getSubAdmin();
1592
-		$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1593
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1594
-		if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1595
-			throw new OCSException('', 104);
1596
-		}
1597
-
1598
-		// Add user to group
1599
-		$group->addUser($targetUser);
1600
-		return new DataResponse();
1601
-	}
1602
-
1603
-	/**
1604
-	 * Remove a user from a group
1605
-	 *
1606
-	 * @param string $userId ID of the user
1607
-	 * @param string $groupid ID of the group
1608
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1609
-	 * @throws OCSException
1610
-	 *
1611
-	 * 200: User removed from group successfully
1612
-	 */
1613
-	#[PasswordConfirmationRequired]
1614
-	#[NoAdminRequired]
1615
-	public function removeFromGroup(string $userId, string $groupid): DataResponse {
1616
-		$loggedInUser = $this->userSession->getUser();
1617
-
1618
-		if ($groupid === null || trim($groupid) === '') {
1619
-			throw new OCSException('', 101);
1620
-		}
1621
-
1622
-		$group = $this->groupManager->get($groupid);
1623
-		if ($group === null) {
1624
-			throw new OCSException('', 102);
1625
-		}
1626
-
1627
-		$targetUser = $this->userManager->get($userId);
1628
-		if ($targetUser === null) {
1629
-			throw new OCSException('', 103);
1630
-		}
1631
-
1632
-		// If they're not an admin, check they are a subadmin of the group in question
1633
-		$subAdminManager = $this->groupManager->getSubAdmin();
1634
-		$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1635
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1636
-		if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1637
-			throw new OCSException('', 104);
1638
-		}
1639
-
1640
-		// Check they aren't removing themselves from 'admin' or their 'subadmin; group
1641
-		if ($targetUser->getUID() === $loggedInUser->getUID()) {
1642
-			if ($isAdmin || $isDelegatedAdmin) {
1643
-				if ($group->getGID() === 'admin') {
1644
-					throw new OCSException($this->l10n->t('Cannot remove yourself from the admin group'), 105);
1645
-				}
1646
-			} else {
1647
-				// Not an admin, so the user must be a subadmin of this group, but that is not allowed.
1648
-				throw new OCSException($this->l10n->t('Cannot remove yourself from this group as you are a sub-admin'), 105);
1649
-			}
1650
-		} elseif (!($isAdmin || $isDelegatedAdmin)) {
1651
-			/** @var IGroup[] $subAdminGroups */
1652
-			$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1653
-			$subAdminGroups = array_map(function (IGroup $subAdminGroup) {
1654
-				return $subAdminGroup->getGID();
1655
-			}, $subAdminGroups);
1656
-			$userGroups = $this->groupManager->getUserGroupIds($targetUser);
1657
-			$userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
1658
-
1659
-			if (count($userSubAdminGroups) <= 1) {
1660
-				// Subadmin must not be able to remove a user from all their subadmin groups.
1661
-				throw new OCSException($this->l10n->t('Not viable to remove user from the last group you are sub-admin of'), 105);
1662
-			}
1663
-		}
1664
-
1665
-		// Remove user from group
1666
-		$group->removeUser($targetUser);
1667
-		return new DataResponse();
1668
-	}
1669
-
1670
-	/**
1671
-	 * Make a user a subadmin of a group
1672
-	 *
1673
-	 * @param string $userId ID of the user
1674
-	 * @param string $groupid ID of the group
1675
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1676
-	 * @throws OCSException
1677
-	 *
1678
-	 * 200: User added as group subadmin successfully
1679
-	 */
1680
-	#[AuthorizedAdminSetting(settings:Users::class)]
1681
-	#[PasswordConfirmationRequired]
1682
-	public function addSubAdmin(string $userId, string $groupid): DataResponse {
1683
-		$group = $this->groupManager->get($groupid);
1684
-		$user = $this->userManager->get($userId);
1685
-
1686
-		// Check if the user exists
1687
-		if ($user === null) {
1688
-			throw new OCSException($this->l10n->t('User does not exist'), 101);
1689
-		}
1690
-		// Check if group exists
1691
-		if ($group === null) {
1692
-			throw new OCSException($this->l10n->t('Group does not exist'), 102);
1693
-		}
1694
-		// Check if trying to make subadmin of admin group
1695
-		if ($group->getGID() === 'admin') {
1696
-			throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103);
1697
-		}
1698
-
1699
-		$subAdminManager = $this->groupManager->getSubAdmin();
1700
-
1701
-		// We cannot be subadmin twice
1702
-		if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
1703
-			return new DataResponse();
1704
-		}
1705
-		// Go
1706
-		$subAdminManager->createSubAdmin($user, $group);
1707
-		return new DataResponse();
1708
-	}
1709
-
1710
-	/**
1711
-	 * Remove a user from the subadmins of a group
1712
-	 *
1713
-	 * @param string $userId ID of the user
1714
-	 * @param string $groupid ID of the group
1715
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1716
-	 * @throws OCSException
1717
-	 *
1718
-	 * 200: User removed as group subadmin successfully
1719
-	 */
1720
-	#[AuthorizedAdminSetting(settings:Users::class)]
1721
-	#[PasswordConfirmationRequired]
1722
-	public function removeSubAdmin(string $userId, string $groupid): DataResponse {
1723
-		$group = $this->groupManager->get($groupid);
1724
-		$user = $this->userManager->get($userId);
1725
-		$subAdminManager = $this->groupManager->getSubAdmin();
1726
-
1727
-		// Check if the user exists
1728
-		if ($user === null) {
1729
-			throw new OCSException($this->l10n->t('User does not exist'), 101);
1730
-		}
1731
-		// Check if the group exists
1732
-		if ($group === null) {
1733
-			throw new OCSException($this->l10n->t('Group does not exist'), 101);
1734
-		}
1735
-		// Check if they are a subadmin of this said group
1736
-		if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
1737
-			throw new OCSException($this->l10n->t('User is not a sub-admin of this group'), 102);
1738
-		}
1739
-
1740
-		// Go
1741
-		$subAdminManager->deleteSubAdmin($user, $group);
1742
-		return new DataResponse();
1743
-	}
1744
-
1745
-	/**
1746
-	 * Get the groups a user is a subadmin of
1747
-	 *
1748
-	 * @param string $userId ID if the user
1749
-	 * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
1750
-	 * @throws OCSException
1751
-	 *
1752
-	 * 200: User subadmin groups returned
1753
-	 */
1754
-	#[AuthorizedAdminSetting(settings:Users::class)]
1755
-	public function getUserSubAdminGroups(string $userId): DataResponse {
1756
-		$groups = $this->getUserSubAdminGroupsData($userId);
1757
-		return new DataResponse($groups);
1758
-	}
1759
-
1760
-	/**
1761
-	 * Resend the welcome message
1762
-	 *
1763
-	 * @param string $userId ID if the user
1764
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1765
-	 * @throws OCSException
1766
-	 *
1767
-	 * 200: Resent welcome message successfully
1768
-	 */
1769
-	#[PasswordConfirmationRequired]
1770
-	#[NoAdminRequired]
1771
-	public function resendWelcomeMessage(string $userId): DataResponse {
1772
-		$currentLoggedInUser = $this->userSession->getUser();
1773
-
1774
-		$targetUser = $this->userManager->get($userId);
1775
-		if ($targetUser === null) {
1776
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1777
-		}
1778
-
1779
-		// Check if admin / subadmin
1780
-		$subAdminManager = $this->groupManager->getSubAdmin();
1781
-		$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1782
-		$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1783
-		if (
1784
-			!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1785
-			&& !($isAdmin || $isDelegatedAdmin)
1786
-		) {
1787
-			// No rights
1788
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1789
-		}
1790
-
1791
-		$email = $targetUser->getEMailAddress();
1792
-		if ($email === '' || $email === null) {
1793
-			throw new OCSException($this->l10n->t('Email address not available'), 101);
1794
-		}
1795
-
1796
-		try {
1797
-			if ($this->config->getUserValue($targetUser->getUID(), 'core', 'lostpassword')) {
1798
-				$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, true);
1799
-			} else {
1800
-				$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
1801
-			}
1802
-
1803
-			$this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
1804
-		} catch (\Exception $e) {
1805
-			$this->logger->error(
1806
-				"Can't send new user mail to $email",
1807
-				[
1808
-					'app' => 'settings',
1809
-					'exception' => $e,
1810
-				]
1811
-			);
1812
-			throw new OCSException($this->l10n->t('Sending email failed'), 102);
1813
-		}
1814
-
1815
-		return new DataResponse();
1816
-	}
62
+    private IL10N $l10n;
63
+
64
+    public function __construct(
65
+        string $appName,
66
+        IRequest $request,
67
+        IUserManager $userManager,
68
+        IConfig $config,
69
+        IGroupManager $groupManager,
70
+        IUserSession $userSession,
71
+        IAccountManager $accountManager,
72
+        ISubAdmin $subAdminManager,
73
+        IFactory $l10nFactory,
74
+        IRootFolder $rootFolder,
75
+        private IURLGenerator $urlGenerator,
76
+        private LoggerInterface $logger,
77
+        private NewUserMailHelper $newUserMailHelper,
78
+        private ISecureRandom $secureRandom,
79
+        private RemoteWipe $remoteWipe,
80
+        private KnownUserService $knownUserService,
81
+        private IEventDispatcher $eventDispatcher,
82
+        private IPhoneNumberUtil $phoneNumberUtil,
83
+        private IAppManager $appManager,
84
+    ) {
85
+        parent::__construct(
86
+            $appName,
87
+            $request,
88
+            $userManager,
89
+            $config,
90
+            $groupManager,
91
+            $userSession,
92
+            $accountManager,
93
+            $subAdminManager,
94
+            $l10nFactory,
95
+            $rootFolder,
96
+        );
97
+
98
+        $this->l10n = $l10nFactory->get($appName);
99
+    }
100
+
101
+    /**
102
+     * Get a list of users
103
+     *
104
+     * @param string $search Text to search for
105
+     * @param int|null $limit Limit the amount of groups returned
106
+     * @param int $offset Offset for searching for groups
107
+     * @return DataResponse<Http::STATUS_OK, array{users: list<string>}, array{}>
108
+     *
109
+     * 200: Users returned
110
+     */
111
+    #[NoAdminRequired]
112
+    public function getUsers(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
113
+        $user = $this->userSession->getUser();
114
+        $users = [];
115
+
116
+        // Admin? Or SubAdmin?
117
+        $uid = $user->getUID();
118
+        $subAdminManager = $this->groupManager->getSubAdmin();
119
+        $isAdmin = $this->groupManager->isAdmin($uid);
120
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
121
+        if ($isAdmin || $isDelegatedAdmin) {
122
+            $users = $this->userManager->search($search, $limit, $offset);
123
+        } elseif ($subAdminManager->isSubAdmin($user)) {
124
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
125
+            foreach ($subAdminOfGroups as $key => $group) {
126
+                $subAdminOfGroups[$key] = $group->getGID();
127
+            }
128
+
129
+            $users = [];
130
+            foreach ($subAdminOfGroups as $group) {
131
+                $users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
132
+            }
133
+        }
134
+
135
+        /** @var list<string> $users */
136
+        $users = array_keys($users);
137
+
138
+        return new DataResponse([
139
+            'users' => $users
140
+        ]);
141
+    }
142
+
143
+    /**
144
+     * Get a list of users and their details
145
+     *
146
+     * @param string $search Text to search for
147
+     * @param int|null $limit Limit the amount of groups returned
148
+     * @param int $offset Offset for searching for groups
149
+     * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
150
+     *
151
+     * 200: Users details returned
152
+     */
153
+    #[NoAdminRequired]
154
+    public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
155
+        $currentUser = $this->userSession->getUser();
156
+        $users = [];
157
+
158
+        // Admin? Or SubAdmin?
159
+        $uid = $currentUser->getUID();
160
+        $subAdminManager = $this->groupManager->getSubAdmin();
161
+        $isAdmin = $this->groupManager->isAdmin($uid);
162
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
163
+        if ($isAdmin || $isDelegatedAdmin) {
164
+            $users = $this->userManager->search($search, $limit, $offset);
165
+            $users = array_keys($users);
166
+        } elseif ($subAdminManager->isSubAdmin($currentUser)) {
167
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
168
+            foreach ($subAdminOfGroups as $key => $group) {
169
+                $subAdminOfGroups[$key] = $group->getGID();
170
+            }
171
+
172
+            $users = [];
173
+            foreach ($subAdminOfGroups as $group) {
174
+                $users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
175
+            }
176
+            $users = array_merge(...$users);
177
+        }
178
+
179
+        $usersDetails = [];
180
+        foreach ($users as $userId) {
181
+            $userId = (string)$userId;
182
+            try {
183
+                $userData = $this->getUserData($userId);
184
+            } catch (OCSNotFoundException $e) {
185
+                // We still want to return all other accounts, but this one was removed from the backends
186
+                // yet they are still in our database. Might be a LDAP remnant.
187
+                $userData = null;
188
+                $this->logger->warning('Found one enabled account that is removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
189
+            }
190
+            // Do not insert empty entry
191
+            if ($userData !== null) {
192
+                $usersDetails[$userId] = $userData;
193
+            } else {
194
+                // Logged user does not have permissions to see this user
195
+                // only showing its id
196
+                $usersDetails[$userId] = ['id' => $userId];
197
+            }
198
+        }
199
+
200
+        return new DataResponse([
201
+            'users' => $usersDetails
202
+        ]);
203
+    }
204
+
205
+    /**
206
+     * Get the list of disabled users and their details
207
+     *
208
+     * @param string $search Text to search for
209
+     * @param ?int $limit Limit the amount of users returned
210
+     * @param int $offset Offset
211
+     * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
212
+     *
213
+     * 200: Disabled users details returned
214
+     */
215
+    #[NoAdminRequired]
216
+    public function getDisabledUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
217
+        $currentUser = $this->userSession->getUser();
218
+        if ($currentUser === null) {
219
+            return new DataResponse(['users' => []]);
220
+        }
221
+        if ($limit !== null && $limit < 0) {
222
+            throw new InvalidArgumentException("Invalid limit value: $limit");
223
+        }
224
+        if ($offset < 0) {
225
+            throw new InvalidArgumentException("Invalid offset value: $offset");
226
+        }
227
+
228
+        $users = [];
229
+
230
+        // Admin? Or SubAdmin?
231
+        $uid = $currentUser->getUID();
232
+        $subAdminManager = $this->groupManager->getSubAdmin();
233
+        $isAdmin = $this->groupManager->isAdmin($uid);
234
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
235
+        if ($isAdmin || $isDelegatedAdmin) {
236
+            $users = $this->userManager->getDisabledUsers($limit, $offset, $search);
237
+            $users = array_map(fn (IUser $user): string => $user->getUID(), $users);
238
+        } elseif ($subAdminManager->isSubAdmin($currentUser)) {
239
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
240
+
241
+            $users = [];
242
+            /* We have to handle offset ourselve for correctness */
243
+            $tempLimit = ($limit === null ? null : $limit + $offset);
244
+            foreach ($subAdminOfGroups as $group) {
245
+                $users = array_unique(array_merge(
246
+                    $users,
247
+                    array_map(
248
+                        fn (IUser $user): string => $user->getUID(),
249
+                        array_filter(
250
+                            $group->searchUsers($search),
251
+                            fn (IUser $user): bool => !$user->isEnabled()
252
+                        )
253
+                    )
254
+                ));
255
+                if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
256
+                    break;
257
+                }
258
+            }
259
+            $users = array_slice($users, $offset, $limit);
260
+        }
261
+
262
+        $usersDetails = [];
263
+        foreach ($users as $userId) {
264
+            try {
265
+                $userData = $this->getUserData($userId);
266
+            } catch (OCSNotFoundException $e) {
267
+                // We still want to return all other accounts, but this one was removed from the backends
268
+                // yet they are still in our database. Might be a LDAP remnant.
269
+                $userData = null;
270
+                $this->logger->warning('Found one disabled account that was removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
271
+            }
272
+            // Do not insert empty entry
273
+            if ($userData !== null) {
274
+                $usersDetails[$userId] = $userData;
275
+            } else {
276
+                // Currently logged in user does not have permissions to see this user
277
+                // only showing its id
278
+                $usersDetails[$userId] = ['id' => $userId];
279
+            }
280
+        }
281
+
282
+        return new DataResponse([
283
+            'users' => $usersDetails
284
+        ]);
285
+    }
286
+
287
+    /**
288
+     * Gets the list of users sorted by lastLogin, from most recent to least recent
289
+     *
290
+     * @param string $search Text to search for
291
+     * @param ?int $limit Limit the amount of users returned
292
+     * @param int $offset Offset
293
+     * @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
294
+     *
295
+     * 200: Users details returned based on last logged in information
296
+     */
297
+    #[AuthorizedAdminSetting(settings:Users::class)]
298
+    public function getLastLoggedInUsers(string $search = '',
299
+        ?int $limit = null,
300
+        int $offset = 0,
301
+    ): DataResponse {
302
+        $currentUser = $this->userSession->getUser();
303
+        if ($currentUser === null) {
304
+            return new DataResponse(['users' => []]);
305
+        }
306
+        if ($limit !== null && $limit < 0) {
307
+            throw new InvalidArgumentException("Invalid limit value: $limit");
308
+        }
309
+        if ($offset < 0) {
310
+            throw new InvalidArgumentException("Invalid offset value: $offset");
311
+        }
312
+
313
+        $users = [];
314
+
315
+        // For Admin alone user sorting based on lastLogin. For sub admin and groups this is not supported
316
+        $users = $this->userManager->getLastLoggedInUsers($limit, $offset, $search);
317
+
318
+        $usersDetails = [];
319
+        foreach ($users as $userId) {
320
+            try {
321
+                $userData = $this->getUserData($userId);
322
+            } catch (OCSNotFoundException $e) {
323
+                // We still want to return all other accounts, but this one was removed from the backends
324
+                // yet they are still in our database. Might be a LDAP remnant.
325
+                $userData = null;
326
+                $this->logger->warning('Found one account that was removed from its backend, but still exists in Nextcloud database', ['accountId' => $userId]);
327
+            }
328
+            // Do not insert empty entry
329
+            if ($userData !== null) {
330
+                $usersDetails[$userId] = $userData;
331
+            } else {
332
+                // Currently logged-in user does not have permissions to see this user
333
+                // only showing its id
334
+                $usersDetails[$userId] = ['id' => $userId];
335
+            }
336
+        }
337
+
338
+        return new DataResponse([
339
+            'users' => $usersDetails
340
+        ]);
341
+    }
342
+
343
+
344
+
345
+    /**
346
+     * @NoSubAdminRequired
347
+     *
348
+     * Search users by their phone numbers
349
+     *
350
+     * @param string $location Location of the phone number (for country code)
351
+     * @param array<string, list<string>> $search Phone numbers to search for
352
+     * @return DataResponse<Http::STATUS_OK, array<string, string>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, list<empty>, array{}>
353
+     *
354
+     * 200: Users returned
355
+     * 400: Invalid location
356
+     */
357
+    #[NoAdminRequired]
358
+    public function searchByPhoneNumbers(string $location, array $search): DataResponse {
359
+        if ($this->phoneNumberUtil->getCountryCodeForRegion($location) === null) {
360
+            // Not a valid region code
361
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
362
+        }
363
+
364
+        /** @var IUser $user */
365
+        $user = $this->userSession->getUser();
366
+        $knownTo = $user->getUID();
367
+        $defaultPhoneRegion = $this->config->getSystemValueString('default_phone_region');
368
+
369
+        $normalizedNumberToKey = [];
370
+        foreach ($search as $key => $phoneNumbers) {
371
+            foreach ($phoneNumbers as $phone) {
372
+                $normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $location);
373
+                if ($normalizedNumber !== null) {
374
+                    $normalizedNumberToKey[$normalizedNumber] = (string)$key;
375
+                }
376
+
377
+                if ($defaultPhoneRegion !== '' && $defaultPhoneRegion !== $location && str_starts_with($phone, '0')) {
378
+                    // If the number has a leading zero (no country code),
379
+                    // we also check the default phone region of the instance,
380
+                    // when it's different to the user's given region.
381
+                    $normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $defaultPhoneRegion);
382
+                    if ($normalizedNumber !== null) {
383
+                        $normalizedNumberToKey[$normalizedNumber] = (string)$key;
384
+                    }
385
+                }
386
+            }
387
+        }
388
+
389
+        $phoneNumbers = array_keys($normalizedNumberToKey);
390
+
391
+        if (empty($phoneNumbers)) {
392
+            return new DataResponse();
393
+        }
394
+
395
+        // Cleanup all previous entries and only allow new matches
396
+        $this->knownUserService->deleteKnownTo($knownTo);
397
+
398
+        $userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers);
399
+
400
+        if (empty($userMatches)) {
401
+            return new DataResponse();
402
+        }
403
+
404
+        $cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
405
+        if (strpos($cloudUrl, 'http://') === 0) {
406
+            $cloudUrl = substr($cloudUrl, strlen('http://'));
407
+        } elseif (strpos($cloudUrl, 'https://') === 0) {
408
+            $cloudUrl = substr($cloudUrl, strlen('https://'));
409
+        }
410
+
411
+        $matches = [];
412
+        foreach ($userMatches as $phone => $userId) {
413
+            // Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
414
+            $matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
415
+            $this->knownUserService->storeIsKnownToUser($knownTo, $userId);
416
+        }
417
+
418
+        return new DataResponse($matches);
419
+    }
420
+
421
+    /**
422
+     * @throws OCSException
423
+     */
424
+    private function createNewUserId(): string {
425
+        $attempts = 0;
426
+        do {
427
+            $uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
428
+            if (!$this->userManager->userExists($uidCandidate)) {
429
+                return $uidCandidate;
430
+            }
431
+            $attempts++;
432
+        } while ($attempts < 10);
433
+        throw new OCSException($this->l10n->t('Could not create non-existing user ID'), 111);
434
+    }
435
+
436
+    /**
437
+     * Create a new user
438
+     *
439
+     * @param string $userid ID of the user
440
+     * @param string $password Password of the user
441
+     * @param string $displayName Display name of the user
442
+     * @param string $email Email of the user
443
+     * @param list<string> $groups Groups of the user
444
+     * @param list<string> $subadmin Groups where the user is subadmin
445
+     * @param string $quota Quota of the user
446
+     * @param string $language Language of the user
447
+     * @param ?string $manager Manager of the user
448
+     * @return DataResponse<Http::STATUS_OK, array{id: string}, array{}>
449
+     * @throws OCSException
450
+     * @throws OCSForbiddenException Missing permissions to make user subadmin
451
+     *
452
+     * 200: User added successfully
453
+     */
454
+    #[PasswordConfirmationRequired]
455
+    #[NoAdminRequired]
456
+    public function addUser(
457
+        string $userid,
458
+        string $password = '',
459
+        string $displayName = '',
460
+        string $email = '',
461
+        array $groups = [],
462
+        array $subadmin = [],
463
+        string $quota = '',
464
+        string $language = '',
465
+        ?string $manager = null,
466
+    ): DataResponse {
467
+        $user = $this->userSession->getUser();
468
+        $isAdmin = $this->groupManager->isAdmin($user->getUID());
469
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($user->getUID());
470
+        $subAdminManager = $this->groupManager->getSubAdmin();
471
+
472
+        if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
473
+            $userid = $this->createNewUserId();
474
+        }
475
+
476
+        if ($this->userManager->userExists($userid)) {
477
+            $this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
478
+            throw new OCSException($this->l10n->t('User already exists'), 102);
479
+        }
480
+
481
+        if ($groups !== []) {
482
+            foreach ($groups as $group) {
483
+                if (!$this->groupManager->groupExists($group)) {
484
+                    throw new OCSException($this->l10n->t('Group %1$s does not exist', [$group]), 104);
485
+                }
486
+                if (!$isAdmin && !($isDelegatedAdmin && $group !== 'admin') && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
487
+                    throw new OCSException($this->l10n->t('Insufficient privileges for group %1$s', [$group]), 105);
488
+                }
489
+            }
490
+        } else {
491
+            if (!$isAdmin && !$isDelegatedAdmin) {
492
+                throw new OCSException($this->l10n->t('No group specified (required for sub-admins)'), 106);
493
+            }
494
+        }
495
+
496
+        $subadminGroups = [];
497
+        if ($subadmin !== []) {
498
+            foreach ($subadmin as $groupid) {
499
+                $group = $this->groupManager->get($groupid);
500
+                // Check if group exists
501
+                if ($group === null) {
502
+                    throw new OCSException($this->l10n->t('Sub-admin group does not exist'), 109);
503
+                }
504
+                // Check if trying to make subadmin of admin group
505
+                if ($group->getGID() === 'admin') {
506
+                    throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103);
507
+                }
508
+                // Check if has permission to promote subadmins
509
+                if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin && !$isDelegatedAdmin) {
510
+                    throw new OCSForbiddenException($this->l10n->t('No permissions to promote sub-admins'));
511
+                }
512
+                $subadminGroups[] = $group;
513
+            }
514
+        }
515
+
516
+        $generatePasswordResetToken = false;
517
+        if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
518
+            throw new OCSException($this->l10n->t('Invalid password value'), 101);
519
+        }
520
+        if ($password === '') {
521
+            if ($email === '') {
522
+                throw new OCSException($this->l10n->t('An email address is required, to send a password link to the user.'), 108);
523
+            }
524
+
525
+            $passwordEvent = new GenerateSecurePasswordEvent();
526
+            $this->eventDispatcher->dispatchTyped($passwordEvent);
527
+
528
+            $password = $passwordEvent->getPassword();
529
+            if ($password === null) {
530
+                // Fallback: ensure to pass password_policy in any case
531
+                $password = $this->secureRandom->generate(10)
532
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_UPPER)
533
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_LOWER)
534
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_DIGITS)
535
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_SYMBOLS);
536
+            }
537
+            $generatePasswordResetToken = true;
538
+        }
539
+
540
+        $email = mb_strtolower(trim($email));
541
+        if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
542
+            throw new OCSException($this->l10n->t('Required email address was not provided'), 110);
543
+        }
544
+
545
+        // Create the user
546
+        try {
547
+            $newUser = $this->userManager->createUser($userid, $password);
548
+            if (!$newUser instanceof IUser) {
549
+                // If the user is not an instance of IUser, it means the user creation failed
550
+                $this->logger->error('Failed addUser attempt: User creation failed.', ['app' => 'ocs_api']);
551
+                throw new OCSException($this->l10n->t('User creation failed'), 111);
552
+            }
553
+
554
+            $this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
555
+            foreach ($groups as $group) {
556
+                $this->groupManager->get($group)->addUser($newUser);
557
+                $this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
558
+            }
559
+            foreach ($subadminGroups as $group) {
560
+                $subAdminManager->createSubAdmin($newUser, $group);
561
+            }
562
+
563
+            if ($displayName !== '') {
564
+                try {
565
+                    $this->editUser($userid, self::USER_FIELD_DISPLAYNAME, $displayName);
566
+                } catch (OCSException $e) {
567
+                    if ($newUser instanceof IUser) {
568
+                        $newUser->delete();
569
+                    }
570
+                    throw $e;
571
+                }
572
+            }
573
+
574
+            if ($quota !== '') {
575
+                $this->editUser($userid, self::USER_FIELD_QUOTA, $quota);
576
+            }
577
+
578
+            if ($language !== '') {
579
+                $this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
580
+            }
581
+
582
+            /**
583
+             * null -> nothing sent
584
+             * '' -> unset manager
585
+             * else -> set manager
586
+             */
587
+            if ($manager !== null) {
588
+                $this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
589
+            }
590
+
591
+            // Send new user mail only if a mail is set
592
+            if ($email !== '') {
593
+                $newUser->setSystemEMailAddress($email);
594
+                if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
595
+                    try {
596
+                        $emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
597
+                        $this->newUserMailHelper->sendMail($newUser, $emailTemplate);
598
+                    } catch (\Exception $e) {
599
+                        // Mail could be failing hard or just be plain not configured
600
+                        // Logging error as it is the hardest of the two
601
+                        $this->logger->error(
602
+                            "Unable to send the invitation mail to $email",
603
+                            [
604
+                                'app' => 'ocs_api',
605
+                                'exception' => $e,
606
+                            ]
607
+                        );
608
+                    }
609
+                }
610
+            }
611
+
612
+            return new DataResponse(['id' => $userid]);
613
+        } catch (HintException $e) {
614
+            $this->logger->warning(
615
+                'Failed addUser attempt with hint exception.',
616
+                [
617
+                    'app' => 'ocs_api',
618
+                    'exception' => $e,
619
+                ]
620
+            );
621
+            throw new OCSException($e->getHint(), 107);
622
+        } catch (OCSException $e) {
623
+            $this->logger->warning(
624
+                'Failed addUser attempt with ocs exception.',
625
+                [
626
+                    'app' => 'ocs_api',
627
+                    'exception' => $e,
628
+                ]
629
+            );
630
+            throw $e;
631
+        } catch (InvalidArgumentException $e) {
632
+            $this->logger->error(
633
+                'Failed addUser attempt with invalid argument exception.',
634
+                [
635
+                    'app' => 'ocs_api',
636
+                    'exception' => $e,
637
+                ]
638
+            );
639
+            throw new OCSException($e->getMessage(), 101);
640
+        } catch (\Exception $e) {
641
+            $this->logger->error(
642
+                'Failed addUser attempt with exception.',
643
+                [
644
+                    'app' => 'ocs_api',
645
+                    'exception' => $e
646
+                ]
647
+            );
648
+            throw new OCSException('Bad request', 101);
649
+        }
650
+    }
651
+
652
+    /**
653
+     * @NoSubAdminRequired
654
+     *
655
+     * Get the details of a user
656
+     *
657
+     * @param string $userId ID of the user
658
+     * @return DataResponse<Http::STATUS_OK, Provisioning_APIUserDetails, array{}>
659
+     * @throws OCSException
660
+     *
661
+     * 200: User returned
662
+     */
663
+    #[NoAdminRequired]
664
+    public function getUser(string $userId): DataResponse {
665
+        $includeScopes = false;
666
+        $currentUser = $this->userSession->getUser();
667
+        if ($currentUser && $currentUser->getUID() === $userId) {
668
+            $includeScopes = true;
669
+        }
670
+
671
+        $data = $this->getUserData($userId, $includeScopes);
672
+        // getUserData returns null if not enough permissions
673
+        if ($data === null) {
674
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
675
+        }
676
+        return new DataResponse($data);
677
+    }
678
+
679
+    /**
680
+     * @NoSubAdminRequired
681
+     *
682
+     * Get the details of the current user
683
+     *
684
+     * @return DataResponse<Http::STATUS_OK, Provisioning_APIUserDetails, array{}>
685
+     * @throws OCSException
686
+     *
687
+     * 200: Current user returned
688
+     */
689
+    #[NoAdminRequired]
690
+    public function getCurrentUser(): DataResponse {
691
+        $user = $this->userSession->getUser();
692
+        if ($user) {
693
+            /** @var Provisioning_APIUserDetails $data */
694
+            $data = $this->getUserData($user->getUID(), true);
695
+            return new DataResponse($data);
696
+        }
697
+
698
+        throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
699
+    }
700
+
701
+    /**
702
+     * @NoSubAdminRequired
703
+     *
704
+     * Get a list of fields that are editable for the current user
705
+     *
706
+     * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
707
+     * @throws OCSException
708
+     *
709
+     * 200: Editable fields returned
710
+     */
711
+    #[NoAdminRequired]
712
+    public function getEditableFields(): DataResponse {
713
+        $currentLoggedInUser = $this->userSession->getUser();
714
+        if (!$currentLoggedInUser instanceof IUser) {
715
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
716
+        }
717
+
718
+        return $this->getEditableFieldsForUser($currentLoggedInUser->getUID());
719
+    }
720
+
721
+    /**
722
+     * Get a list of enabled apps for the current user
723
+     *
724
+     * @return DataResponse<Http::STATUS_OK, array{apps: list<string>}, array{}>
725
+     *
726
+     * 200: Enabled apps returned
727
+     */
728
+    #[NoAdminRequired]
729
+    public function getEnabledApps(): DataResponse {
730
+        $currentLoggedInUser = $this->userSession->getUser();
731
+        return new DataResponse(['apps' => $this->appManager->getEnabledAppsForUser($currentLoggedInUser)]);
732
+    }
733
+
734
+    /**
735
+     * @NoSubAdminRequired
736
+     *
737
+     * Get a list of fields that are editable for a user
738
+     *
739
+     * @param string $userId ID of the user
740
+     * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
741
+     * @throws OCSException
742
+     *
743
+     * 200: Editable fields for user returned
744
+     */
745
+    #[NoAdminRequired]
746
+    public function getEditableFieldsForUser(string $userId): DataResponse {
747
+        $currentLoggedInUser = $this->userSession->getUser();
748
+        if (!$currentLoggedInUser instanceof IUser) {
749
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
750
+        }
751
+
752
+        $permittedFields = [];
753
+
754
+        if ($userId !== $currentLoggedInUser->getUID()) {
755
+            $targetUser = $this->userManager->get($userId);
756
+            if (!$targetUser instanceof IUser) {
757
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
758
+            }
759
+
760
+            $subAdminManager = $this->groupManager->getSubAdmin();
761
+            $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
762
+            $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
763
+            if (
764
+                !($isAdmin || $isDelegatedAdmin)
765
+                && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
766
+            ) {
767
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
768
+            }
769
+        } else {
770
+            $targetUser = $currentLoggedInUser;
771
+        }
772
+
773
+        $allowDisplayNameChange = $this->config->getSystemValue('allow_user_to_change_display_name', true);
774
+        if ($allowDisplayNameChange === true && (
775
+            $targetUser->getBackend() instanceof ISetDisplayNameBackend
776
+            || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
777
+        )) {
778
+            $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
779
+        }
780
+
781
+        // Fallback to display name value to avoid changing behavior with the new option.
782
+        if ($this->config->getSystemValue('allow_user_to_change_email', $allowDisplayNameChange)) {
783
+            $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
784
+        }
785
+
786
+        $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
787
+        $permittedFields[] = IAccountManager::PROPERTY_PHONE;
788
+        $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
789
+        $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
790
+        $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
791
+        $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
792
+        $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
793
+        $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
794
+        $permittedFields[] = IAccountManager::PROPERTY_ROLE;
795
+        $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
796
+        $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
797
+        $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
798
+        $permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
799
+
800
+        return new DataResponse($permittedFields);
801
+    }
802
+
803
+    /**
804
+     * @NoSubAdminRequired
805
+     *
806
+     * Update multiple values of the user's details
807
+     *
808
+     * @param string $userId ID of the user
809
+     * @param string $collectionName Collection to update
810
+     * @param string $key Key that will be updated
811
+     * @param string $value New value for the key
812
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
813
+     * @throws OCSException
814
+     *
815
+     * 200: User values edited successfully
816
+     */
817
+    #[PasswordConfirmationRequired]
818
+    #[NoAdminRequired]
819
+    #[UserRateLimit(limit: 5, period: 60)]
820
+    public function editUserMultiValue(
821
+        string $userId,
822
+        string $collectionName,
823
+        string $key,
824
+        string $value,
825
+    ): DataResponse {
826
+        $currentLoggedInUser = $this->userSession->getUser();
827
+        if ($currentLoggedInUser === null) {
828
+            throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
829
+        }
830
+
831
+        $targetUser = $this->userManager->get($userId);
832
+        if ($targetUser === null) {
833
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
834
+        }
835
+
836
+        $subAdminManager = $this->groupManager->getSubAdmin();
837
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
838
+        $isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID())
839
+            || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
840
+
841
+        $permittedFields = [];
842
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
843
+            // Editing self (display, email)
844
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
845
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
846
+        } else {
847
+            // Check if admin / subadmin
848
+            if ($isAdminOrSubadmin || $isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) {
849
+                // They have permissions over the user
850
+                $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
851
+            } else {
852
+                // No rights
853
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
854
+            }
855
+        }
856
+
857
+        // Check if permitted to edit this field
858
+        if (!in_array($collectionName, $permittedFields)) {
859
+            throw new OCSException('', 103);
860
+        }
861
+
862
+        switch ($collectionName) {
863
+            case IAccountManager::COLLECTION_EMAIL:
864
+                $userAccount = $this->accountManager->getAccount($targetUser);
865
+                $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
866
+                $mailCollection->removePropertyByValue($key);
867
+                if ($value !== '') {
868
+                    $value = mb_strtolower(trim($value));
869
+                    $mailCollection->addPropertyWithDefaults($value);
870
+                    $property = $mailCollection->getPropertyByValue($key);
871
+                    if ($isAdminOrSubadmin && $property) {
872
+                        // admin set mails are auto-verified
873
+                        $property->setLocallyVerified(IAccountManager::VERIFIED);
874
+                    }
875
+                }
876
+                $this->accountManager->updateAccount($userAccount);
877
+                if ($value === '' && $key === $targetUser->getPrimaryEMailAddress()) {
878
+                    $targetUser->setPrimaryEMailAddress('');
879
+                }
880
+                break;
881
+
882
+            case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX:
883
+                $userAccount = $this->accountManager->getAccount($targetUser);
884
+                $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
885
+                $targetProperty = null;
886
+                foreach ($mailCollection->getProperties() as $property) {
887
+                    if ($property->getValue() === $key) {
888
+                        $targetProperty = $property;
889
+                        break;
890
+                    }
891
+                }
892
+                if ($targetProperty instanceof IAccountProperty) {
893
+                    try {
894
+                        $targetProperty->setScope($value);
895
+                        $this->accountManager->updateAccount($userAccount);
896
+                    } catch (InvalidArgumentException $e) {
897
+                        throw new OCSException('', 102);
898
+                    }
899
+                } else {
900
+                    throw new OCSException('', 102);
901
+                }
902
+                break;
903
+
904
+            default:
905
+                throw new OCSException('', 103);
906
+        }
907
+        return new DataResponse();
908
+    }
909
+
910
+    /**
911
+     * @NoSubAdminRequired
912
+     *
913
+     * Update a value of the user's details
914
+     *
915
+     * @param string $userId ID of the user
916
+     * @param string $key Key that will be updated
917
+     * @param string $value New value for the key
918
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
919
+     * @throws OCSException
920
+     *
921
+     * 200: User value edited successfully
922
+     */
923
+    #[PasswordConfirmationRequired]
924
+    #[NoAdminRequired]
925
+    #[UserRateLimit(limit: 50, period: 600)]
926
+    public function editUser(string $userId, string $key, string $value): DataResponse {
927
+        $currentLoggedInUser = $this->userSession->getUser();
928
+
929
+        $targetUser = $this->userManager->get($userId);
930
+        if ($targetUser === null) {
931
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
932
+        }
933
+
934
+        $permittedFields = [];
935
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
936
+            $allowDisplayNameChange = $this->config->getSystemValue('allow_user_to_change_display_name', true);
937
+            if ($allowDisplayNameChange !== false && (
938
+                $targetUser->getBackend() instanceof ISetDisplayNameBackend
939
+                || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
940
+            )) {
941
+                $permittedFields[] = self::USER_FIELD_DISPLAYNAME;
942
+                $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
943
+            }
944
+
945
+            // Fallback to display name value to avoid changing behavior with the new option.
946
+            if ($this->config->getSystemValue('allow_user_to_change_email', $allowDisplayNameChange)) {
947
+                $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
948
+            }
949
+
950
+            $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX;
951
+            $permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX;
952
+
953
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
954
+
955
+            $permittedFields[] = self::USER_FIELD_PASSWORD;
956
+            $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
957
+            if (
958
+                $this->config->getSystemValue('force_language', false) === false
959
+                || $this->groupManager->isAdmin($currentLoggedInUser->getUID())
960
+                || $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
961
+            ) {
962
+                $permittedFields[] = self::USER_FIELD_LANGUAGE;
963
+            }
964
+
965
+            if (
966
+                $this->config->getSystemValue('force_locale', false) === false
967
+                || $this->groupManager->isAdmin($currentLoggedInUser->getUID())
968
+                || $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
969
+            ) {
970
+                $permittedFields[] = self::USER_FIELD_LOCALE;
971
+                $permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
972
+            }
973
+
974
+            $permittedFields[] = IAccountManager::PROPERTY_PHONE;
975
+            $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
976
+            $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
977
+            $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
978
+            $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
979
+            $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
980
+            $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
981
+            $permittedFields[] = IAccountManager::PROPERTY_ROLE;
982
+            $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
983
+            $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
984
+            $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
985
+            $permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE;
986
+            $permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
987
+
988
+            $permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
989
+            $permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
990
+            $permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
991
+            $permittedFields[] = IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX;
992
+            $permittedFields[] = IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX;
993
+            $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX;
994
+            $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX;
995
+            $permittedFields[] = IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX;
996
+            $permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
997
+            $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
998
+            $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
999
+            $permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX;
1000
+            $permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
1001
+            $permittedFields[] = IAccountManager::PROPERTY_PRONOUNS . self::SCOPE_SUFFIX;
1002
+
1003
+            // If admin they can edit their own quota and manager
1004
+            $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1005
+            $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1006
+            if ($isAdmin || $isDelegatedAdmin) {
1007
+                $permittedFields[] = self::USER_FIELD_QUOTA;
1008
+                $permittedFields[] = self::USER_FIELD_MANAGER;
1009
+            }
1010
+        } else {
1011
+            // Check if admin / subadmin
1012
+            $subAdminManager = $this->groupManager->getSubAdmin();
1013
+            if (
1014
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())
1015
+                || $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID()) && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')
1016
+                || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1017
+            ) {
1018
+                // They have permissions over the user
1019
+                if (
1020
+                    $targetUser->getBackend() instanceof ISetDisplayNameBackend
1021
+                    || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
1022
+                ) {
1023
+                    $permittedFields[] = self::USER_FIELD_DISPLAYNAME;
1024
+                    $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
1025
+                }
1026
+                $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
1027
+                $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
1028
+                $permittedFields[] = self::USER_FIELD_PASSWORD;
1029
+                $permittedFields[] = self::USER_FIELD_LANGUAGE;
1030
+                $permittedFields[] = self::USER_FIELD_LOCALE;
1031
+                $permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
1032
+                $permittedFields[] = IAccountManager::PROPERTY_PHONE;
1033
+                $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
1034
+                $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
1035
+                $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
1036
+                $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
1037
+                $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
1038
+                $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
1039
+                $permittedFields[] = IAccountManager::PROPERTY_ROLE;
1040
+                $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
1041
+                $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
1042
+                $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
1043
+                $permittedFields[] = IAccountManager::PROPERTY_PRONOUNS;
1044
+                $permittedFields[] = self::USER_FIELD_QUOTA;
1045
+                $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
1046
+                $permittedFields[] = self::USER_FIELD_MANAGER;
1047
+            } else {
1048
+                // No rights
1049
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1050
+            }
1051
+        }
1052
+        // Check if permitted to edit this field
1053
+        if (!in_array($key, $permittedFields)) {
1054
+            throw new OCSException('', 113);
1055
+        }
1056
+        // Process the edit
1057
+        switch ($key) {
1058
+            case self::USER_FIELD_DISPLAYNAME:
1059
+            case IAccountManager::PROPERTY_DISPLAYNAME:
1060
+                try {
1061
+                    $targetUser->setDisplayName($value);
1062
+                } catch (InvalidArgumentException $e) {
1063
+                    throw new OCSException($e->getMessage(), 101);
1064
+                }
1065
+                break;
1066
+            case self::USER_FIELD_QUOTA:
1067
+                $quota = $value;
1068
+                if ($quota !== 'none' && $quota !== 'default') {
1069
+                    if (is_numeric($quota)) {
1070
+                        $quota = (float)$quota;
1071
+                    } else {
1072
+                        $quota = Util::computerFileSize($quota);
1073
+                    }
1074
+                    if ($quota === false) {
1075
+                        throw new OCSException($this->l10n->t('Invalid quota value: %1$s', [$value]), 101);
1076
+                    }
1077
+                    if ($quota === -1) {
1078
+                        $quota = 'none';
1079
+                    } else {
1080
+                        $maxQuota = (int)$this->config->getAppValue('files', 'max_quota', '-1');
1081
+                        if ($maxQuota !== -1 && $quota > $maxQuota) {
1082
+                            throw new OCSException($this->l10n->t('Invalid quota value. %1$s is exceeding the maximum quota', [$value]), 101);
1083
+                        }
1084
+                        $quota = Util::humanFileSize($quota);
1085
+                    }
1086
+                }
1087
+                // no else block because quota can be set to 'none' in previous if
1088
+                if ($quota === 'none') {
1089
+                    $allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
1090
+                    if (!$allowUnlimitedQuota) {
1091
+                        throw new OCSException($this->l10n->t('Unlimited quota is forbidden on this instance'), 101);
1092
+                    }
1093
+                }
1094
+                $targetUser->setQuota($quota);
1095
+                break;
1096
+            case self::USER_FIELD_MANAGER:
1097
+                $targetUser->setManagerUids([$value]);
1098
+                break;
1099
+            case self::USER_FIELD_PASSWORD:
1100
+                try {
1101
+                    if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {
1102
+                        throw new OCSException($this->l10n->t('Invalid password value'), 101);
1103
+                    }
1104
+                    if (!$targetUser->canChangePassword()) {
1105
+                        throw new OCSException($this->l10n->t('Setting the password is not supported by the users backend'), 112);
1106
+                    }
1107
+                    $targetUser->setPassword($value);
1108
+                } catch (HintException $e) { // password policy error
1109
+                    throw new OCSException($e->getHint(), 107);
1110
+                }
1111
+                break;
1112
+            case self::USER_FIELD_LANGUAGE:
1113
+                $languagesCodes = $this->l10nFactory->findAvailableLanguages();
1114
+                if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
1115
+                    throw new OCSException($this->l10n->t('Invalid language'), 101);
1116
+                }
1117
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
1118
+                break;
1119
+            case self::USER_FIELD_LOCALE:
1120
+                if (!$this->l10nFactory->localeExists($value)) {
1121
+                    throw new OCSException($this->l10n->t('Invalid locale'), 101);
1122
+                }
1123
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
1124
+                break;
1125
+            case self::USER_FIELD_FIRST_DAY_OF_WEEK:
1126
+                $intValue = (int)$value;
1127
+                if ($intValue < -1 || $intValue > 6) {
1128
+                    throw new OCSException($this->l10n->t('Invalid first day of week'), 101);
1129
+                }
1130
+                if ($intValue === -1) {
1131
+                    $this->config->deleteUserValue($targetUser->getUID(), 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK);
1132
+                } else {
1133
+                    $this->config->setUserValue($targetUser->getUID(), 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK, $value);
1134
+                }
1135
+                break;
1136
+            case self::USER_FIELD_NOTIFICATION_EMAIL:
1137
+                $success = false;
1138
+                if ($value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)) {
1139
+                    try {
1140
+                        $targetUser->setPrimaryEMailAddress($value);
1141
+                        $success = true;
1142
+                    } catch (InvalidArgumentException $e) {
1143
+                        $this->logger->info(
1144
+                            'Cannot set primary email, because provided address is not verified',
1145
+                            [
1146
+                                'app' => 'provisioning_api',
1147
+                                'exception' => $e,
1148
+                            ]
1149
+                        );
1150
+                    }
1151
+                }
1152
+                if (!$success) {
1153
+                    throw new OCSException('', 101);
1154
+                }
1155
+                break;
1156
+            case IAccountManager::PROPERTY_EMAIL:
1157
+                $value = mb_strtolower(trim($value));
1158
+                if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
1159
+                    $targetUser->setSystemEMailAddress($value);
1160
+                } else {
1161
+                    throw new OCSException('', 101);
1162
+                }
1163
+                break;
1164
+            case IAccountManager::COLLECTION_EMAIL:
1165
+                $value = mb_strtolower(trim($value));
1166
+                if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getSystemEMailAddress()) {
1167
+                    $userAccount = $this->accountManager->getAccount($targetUser);
1168
+                    $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
1169
+
1170
+                    if ($mailCollection->getPropertyByValue($value)) {
1171
+                        throw new OCSException('', 101);
1172
+                    }
1173
+
1174
+                    $mailCollection->addPropertyWithDefaults($value);
1175
+                    $this->accountManager->updateAccount($userAccount);
1176
+                } else {
1177
+                    throw new OCSException('', 101);
1178
+                }
1179
+                break;
1180
+            case IAccountManager::PROPERTY_PHONE:
1181
+            case IAccountManager::PROPERTY_ADDRESS:
1182
+            case IAccountManager::PROPERTY_WEBSITE:
1183
+            case IAccountManager::PROPERTY_TWITTER:
1184
+            case IAccountManager::PROPERTY_BLUESKY:
1185
+            case IAccountManager::PROPERTY_FEDIVERSE:
1186
+            case IAccountManager::PROPERTY_ORGANISATION:
1187
+            case IAccountManager::PROPERTY_ROLE:
1188
+            case IAccountManager::PROPERTY_HEADLINE:
1189
+            case IAccountManager::PROPERTY_BIOGRAPHY:
1190
+            case IAccountManager::PROPERTY_BIRTHDATE:
1191
+            case IAccountManager::PROPERTY_PRONOUNS:
1192
+                $userAccount = $this->accountManager->getAccount($targetUser);
1193
+                try {
1194
+                    $userProperty = $userAccount->getProperty($key);
1195
+                    if ($userProperty->getValue() !== $value) {
1196
+                        try {
1197
+                            $userProperty->setValue($value);
1198
+                            if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
1199
+                                $this->knownUserService->deleteByContactUserId($targetUser->getUID());
1200
+                            }
1201
+                        } catch (InvalidArgumentException $e) {
1202
+                            throw new OCSException('Invalid ' . $e->getMessage(), 101);
1203
+                        }
1204
+                    }
1205
+                } catch (PropertyDoesNotExistException $e) {
1206
+                    $userAccount->setProperty($key, $value, IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED);
1207
+                }
1208
+                try {
1209
+                    $this->accountManager->updateAccount($userAccount);
1210
+                } catch (InvalidArgumentException $e) {
1211
+                    throw new OCSException('Invalid ' . $e->getMessage(), 101);
1212
+                }
1213
+                break;
1214
+            case IAccountManager::PROPERTY_PROFILE_ENABLED:
1215
+                $userAccount = $this->accountManager->getAccount($targetUser);
1216
+                try {
1217
+                    $userProperty = $userAccount->getProperty($key);
1218
+                    if ($userProperty->getValue() !== $value) {
1219
+                        $userProperty->setValue($value);
1220
+                    }
1221
+                } catch (PropertyDoesNotExistException $e) {
1222
+                    $userAccount->setProperty($key, $value, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
1223
+                }
1224
+                $this->accountManager->updateAccount($userAccount);
1225
+                break;
1226
+            case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX:
1227
+            case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX:
1228
+            case IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX:
1229
+            case IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX:
1230
+            case IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX:
1231
+            case IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX:
1232
+            case IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX:
1233
+            case IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX:
1234
+            case IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX:
1235
+            case IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX:
1236
+            case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
1237
+            case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
1238
+            case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
1239
+            case IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX:
1240
+            case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
1241
+            case IAccountManager::PROPERTY_PRONOUNS . self::SCOPE_SUFFIX:
1242
+                $propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
1243
+                $userAccount = $this->accountManager->getAccount($targetUser);
1244
+                $userProperty = $userAccount->getProperty($propertyName);
1245
+                if ($userProperty->getScope() !== $value) {
1246
+                    try {
1247
+                        $userProperty->setScope($value);
1248
+                        $this->accountManager->updateAccount($userAccount);
1249
+                    } catch (InvalidArgumentException $e) {
1250
+                        throw new OCSException('Invalid ' . $e->getMessage(), 101);
1251
+                    }
1252
+                }
1253
+                break;
1254
+            default:
1255
+                throw new OCSException('', 113);
1256
+        }
1257
+        return new DataResponse();
1258
+    }
1259
+
1260
+    /**
1261
+     * Wipe all devices of a user
1262
+     *
1263
+     * @param string $userId ID of the user
1264
+     *
1265
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1266
+     *
1267
+     * @throws OCSException
1268
+     *
1269
+     * 200: Wiped all user devices successfully
1270
+     */
1271
+    #[PasswordConfirmationRequired]
1272
+    #[NoAdminRequired]
1273
+    public function wipeUserDevices(string $userId): DataResponse {
1274
+        /** @var IUser $currentLoggedInUser */
1275
+        $currentLoggedInUser = $this->userSession->getUser();
1276
+
1277
+        $targetUser = $this->userManager->get($userId);
1278
+
1279
+        if ($targetUser === null) {
1280
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1281
+        }
1282
+
1283
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1284
+            throw new OCSException('', 101);
1285
+        }
1286
+
1287
+        // If not permitted
1288
+        $subAdminManager = $this->groupManager->getSubAdmin();
1289
+        $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1290
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1291
+        if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1292
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1293
+        }
1294
+
1295
+        $this->remoteWipe->markAllTokensForWipe($targetUser);
1296
+
1297
+        return new DataResponse();
1298
+    }
1299
+
1300
+    /**
1301
+     * Delete a user
1302
+     *
1303
+     * @param string $userId ID of the user
1304
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1305
+     * @throws OCSException
1306
+     *
1307
+     * 200: User deleted successfully
1308
+     */
1309
+    #[PasswordConfirmationRequired]
1310
+    #[NoAdminRequired]
1311
+    public function deleteUser(string $userId): DataResponse {
1312
+        $currentLoggedInUser = $this->userSession->getUser();
1313
+
1314
+        $targetUser = $this->userManager->get($userId);
1315
+
1316
+        if ($targetUser === null) {
1317
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1318
+        }
1319
+
1320
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1321
+            throw new OCSException('', 101);
1322
+        }
1323
+
1324
+        // If not permitted
1325
+        $subAdminManager = $this->groupManager->getSubAdmin();
1326
+        $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1327
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1328
+        if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1329
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1330
+        }
1331
+
1332
+        // Go ahead with the delete
1333
+        if ($targetUser->delete()) {
1334
+            return new DataResponse();
1335
+        } else {
1336
+            throw new OCSException('', 101);
1337
+        }
1338
+    }
1339
+
1340
+    /**
1341
+     * Disable a user
1342
+     *
1343
+     * @param string $userId ID of the user
1344
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1345
+     * @throws OCSException
1346
+     *
1347
+     * 200: User disabled successfully
1348
+     */
1349
+    #[PasswordConfirmationRequired]
1350
+    #[NoAdminRequired]
1351
+    public function disableUser(string $userId): DataResponse {
1352
+        return $this->setEnabled($userId, false);
1353
+    }
1354
+
1355
+    /**
1356
+     * Enable a user
1357
+     *
1358
+     * @param string $userId ID of the user
1359
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1360
+     * @throws OCSException
1361
+     *
1362
+     * 200: User enabled successfully
1363
+     */
1364
+    #[PasswordConfirmationRequired]
1365
+    #[NoAdminRequired]
1366
+    public function enableUser(string $userId): DataResponse {
1367
+        return $this->setEnabled($userId, true);
1368
+    }
1369
+
1370
+    /**
1371
+     * @param string $userId
1372
+     * @param bool $value
1373
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1374
+     * @throws OCSException
1375
+     */
1376
+    private function setEnabled(string $userId, bool $value): DataResponse {
1377
+        $currentLoggedInUser = $this->userSession->getUser();
1378
+
1379
+        $targetUser = $this->userManager->get($userId);
1380
+        if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
1381
+            throw new OCSException('', 101);
1382
+        }
1383
+
1384
+        // If not permitted
1385
+        $subAdminManager = $this->groupManager->getSubAdmin();
1386
+        $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1387
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1388
+        if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1389
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1390
+        }
1391
+
1392
+        // enable/disable the user now
1393
+        $targetUser->setEnabled($value);
1394
+        return new DataResponse();
1395
+    }
1396
+
1397
+    /**
1398
+     * @NoSubAdminRequired
1399
+     *
1400
+     * Get a list of groups the user belongs to
1401
+     *
1402
+     * @param string $userId ID of the user
1403
+     * @return DataResponse<Http::STATUS_OK, array{groups: list<string>}, array{}>
1404
+     * @throws OCSException
1405
+     *
1406
+     * 200: Users groups returned
1407
+     */
1408
+    #[NoAdminRequired]
1409
+    public function getUsersGroups(string $userId): DataResponse {
1410
+        $loggedInUser = $this->userSession->getUser();
1411
+
1412
+        $targetUser = $this->userManager->get($userId);
1413
+        if ($targetUser === null) {
1414
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1415
+        }
1416
+
1417
+        $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1418
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1419
+        if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1420
+            // Self lookup or admin lookup
1421
+            return new DataResponse([
1422
+                'groups' => $this->groupManager->getUserGroupIds($targetUser)
1423
+            ]);
1424
+        } else {
1425
+            $subAdminManager = $this->groupManager->getSubAdmin();
1426
+
1427
+            // Looking up someone else
1428
+            if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1429
+                // Return the group that the method caller is subadmin of for the user in question
1430
+                $groups = array_values(array_intersect(
1431
+                    array_map(static fn (IGroup $group) => $group->getGID(), $subAdminManager->getSubAdminsGroups($loggedInUser)),
1432
+                    $this->groupManager->getUserGroupIds($targetUser)
1433
+                ));
1434
+                return new DataResponse(['groups' => $groups]);
1435
+            } else {
1436
+                // Not permitted
1437
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1438
+            }
1439
+        }
1440
+    }
1441
+
1442
+    /**
1443
+     * @NoSubAdminRequired
1444
+     *
1445
+     * Get a list of groups with details
1446
+     *
1447
+     * @param string $userId ID of the user
1448
+     * @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
1449
+     * @throws OCSException
1450
+     *
1451
+     * 200: Users groups returned
1452
+     */
1453
+    #[NoAdminRequired]
1454
+    public function getUsersGroupsDetails(string $userId): DataResponse {
1455
+        $loggedInUser = $this->userSession->getUser();
1456
+
1457
+        $targetUser = $this->userManager->get($userId);
1458
+        if ($targetUser === null) {
1459
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1460
+        }
1461
+
1462
+        $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1463
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1464
+        if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1465
+            // Self lookup or admin lookup
1466
+            $groups = array_map(
1467
+                function (Group $group) {
1468
+                    return [
1469
+                        'id' => $group->getGID(),
1470
+                        'displayname' => $group->getDisplayName(),
1471
+                        'usercount' => $group->count(),
1472
+                        'disabled' => $group->countDisabled(),
1473
+                        'canAdd' => $group->canAddUser(),
1474
+                        'canRemove' => $group->canRemoveUser(),
1475
+                    ];
1476
+                },
1477
+                array_values($this->groupManager->getUserGroups($targetUser)),
1478
+            );
1479
+            return new DataResponse([
1480
+                'groups' => $groups,
1481
+            ]);
1482
+        } else {
1483
+            $subAdminManager = $this->groupManager->getSubAdmin();
1484
+
1485
+            // Looking up someone else
1486
+            if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1487
+                // Return the group that the method caller is subadmin of for the user in question
1488
+                $gids = array_values(array_intersect(
1489
+                    array_map(
1490
+                        static fn (IGroup $group) => $group->getGID(),
1491
+                        $subAdminManager->getSubAdminsGroups($loggedInUser),
1492
+                    ),
1493
+                    $this->groupManager->getUserGroupIds($targetUser)
1494
+                ));
1495
+                $groups = array_map(
1496
+                    function (string $gid) {
1497
+                        $group = $this->groupManager->get($gid);
1498
+                        return [
1499
+                            'id' => $group->getGID(),
1500
+                            'displayname' => $group->getDisplayName(),
1501
+                            'usercount' => $group->count(),
1502
+                            'disabled' => $group->countDisabled(),
1503
+                            'canAdd' => $group->canAddUser(),
1504
+                            'canRemove' => $group->canRemoveUser(),
1505
+                        ];
1506
+                    },
1507
+                    $gids,
1508
+                );
1509
+                return new DataResponse([
1510
+                    'groups' => $groups,
1511
+                ]);
1512
+            } else {
1513
+                // Not permitted
1514
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1515
+            }
1516
+        }
1517
+    }
1518
+
1519
+    /**
1520
+     * @NoSubAdminRequired
1521
+     *
1522
+     * Get a list of the groups the user is a subadmin of, with details
1523
+     *
1524
+     * @param string $userId ID of the user
1525
+     * @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
1526
+     * @throws OCSException
1527
+     *
1528
+     * 200: Users subadmin groups returned
1529
+     */
1530
+    #[NoAdminRequired]
1531
+    public function getUserSubAdminGroupsDetails(string $userId): DataResponse {
1532
+        $loggedInUser = $this->userSession->getUser();
1533
+
1534
+        $targetUser = $this->userManager->get($userId);
1535
+        if ($targetUser === null) {
1536
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1537
+        }
1538
+
1539
+        $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1540
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1541
+        if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
1542
+            $subAdminManager = $this->groupManager->getSubAdmin();
1543
+            $groups = array_map(
1544
+                function (IGroup $group) {
1545
+                    return [
1546
+                        'id' => $group->getGID(),
1547
+                        'displayname' => $group->getDisplayName(),
1548
+                        'usercount' => $group->count(),
1549
+                        'disabled' => $group->countDisabled(),
1550
+                        'canAdd' => $group->canAddUser(),
1551
+                        'canRemove' => $group->canRemoveUser(),
1552
+                    ];
1553
+                },
1554
+                array_values($subAdminManager->getSubAdminsGroups($targetUser)),
1555
+            );
1556
+            return new DataResponse([
1557
+                'groups' => $groups,
1558
+            ]);
1559
+        }
1560
+        throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1561
+    }
1562
+
1563
+    /**
1564
+     * Add a user to a group
1565
+     *
1566
+     * @param string $userId ID of the user
1567
+     * @param string $groupid ID of the group
1568
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1569
+     * @throws OCSException
1570
+     *
1571
+     * 200: User added to group successfully
1572
+     */
1573
+    #[PasswordConfirmationRequired]
1574
+    #[NoAdminRequired]
1575
+    public function addToGroup(string $userId, string $groupid = ''): DataResponse {
1576
+        if ($groupid === '') {
1577
+            throw new OCSException('', 101);
1578
+        }
1579
+
1580
+        $group = $this->groupManager->get($groupid);
1581
+        $targetUser = $this->userManager->get($userId);
1582
+        if ($group === null) {
1583
+            throw new OCSException('', 102);
1584
+        }
1585
+        if ($targetUser === null) {
1586
+            throw new OCSException('', 103);
1587
+        }
1588
+
1589
+        // If they're not an admin, check they are a subadmin of the group in question
1590
+        $loggedInUser = $this->userSession->getUser();
1591
+        $subAdminManager = $this->groupManager->getSubAdmin();
1592
+        $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1593
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1594
+        if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1595
+            throw new OCSException('', 104);
1596
+        }
1597
+
1598
+        // Add user to group
1599
+        $group->addUser($targetUser);
1600
+        return new DataResponse();
1601
+    }
1602
+
1603
+    /**
1604
+     * Remove a user from a group
1605
+     *
1606
+     * @param string $userId ID of the user
1607
+     * @param string $groupid ID of the group
1608
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1609
+     * @throws OCSException
1610
+     *
1611
+     * 200: User removed from group successfully
1612
+     */
1613
+    #[PasswordConfirmationRequired]
1614
+    #[NoAdminRequired]
1615
+    public function removeFromGroup(string $userId, string $groupid): DataResponse {
1616
+        $loggedInUser = $this->userSession->getUser();
1617
+
1618
+        if ($groupid === null || trim($groupid) === '') {
1619
+            throw new OCSException('', 101);
1620
+        }
1621
+
1622
+        $group = $this->groupManager->get($groupid);
1623
+        if ($group === null) {
1624
+            throw new OCSException('', 102);
1625
+        }
1626
+
1627
+        $targetUser = $this->userManager->get($userId);
1628
+        if ($targetUser === null) {
1629
+            throw new OCSException('', 103);
1630
+        }
1631
+
1632
+        // If they're not an admin, check they are a subadmin of the group in question
1633
+        $subAdminManager = $this->groupManager->getSubAdmin();
1634
+        $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
1635
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
1636
+        if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1637
+            throw new OCSException('', 104);
1638
+        }
1639
+
1640
+        // Check they aren't removing themselves from 'admin' or their 'subadmin; group
1641
+        if ($targetUser->getUID() === $loggedInUser->getUID()) {
1642
+            if ($isAdmin || $isDelegatedAdmin) {
1643
+                if ($group->getGID() === 'admin') {
1644
+                    throw new OCSException($this->l10n->t('Cannot remove yourself from the admin group'), 105);
1645
+                }
1646
+            } else {
1647
+                // Not an admin, so the user must be a subadmin of this group, but that is not allowed.
1648
+                throw new OCSException($this->l10n->t('Cannot remove yourself from this group as you are a sub-admin'), 105);
1649
+            }
1650
+        } elseif (!($isAdmin || $isDelegatedAdmin)) {
1651
+            /** @var IGroup[] $subAdminGroups */
1652
+            $subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1653
+            $subAdminGroups = array_map(function (IGroup $subAdminGroup) {
1654
+                return $subAdminGroup->getGID();
1655
+            }, $subAdminGroups);
1656
+            $userGroups = $this->groupManager->getUserGroupIds($targetUser);
1657
+            $userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
1658
+
1659
+            if (count($userSubAdminGroups) <= 1) {
1660
+                // Subadmin must not be able to remove a user from all their subadmin groups.
1661
+                throw new OCSException($this->l10n->t('Not viable to remove user from the last group you are sub-admin of'), 105);
1662
+            }
1663
+        }
1664
+
1665
+        // Remove user from group
1666
+        $group->removeUser($targetUser);
1667
+        return new DataResponse();
1668
+    }
1669
+
1670
+    /**
1671
+     * Make a user a subadmin of a group
1672
+     *
1673
+     * @param string $userId ID of the user
1674
+     * @param string $groupid ID of the group
1675
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1676
+     * @throws OCSException
1677
+     *
1678
+     * 200: User added as group subadmin successfully
1679
+     */
1680
+    #[AuthorizedAdminSetting(settings:Users::class)]
1681
+    #[PasswordConfirmationRequired]
1682
+    public function addSubAdmin(string $userId, string $groupid): DataResponse {
1683
+        $group = $this->groupManager->get($groupid);
1684
+        $user = $this->userManager->get($userId);
1685
+
1686
+        // Check if the user exists
1687
+        if ($user === null) {
1688
+            throw new OCSException($this->l10n->t('User does not exist'), 101);
1689
+        }
1690
+        // Check if group exists
1691
+        if ($group === null) {
1692
+            throw new OCSException($this->l10n->t('Group does not exist'), 102);
1693
+        }
1694
+        // Check if trying to make subadmin of admin group
1695
+        if ($group->getGID() === 'admin') {
1696
+            throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103);
1697
+        }
1698
+
1699
+        $subAdminManager = $this->groupManager->getSubAdmin();
1700
+
1701
+        // We cannot be subadmin twice
1702
+        if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
1703
+            return new DataResponse();
1704
+        }
1705
+        // Go
1706
+        $subAdminManager->createSubAdmin($user, $group);
1707
+        return new DataResponse();
1708
+    }
1709
+
1710
+    /**
1711
+     * Remove a user from the subadmins of a group
1712
+     *
1713
+     * @param string $userId ID of the user
1714
+     * @param string $groupid ID of the group
1715
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1716
+     * @throws OCSException
1717
+     *
1718
+     * 200: User removed as group subadmin successfully
1719
+     */
1720
+    #[AuthorizedAdminSetting(settings:Users::class)]
1721
+    #[PasswordConfirmationRequired]
1722
+    public function removeSubAdmin(string $userId, string $groupid): DataResponse {
1723
+        $group = $this->groupManager->get($groupid);
1724
+        $user = $this->userManager->get($userId);
1725
+        $subAdminManager = $this->groupManager->getSubAdmin();
1726
+
1727
+        // Check if the user exists
1728
+        if ($user === null) {
1729
+            throw new OCSException($this->l10n->t('User does not exist'), 101);
1730
+        }
1731
+        // Check if the group exists
1732
+        if ($group === null) {
1733
+            throw new OCSException($this->l10n->t('Group does not exist'), 101);
1734
+        }
1735
+        // Check if they are a subadmin of this said group
1736
+        if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
1737
+            throw new OCSException($this->l10n->t('User is not a sub-admin of this group'), 102);
1738
+        }
1739
+
1740
+        // Go
1741
+        $subAdminManager->deleteSubAdmin($user, $group);
1742
+        return new DataResponse();
1743
+    }
1744
+
1745
+    /**
1746
+     * Get the groups a user is a subadmin of
1747
+     *
1748
+     * @param string $userId ID if the user
1749
+     * @return DataResponse<Http::STATUS_OK, list<string>, array{}>
1750
+     * @throws OCSException
1751
+     *
1752
+     * 200: User subadmin groups returned
1753
+     */
1754
+    #[AuthorizedAdminSetting(settings:Users::class)]
1755
+    public function getUserSubAdminGroups(string $userId): DataResponse {
1756
+        $groups = $this->getUserSubAdminGroupsData($userId);
1757
+        return new DataResponse($groups);
1758
+    }
1759
+
1760
+    /**
1761
+     * Resend the welcome message
1762
+     *
1763
+     * @param string $userId ID if the user
1764
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
1765
+     * @throws OCSException
1766
+     *
1767
+     * 200: Resent welcome message successfully
1768
+     */
1769
+    #[PasswordConfirmationRequired]
1770
+    #[NoAdminRequired]
1771
+    public function resendWelcomeMessage(string $userId): DataResponse {
1772
+        $currentLoggedInUser = $this->userSession->getUser();
1773
+
1774
+        $targetUser = $this->userManager->get($userId);
1775
+        if ($targetUser === null) {
1776
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1777
+        }
1778
+
1779
+        // Check if admin / subadmin
1780
+        $subAdminManager = $this->groupManager->getSubAdmin();
1781
+        $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
1782
+        $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
1783
+        if (
1784
+            !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1785
+            && !($isAdmin || $isDelegatedAdmin)
1786
+        ) {
1787
+            // No rights
1788
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1789
+        }
1790
+
1791
+        $email = $targetUser->getEMailAddress();
1792
+        if ($email === '' || $email === null) {
1793
+            throw new OCSException($this->l10n->t('Email address not available'), 101);
1794
+        }
1795
+
1796
+        try {
1797
+            if ($this->config->getUserValue($targetUser->getUID(), 'core', 'lostpassword')) {
1798
+                $emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, true);
1799
+            } else {
1800
+                $emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
1801
+            }
1802
+
1803
+            $this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
1804
+        } catch (\Exception $e) {
1805
+            $this->logger->error(
1806
+                "Can't send new user mail to $email",
1807
+                [
1808
+                    'app' => 'settings',
1809
+                    'exception' => $e,
1810
+                ]
1811
+            );
1812
+            throw new OCSException($this->l10n->t('Sending email failed'), 102);
1813
+        }
1814
+
1815
+        return new DataResponse();
1816
+    }
1817 1817
 }
Please login to merge, or discard this patch.
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +843 added lines, -843 removed lines patch added patch discarded remove patch
@@ -52,847 +52,847 @@
 block discarded – undo
52 52
  * @package OC\Accounts
53 53
  */
54 54
 class AccountManager implements IAccountManager {
55
-	use TAccountsHelper;
56
-
57
-	use TProfileHelper;
58
-
59
-	private string $table = 'accounts';
60
-	private string $dataTable = 'accounts_data';
61
-	private ?IL10N $l10n = null;
62
-	private CappedMemoryCache $internalCache;
63
-
64
-	/**
65
-	 * The list of default scopes for each property.
66
-	 */
67
-	public const DEFAULT_SCOPES = [
68
-		self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
69
-		self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
70
-		self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
71
-		self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL,
72
-		self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
73
-		self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
74
-		self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
75
-		self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
76
-		self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
77
-		self::PROPERTY_PHONE => self::SCOPE_LOCAL,
78
-		self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED,
79
-		self::PROPERTY_ROLE => self::SCOPE_LOCAL,
80
-		self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
81
-		self::PROPERTY_BLUESKY => self::SCOPE_LOCAL,
82
-		self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
83
-	];
84
-
85
-	public function __construct(
86
-		private IDBConnection $connection,
87
-		private IConfig $config,
88
-		private IEventDispatcher $dispatcher,
89
-		private IJobList $jobList,
90
-		private LoggerInterface $logger,
91
-		private IVerificationToken $verificationToken,
92
-		private IMailer $mailer,
93
-		private Defaults $defaults,
94
-		private IFactory $l10nFactory,
95
-		private IURLGenerator $urlGenerator,
96
-		private ICrypto $crypto,
97
-		private IPhoneNumberUtil $phoneNumberUtil,
98
-		private IClientService $clientService,
99
-	) {
100
-		$this->internalCache = new CappedMemoryCache();
101
-	}
102
-
103
-	/**
104
-	 * @param IAccountProperty[] $properties
105
-	 */
106
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
107
-		foreach ($properties as $property) {
108
-			if (strlen($property->getValue()) > 2048) {
109
-				if ($throwOnData) {
110
-					throw new InvalidArgumentException($property->getName());
111
-				} else {
112
-					$property->setValue('');
113
-				}
114
-			}
115
-		}
116
-	}
117
-
118
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
119
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
120
-			throw new InvalidArgumentException('scope');
121
-		}
122
-
123
-		if (
124
-			$property->getScope() === self::SCOPE_PRIVATE
125
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
126
-		) {
127
-			if ($throwOnData) {
128
-				// v2-private is not available for these fields
129
-				throw new InvalidArgumentException('scope');
130
-			} else {
131
-				// default to local
132
-				$property->setScope(self::SCOPE_LOCAL);
133
-			}
134
-		} else {
135
-			$property->setScope($property->getScope());
136
-		}
137
-	}
138
-
139
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
140
-		if ($oldUserData === null) {
141
-			$oldUserData = $this->getUser($user, false);
142
-		}
143
-
144
-		$updated = true;
145
-
146
-		if ($oldUserData !== $data) {
147
-			$this->updateExistingUser($user, $data, $oldUserData);
148
-		} else {
149
-			// nothing needs to be done if new and old data set are the same
150
-			$updated = false;
151
-		}
152
-
153
-		if ($updated) {
154
-			$this->dispatcher->dispatchTyped(new UserUpdatedEvent(
155
-				$user,
156
-				$data,
157
-			));
158
-		}
159
-
160
-		return $data;
161
-	}
162
-
163
-	/**
164
-	 * delete user from accounts table
165
-	 */
166
-	public function deleteUser(IUser $user): void {
167
-		$uid = $user->getUID();
168
-		$query = $this->connection->getQueryBuilder();
169
-		$query->delete($this->table)
170
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
171
-			->executeStatement();
172
-
173
-		$this->deleteUserData($user);
174
-	}
175
-
176
-	/**
177
-	 * delete user from accounts table
178
-	 */
179
-	public function deleteUserData(IUser $user): void {
180
-		$uid = $user->getUID();
181
-		$query = $this->connection->getQueryBuilder();
182
-		$query->delete($this->dataTable)
183
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
184
-			->executeStatement();
185
-	}
186
-
187
-	/**
188
-	 * get stored data from a given user
189
-	 */
190
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
191
-		$uid = $user->getUID();
192
-		$query = $this->connection->getQueryBuilder();
193
-		$query->select('data')
194
-			->from($this->table)
195
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
196
-			->setParameter('uid', $uid);
197
-		$result = $query->executeQuery();
198
-		$accountData = $result->fetchAll();
199
-		$result->closeCursor();
200
-
201
-		if (empty($accountData)) {
202
-			$userData = $this->buildDefaultUserRecord($user);
203
-			if ($insertIfNotExists) {
204
-				$this->insertNewUser($user, $userData);
205
-			}
206
-			return $userData;
207
-		}
208
-
209
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
210
-		if ($userDataArray === null || $userDataArray === []) {
211
-			return $this->buildDefaultUserRecord($user);
212
-		}
213
-
214
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
215
-	}
216
-
217
-	public function searchUsers(string $property, array $values): array {
218
-		// the value col is limited to 255 bytes. It is used for searches only.
219
-		$values = array_map(function (string $value) {
220
-			return Util::shortenMultibyteString($value, 255);
221
-		}, $values);
222
-		$chunks = array_chunk($values, 500);
223
-		$query = $this->connection->getQueryBuilder();
224
-		$query->select('*')
225
-			->from($this->dataTable)
226
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
227
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
228
-
229
-		$matches = [];
230
-		foreach ($chunks as $chunk) {
231
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
232
-			$result = $query->executeQuery();
233
-
234
-			while ($row = $result->fetch()) {
235
-				$matches[$row['uid']] = $row['value'];
236
-			}
237
-			$result->closeCursor();
238
-		}
239
-
240
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
241
-
242
-		return array_flip($result);
243
-	}
244
-
245
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
246
-		return match ($property) {
247
-			IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
248
-			default => [],
249
-		};
250
-	}
251
-
252
-	/**
253
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
254
-	 */
255
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
256
-		try {
257
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
258
-		} catch (PropertyDoesNotExistException $e) {
259
-			return;
260
-		}
261
-
262
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
263
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
264
-
265
-		if ($oldMail !== $property->getValue()) {
266
-			$this->jobList->add(
267
-				VerifyUserData::class,
268
-				[
269
-					'verificationCode' => '',
270
-					'data' => $property->getValue(),
271
-					'type' => self::PROPERTY_EMAIL,
272
-					'uid' => $updatedAccount->getUser()->getUID(),
273
-					'try' => 0,
274
-					'lastRun' => time()
275
-				]
276
-			);
277
-
278
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
279
-		}
280
-	}
281
-
282
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
283
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
284
-		foreach ($mailCollection->getProperties() as $property) {
285
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
286
-				continue;
287
-			}
288
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
289
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
290
-			}
291
-		}
292
-	}
293
-
294
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
295
-		$ref = \substr(hash('sha256', $email), 0, 8);
296
-		$key = $this->crypto->encrypt($email);
297
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
298
-
299
-		$link = $this->urlGenerator->linkToRouteAbsolute(
300
-			'provisioning_api.Verification.verifyMail',
301
-			[
302
-				'userId' => $user->getUID(),
303
-				'token' => $token,
304
-				'key' => $key
305
-			]
306
-		);
307
-
308
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
309
-			'link' => $link,
310
-		]);
311
-
312
-		if (!$this->l10n) {
313
-			$this->l10n = $this->l10nFactory->get('core');
314
-		}
315
-
316
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
317
-		$emailTemplate->addHeader();
318
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
319
-
320
-		$emailTemplate->addBodyText(
321
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
322
-			$this->l10n->t('Click the following link to confirm your email.')
323
-		);
324
-
325
-		$emailTemplate->addBodyButton(
326
-			htmlspecialchars($this->l10n->t('Confirm your email')),
327
-			$link,
328
-			false
329
-		);
330
-		$emailTemplate->addFooter();
331
-
332
-		try {
333
-			$message = $this->mailer->createMessage();
334
-			$message->setTo([$email => $user->getDisplayName()]);
335
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
336
-			$message->useTemplate($emailTemplate);
337
-			$this->mailer->send($message);
338
-		} catch (Exception $e) {
339
-			// Log the exception and continue
340
-			$this->logger->info('Failed to send verification mail', [
341
-				'app' => 'core',
342
-				'exception' => $e
343
-			]);
344
-			return false;
345
-		}
346
-		return true;
347
-	}
348
-
349
-	/**
350
-	 * Make sure that all expected data are set
351
-	 */
352
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
353
-		foreach ($defaultUserData as $defaultDataItem) {
354
-			// If property does not exist, initialize it
355
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
356
-			if ($userDataIndex === false) {
357
-				$userData[] = $defaultDataItem;
358
-				continue;
359
-			}
360
-
361
-			// Merge and extend default missing values
362
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
363
-		}
364
-
365
-		return $userData;
366
-	}
367
-
368
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
369
-		static $propertiesVerifiableByLookupServer = [
370
-			self::PROPERTY_TWITTER,
371
-			self::PROPERTY_FEDIVERSE,
372
-			self::PROPERTY_WEBSITE,
373
-			self::PROPERTY_EMAIL,
374
-		];
375
-
376
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
377
-			try {
378
-				$property = $updatedAccount->getProperty($propertyName);
379
-			} catch (PropertyDoesNotExistException $e) {
380
-				continue;
381
-			}
382
-			$wasVerified = isset($oldData[$propertyName])
383
-				&& isset($oldData[$propertyName]['verified'])
384
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
385
-			if ((!isset($oldData[$propertyName])
386
-					|| !isset($oldData[$propertyName]['value'])
387
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
388
-				&& ($property->getVerified() !== self::NOT_VERIFIED
389
-					|| $wasVerified)
390
-			) {
391
-				$property->setVerified(self::NOT_VERIFIED);
392
-			}
393
-		}
394
-	}
395
-
396
-	/**
397
-	 * add new user to accounts table
398
-	 */
399
-	protected function insertNewUser(IUser $user, array $data): void {
400
-		$uid = $user->getUID();
401
-		$jsonEncodedData = $this->prepareJson($data);
402
-		$query = $this->connection->getQueryBuilder();
403
-		$query->insert($this->table)
404
-			->values(
405
-				[
406
-					'uid' => $query->createNamedParameter($uid),
407
-					'data' => $query->createNamedParameter($jsonEncodedData),
408
-				]
409
-			)
410
-			->executeStatement();
411
-
412
-		$this->deleteUserData($user);
413
-		$this->writeUserData($user, $data);
414
-	}
415
-
416
-	protected function prepareJson(array $data): string {
417
-		$preparedData = [];
418
-		foreach ($data as $dataRow) {
419
-			$propertyName = $dataRow['name'];
420
-			unset($dataRow['name']);
421
-
422
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
423
-				// do not write default value, save DB space
424
-				unset($dataRow['locallyVerified']);
425
-			}
426
-
427
-			if (!$this->isCollection($propertyName)) {
428
-				$preparedData[$propertyName] = $dataRow;
429
-				continue;
430
-			}
431
-			if (!isset($preparedData[$propertyName])) {
432
-				$preparedData[$propertyName] = [];
433
-			}
434
-			$preparedData[$propertyName][] = $dataRow;
435
-		}
436
-		return json_encode($preparedData);
437
-	}
438
-
439
-	protected function importFromJson(string $json, string $userId): ?array {
440
-		$result = [];
441
-		$jsonArray = json_decode($json, true);
442
-		$jsonError = json_last_error();
443
-		if ($jsonError !== JSON_ERROR_NONE) {
444
-			$this->logger->critical(
445
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
446
-				[
447
-					'uid' => $userId,
448
-					'json_error' => $jsonError
449
-				]
450
-			);
451
-			return null;
452
-		}
453
-		foreach ($jsonArray as $propertyName => $row) {
454
-			if (!$this->isCollection($propertyName)) {
455
-				$result[] = array_merge($row, ['name' => $propertyName]);
456
-				continue;
457
-			}
458
-			foreach ($row as $singleRow) {
459
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
460
-			}
461
-		}
462
-		return $result;
463
-	}
464
-
465
-	/**
466
-	 * Update existing user in accounts table
467
-	 */
468
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
469
-		$uid = $user->getUID();
470
-		$jsonEncodedData = $this->prepareJson($data);
471
-		$query = $this->connection->getQueryBuilder();
472
-		$query->update($this->table)
473
-			->set('data', $query->createNamedParameter($jsonEncodedData))
474
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
475
-			->executeStatement();
476
-
477
-		$this->deleteUserData($user);
478
-		$this->writeUserData($user, $data);
479
-	}
480
-
481
-	protected function writeUserData(IUser $user, array $data): void {
482
-		$query = $this->connection->getQueryBuilder();
483
-		$query->insert($this->dataTable)
484
-			->values(
485
-				[
486
-					'uid' => $query->createNamedParameter($user->getUID()),
487
-					'name' => $query->createParameter('name'),
488
-					'value' => $query->createParameter('value'),
489
-				]
490
-			);
491
-		$this->writeUserDataProperties($query, $data);
492
-	}
493
-
494
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
495
-		foreach ($data as $property) {
496
-			if ($property['name'] === self::PROPERTY_AVATAR) {
497
-				continue;
498
-			}
499
-
500
-			// the value col is limited to 255 bytes. It is used for searches only.
501
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
502
-
503
-			$query->setParameter('name', $property['name'])
504
-				->setParameter('value', $value);
505
-			$query->executeStatement();
506
-		}
507
-	}
508
-
509
-	/**
510
-	 * build default user record in case not data set exists yet
511
-	 */
512
-	protected function buildDefaultUserRecord(IUser $user): array {
513
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
514
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
515
-		}, ARRAY_FILTER_USE_BOTH));
516
-
517
-		return [
518
-			[
519
-				'name' => self::PROPERTY_DISPLAYNAME,
520
-				'value' => $user->getDisplayName(),
521
-				// Display name must be at least SCOPE_LOCAL
522
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
523
-				'verified' => self::NOT_VERIFIED,
524
-			],
525
-
526
-			[
527
-				'name' => self::PROPERTY_ADDRESS,
528
-				'value' => '',
529
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
530
-				'verified' => self::NOT_VERIFIED,
531
-			],
532
-
533
-			[
534
-				'name' => self::PROPERTY_WEBSITE,
535
-				'value' => '',
536
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
537
-				'verified' => self::NOT_VERIFIED,
538
-			],
539
-
540
-			[
541
-				'name' => self::PROPERTY_EMAIL,
542
-				'value' => $user->getEMailAddress(),
543
-				// Email must be at least SCOPE_LOCAL
544
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
545
-				'verified' => self::NOT_VERIFIED,
546
-			],
547
-
548
-			[
549
-				'name' => self::PROPERTY_AVATAR,
550
-				'scope' => $scopes[self::PROPERTY_AVATAR],
551
-			],
552
-
553
-			[
554
-				'name' => self::PROPERTY_PHONE,
555
-				'value' => '',
556
-				'scope' => $scopes[self::PROPERTY_PHONE],
557
-				'verified' => self::NOT_VERIFIED,
558
-			],
559
-
560
-			[
561
-				'name' => self::PROPERTY_TWITTER,
562
-				'value' => '',
563
-				'scope' => $scopes[self::PROPERTY_TWITTER],
564
-				'verified' => self::NOT_VERIFIED,
565
-			],
566
-
567
-			[
568
-				'name' => self::PROPERTY_BLUESKY,
569
-				'value' => '',
570
-				'scope' => $scopes[self::PROPERTY_BLUESKY],
571
-				'verified' => self::NOT_VERIFIED,
572
-			],
573
-
574
-			[
575
-				'name' => self::PROPERTY_FEDIVERSE,
576
-				'value' => '',
577
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
578
-				'verified' => self::NOT_VERIFIED,
579
-			],
580
-
581
-			[
582
-				'name' => self::PROPERTY_ORGANISATION,
583
-				'value' => '',
584
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
585
-			],
586
-
587
-			[
588
-				'name' => self::PROPERTY_ROLE,
589
-				'value' => '',
590
-				'scope' => $scopes[self::PROPERTY_ROLE],
591
-			],
592
-
593
-			[
594
-				'name' => self::PROPERTY_HEADLINE,
595
-				'value' => '',
596
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
597
-			],
598
-
599
-			[
600
-				'name' => self::PROPERTY_BIOGRAPHY,
601
-				'value' => '',
602
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
603
-			],
604
-
605
-			[
606
-				'name' => self::PROPERTY_BIRTHDATE,
607
-				'value' => '',
608
-				'scope' => $scopes[self::PROPERTY_BIRTHDATE],
609
-			],
610
-
611
-			[
612
-				'name' => self::PROPERTY_PROFILE_ENABLED,
613
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
614
-			],
615
-
616
-			[
617
-				'name' => self::PROPERTY_PRONOUNS,
618
-				'value' => '',
619
-				'scope' => $scopes[self::PROPERTY_PRONOUNS],
620
-			],
621
-		];
622
-	}
623
-
624
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
625
-		$collection = $account->getPropertyCollection($data['name']);
626
-
627
-		$p = new AccountProperty(
628
-			$data['name'],
629
-			$data['value'] ?? '',
630
-			$data['scope'] ?? self::SCOPE_LOCAL,
631
-			$data['verified'] ?? self::NOT_VERIFIED,
632
-			''
633
-		);
634
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
635
-		$collection->addProperty($p);
636
-
637
-		return $collection;
638
-	}
639
-
640
-	private function parseAccountData(IUser $user, $data): Account {
641
-		$account = new Account($user);
642
-		foreach ($data as $accountData) {
643
-			if ($this->isCollection($accountData['name'])) {
644
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
645
-			} else {
646
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
647
-				if (isset($accountData['locallyVerified'])) {
648
-					$property = $account->getProperty($accountData['name']);
649
-					$property->setLocallyVerified($accountData['locallyVerified']);
650
-				}
651
-			}
652
-		}
653
-		return $account;
654
-	}
655
-
656
-	public function getAccount(IUser $user): IAccount {
657
-		$cached = $this->internalCache->get($user->getUID());
658
-		if ($cached !== null) {
659
-			return $cached;
660
-		}
661
-		$account = $this->parseAccountData($user, $this->getUser($user));
662
-		if ($user->getBackend() instanceof IGetDisplayNameBackend) {
663
-			$property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
664
-			$account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
665
-		}
666
-		$this->internalCache->set($user->getUID(), $account);
667
-		return $account;
668
-	}
669
-
670
-	/**
671
-	 * Converts value (phone number) in E.164 format when it was a valid number
672
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
673
-	 */
674
-	protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
675
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
676
-
677
-		if ($defaultRegion === '') {
678
-			// When no default region is set, only +49… numbers are valid
679
-			if (!str_starts_with($property->getValue(), '+')) {
680
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
-			}
682
-
683
-			$defaultRegion = 'EN';
684
-		}
685
-
686
-		$phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
687
-		if ($phoneNumber === null) {
688
-			throw new InvalidArgumentException(self::PROPERTY_PHONE);
689
-		}
690
-		$property->setValue($phoneNumber);
691
-	}
692
-
693
-	/**
694
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
695
-	 */
696
-	private function sanitizePropertyWebsite(IAccountProperty $property): void {
697
-		$parts = parse_url($property->getValue());
698
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
699
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
700
-		}
701
-
702
-		if (!isset($parts['host']) || $parts['host'] === '') {
703
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
704
-		}
705
-	}
706
-
707
-	/**
708
-	 * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
709
-	 */
710
-	private function sanitizePropertyTwitter(IAccountProperty $property): void {
711
-		if ($property->getName() === self::PROPERTY_TWITTER) {
712
-			$matches = [];
713
-			// twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
714
-			if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
715
-				throw new InvalidArgumentException(self::PROPERTY_TWITTER);
716
-			}
717
-
718
-			// drop the leading @ if any to make it the valid handle
719
-			$property->setValue($matches[1]);
720
-
721
-		}
722
-	}
723
-
724
-	private function validateBlueSkyHandle(string $text): bool {
725
-		if ($text === '') {
726
-			return true;
727
-		}
728
-
729
-		$lowerText = strtolower($text);
730
-
731
-		if ($lowerText === 'bsky.social') {
732
-			// "bsky.social" itself is not a valid handle
733
-			return false;
734
-		}
735
-
736
-		if (str_ends_with($lowerText, '.bsky.social')) {
737
-			$parts = explode('.', $lowerText);
738
-
739
-			// Must be exactly: username.bsky.social → 3 parts
740
-			if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
741
-				return false;
742
-			}
743
-
744
-			$username = $parts[0];
745
-
746
-			// Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
747
-			return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
748
-		}
749
-
750
-		// Allow custom domains (Bluesky handle via personal domain)
751
-		return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
752
-	}
753
-
754
-
755
-	private function sanitizePropertyBluesky(IAccountProperty $property): void {
756
-		if ($property->getName() === self::PROPERTY_BLUESKY) {
757
-			if (!$this->validateBlueSkyHandle($property->getValue())) {
758
-				throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
759
-			}
760
-
761
-			$property->setValue($property->getValue());
762
-		}
763
-	}
764
-
765
-	/**
766
-	 * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
767
-	 */
768
-	private function sanitizePropertyFediverse(IAccountProperty $property): void {
769
-		if ($property->getName() === self::PROPERTY_FEDIVERSE) {
770
-			$matches = [];
771
-			if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
772
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
773
-			}
774
-
775
-			[, $username, $instance] = $matches;
776
-			$validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
777
-			if ($validated !== $instance) {
778
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
779
-			}
780
-
781
-			if ($this->config->getSystemValueBool('has_internet_connection', true)) {
782
-				$client = $this->clientService->newClient();
783
-
784
-				try {
785
-					// try the public account lookup API of mastodon
786
-					$response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
787
-					// should be a json response with account information
788
-					$data = $response->getBody();
789
-					if (is_resource($data)) {
790
-						$data = stream_get_contents($data);
791
-					}
792
-					$decoded = json_decode($data, true);
793
-					// ensure the username is the same the user passed
794
-					// in this case we can assume this is a valid fediverse server and account
795
-					if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
796
-						throw new InvalidArgumentException();
797
-					}
798
-					// check for activitypub link
799
-					if (is_array($decoded['links']) && isset($decoded['links'])) {
800
-						$found = false;
801
-						foreach ($decoded['links'] as $link) {
802
-							// have application/activity+json or application/ld+json
803
-							if (isset($link['type']) && (
804
-								$link['type'] === 'application/activity+json'
805
-								|| $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
806
-							)) {
807
-								$found = true;
808
-								break;
809
-							}
810
-						}
811
-						if (!$found) {
812
-							throw new InvalidArgumentException();
813
-						}
814
-					}
815
-				} catch (InvalidArgumentException) {
816
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
817
-				} catch (\Exception $error) {
818
-					$this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
819
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
820
-				}
821
-			}
822
-
823
-			$property->setValue("$username@$instance");
824
-		}
825
-	}
826
-
827
-	public function updateAccount(IAccount $account): void {
828
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
829
-		try {
830
-			$property = $account->getProperty(self::PROPERTY_PHONE);
831
-			if ($property->getValue() !== '') {
832
-				$this->sanitizePropertyPhoneNumber($property);
833
-			}
834
-		} catch (PropertyDoesNotExistException $e) {
835
-			//  valid case, nothing to do
836
-		}
837
-
838
-		try {
839
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
840
-			if ($property->getValue() !== '') {
841
-				$this->sanitizePropertyWebsite($property);
842
-			}
843
-		} catch (PropertyDoesNotExistException $e) {
844
-			//  valid case, nothing to do
845
-		}
846
-
847
-		try {
848
-			$property = $account->getProperty(self::PROPERTY_TWITTER);
849
-			if ($property->getValue() !== '') {
850
-				$this->sanitizePropertyTwitter($property);
851
-			}
852
-		} catch (PropertyDoesNotExistException $e) {
853
-			//  valid case, nothing to do
854
-		}
855
-
856
-		try {
857
-			$property = $account->getProperty(self::PROPERTY_BLUESKY);
858
-			if ($property->getValue() !== '') {
859
-				$this->sanitizePropertyBluesky($property);
860
-			}
861
-		} catch (PropertyDoesNotExistException $e) {
862
-			//  valid case, nothing to do
863
-		}
864
-
865
-		try {
866
-			$property = $account->getProperty(self::PROPERTY_FEDIVERSE);
867
-			if ($property->getValue() !== '') {
868
-				$this->sanitizePropertyFediverse($property);
869
-			}
870
-		} catch (PropertyDoesNotExistException $e) {
871
-			//  valid case, nothing to do
872
-		}
873
-
874
-		foreach ($account->getAllProperties() as $property) {
875
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
876
-		}
877
-
878
-		$oldData = $this->getUser($account->getUser(), false);
879
-		$this->updateVerificationStatus($account, $oldData);
880
-		$this->checkEmailVerification($account, $oldData);
881
-		$this->checkLocalEmailVerification($account, $oldData);
882
-
883
-		$data = [];
884
-		foreach ($account->getAllProperties() as $property) {
885
-			/** @var IAccountProperty $property */
886
-			$data[] = [
887
-				'name' => $property->getName(),
888
-				'value' => $property->getValue(),
889
-				'scope' => $property->getScope(),
890
-				'verified' => $property->getVerified(),
891
-				'locallyVerified' => $property->getLocallyVerified(),
892
-			];
893
-		}
894
-
895
-		$this->updateUser($account->getUser(), $data, $oldData, true);
896
-		$this->internalCache->set($account->getUser()->getUID(), $account);
897
-	}
55
+    use TAccountsHelper;
56
+
57
+    use TProfileHelper;
58
+
59
+    private string $table = 'accounts';
60
+    private string $dataTable = 'accounts_data';
61
+    private ?IL10N $l10n = null;
62
+    private CappedMemoryCache $internalCache;
63
+
64
+    /**
65
+     * The list of default scopes for each property.
66
+     */
67
+    public const DEFAULT_SCOPES = [
68
+        self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
69
+        self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
70
+        self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
71
+        self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL,
72
+        self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
73
+        self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
74
+        self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
75
+        self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
76
+        self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
77
+        self::PROPERTY_PHONE => self::SCOPE_LOCAL,
78
+        self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED,
79
+        self::PROPERTY_ROLE => self::SCOPE_LOCAL,
80
+        self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
81
+        self::PROPERTY_BLUESKY => self::SCOPE_LOCAL,
82
+        self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
83
+    ];
84
+
85
+    public function __construct(
86
+        private IDBConnection $connection,
87
+        private IConfig $config,
88
+        private IEventDispatcher $dispatcher,
89
+        private IJobList $jobList,
90
+        private LoggerInterface $logger,
91
+        private IVerificationToken $verificationToken,
92
+        private IMailer $mailer,
93
+        private Defaults $defaults,
94
+        private IFactory $l10nFactory,
95
+        private IURLGenerator $urlGenerator,
96
+        private ICrypto $crypto,
97
+        private IPhoneNumberUtil $phoneNumberUtil,
98
+        private IClientService $clientService,
99
+    ) {
100
+        $this->internalCache = new CappedMemoryCache();
101
+    }
102
+
103
+    /**
104
+     * @param IAccountProperty[] $properties
105
+     */
106
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
107
+        foreach ($properties as $property) {
108
+            if (strlen($property->getValue()) > 2048) {
109
+                if ($throwOnData) {
110
+                    throw new InvalidArgumentException($property->getName());
111
+                } else {
112
+                    $property->setValue('');
113
+                }
114
+            }
115
+        }
116
+    }
117
+
118
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
119
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
120
+            throw new InvalidArgumentException('scope');
121
+        }
122
+
123
+        if (
124
+            $property->getScope() === self::SCOPE_PRIVATE
125
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
126
+        ) {
127
+            if ($throwOnData) {
128
+                // v2-private is not available for these fields
129
+                throw new InvalidArgumentException('scope');
130
+            } else {
131
+                // default to local
132
+                $property->setScope(self::SCOPE_LOCAL);
133
+            }
134
+        } else {
135
+            $property->setScope($property->getScope());
136
+        }
137
+    }
138
+
139
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
140
+        if ($oldUserData === null) {
141
+            $oldUserData = $this->getUser($user, false);
142
+        }
143
+
144
+        $updated = true;
145
+
146
+        if ($oldUserData !== $data) {
147
+            $this->updateExistingUser($user, $data, $oldUserData);
148
+        } else {
149
+            // nothing needs to be done if new and old data set are the same
150
+            $updated = false;
151
+        }
152
+
153
+        if ($updated) {
154
+            $this->dispatcher->dispatchTyped(new UserUpdatedEvent(
155
+                $user,
156
+                $data,
157
+            ));
158
+        }
159
+
160
+        return $data;
161
+    }
162
+
163
+    /**
164
+     * delete user from accounts table
165
+     */
166
+    public function deleteUser(IUser $user): void {
167
+        $uid = $user->getUID();
168
+        $query = $this->connection->getQueryBuilder();
169
+        $query->delete($this->table)
170
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
171
+            ->executeStatement();
172
+
173
+        $this->deleteUserData($user);
174
+    }
175
+
176
+    /**
177
+     * delete user from accounts table
178
+     */
179
+    public function deleteUserData(IUser $user): void {
180
+        $uid = $user->getUID();
181
+        $query = $this->connection->getQueryBuilder();
182
+        $query->delete($this->dataTable)
183
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
184
+            ->executeStatement();
185
+    }
186
+
187
+    /**
188
+     * get stored data from a given user
189
+     */
190
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
191
+        $uid = $user->getUID();
192
+        $query = $this->connection->getQueryBuilder();
193
+        $query->select('data')
194
+            ->from($this->table)
195
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
196
+            ->setParameter('uid', $uid);
197
+        $result = $query->executeQuery();
198
+        $accountData = $result->fetchAll();
199
+        $result->closeCursor();
200
+
201
+        if (empty($accountData)) {
202
+            $userData = $this->buildDefaultUserRecord($user);
203
+            if ($insertIfNotExists) {
204
+                $this->insertNewUser($user, $userData);
205
+            }
206
+            return $userData;
207
+        }
208
+
209
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
210
+        if ($userDataArray === null || $userDataArray === []) {
211
+            return $this->buildDefaultUserRecord($user);
212
+        }
213
+
214
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
215
+    }
216
+
217
+    public function searchUsers(string $property, array $values): array {
218
+        // the value col is limited to 255 bytes. It is used for searches only.
219
+        $values = array_map(function (string $value) {
220
+            return Util::shortenMultibyteString($value, 255);
221
+        }, $values);
222
+        $chunks = array_chunk($values, 500);
223
+        $query = $this->connection->getQueryBuilder();
224
+        $query->select('*')
225
+            ->from($this->dataTable)
226
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
227
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
228
+
229
+        $matches = [];
230
+        foreach ($chunks as $chunk) {
231
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
232
+            $result = $query->executeQuery();
233
+
234
+            while ($row = $result->fetch()) {
235
+                $matches[$row['uid']] = $row['value'];
236
+            }
237
+            $result->closeCursor();
238
+        }
239
+
240
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
241
+
242
+        return array_flip($result);
243
+    }
244
+
245
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
246
+        return match ($property) {
247
+            IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
248
+            default => [],
249
+        };
250
+    }
251
+
252
+    /**
253
+     * check if we need to ask the server for email verification, if yes we create a cronjob
254
+     */
255
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
256
+        try {
257
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
258
+        } catch (PropertyDoesNotExistException $e) {
259
+            return;
260
+        }
261
+
262
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
263
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
264
+
265
+        if ($oldMail !== $property->getValue()) {
266
+            $this->jobList->add(
267
+                VerifyUserData::class,
268
+                [
269
+                    'verificationCode' => '',
270
+                    'data' => $property->getValue(),
271
+                    'type' => self::PROPERTY_EMAIL,
272
+                    'uid' => $updatedAccount->getUser()->getUID(),
273
+                    'try' => 0,
274
+                    'lastRun' => time()
275
+                ]
276
+            );
277
+
278
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
279
+        }
280
+    }
281
+
282
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
283
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
284
+        foreach ($mailCollection->getProperties() as $property) {
285
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
286
+                continue;
287
+            }
288
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
289
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
290
+            }
291
+        }
292
+    }
293
+
294
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
295
+        $ref = \substr(hash('sha256', $email), 0, 8);
296
+        $key = $this->crypto->encrypt($email);
297
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
298
+
299
+        $link = $this->urlGenerator->linkToRouteAbsolute(
300
+            'provisioning_api.Verification.verifyMail',
301
+            [
302
+                'userId' => $user->getUID(),
303
+                'token' => $token,
304
+                'key' => $key
305
+            ]
306
+        );
307
+
308
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
309
+            'link' => $link,
310
+        ]);
311
+
312
+        if (!$this->l10n) {
313
+            $this->l10n = $this->l10nFactory->get('core');
314
+        }
315
+
316
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
317
+        $emailTemplate->addHeader();
318
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
319
+
320
+        $emailTemplate->addBodyText(
321
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
322
+            $this->l10n->t('Click the following link to confirm your email.')
323
+        );
324
+
325
+        $emailTemplate->addBodyButton(
326
+            htmlspecialchars($this->l10n->t('Confirm your email')),
327
+            $link,
328
+            false
329
+        );
330
+        $emailTemplate->addFooter();
331
+
332
+        try {
333
+            $message = $this->mailer->createMessage();
334
+            $message->setTo([$email => $user->getDisplayName()]);
335
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
336
+            $message->useTemplate($emailTemplate);
337
+            $this->mailer->send($message);
338
+        } catch (Exception $e) {
339
+            // Log the exception and continue
340
+            $this->logger->info('Failed to send verification mail', [
341
+                'app' => 'core',
342
+                'exception' => $e
343
+            ]);
344
+            return false;
345
+        }
346
+        return true;
347
+    }
348
+
349
+    /**
350
+     * Make sure that all expected data are set
351
+     */
352
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
353
+        foreach ($defaultUserData as $defaultDataItem) {
354
+            // If property does not exist, initialize it
355
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
356
+            if ($userDataIndex === false) {
357
+                $userData[] = $defaultDataItem;
358
+                continue;
359
+            }
360
+
361
+            // Merge and extend default missing values
362
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
363
+        }
364
+
365
+        return $userData;
366
+    }
367
+
368
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
369
+        static $propertiesVerifiableByLookupServer = [
370
+            self::PROPERTY_TWITTER,
371
+            self::PROPERTY_FEDIVERSE,
372
+            self::PROPERTY_WEBSITE,
373
+            self::PROPERTY_EMAIL,
374
+        ];
375
+
376
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
377
+            try {
378
+                $property = $updatedAccount->getProperty($propertyName);
379
+            } catch (PropertyDoesNotExistException $e) {
380
+                continue;
381
+            }
382
+            $wasVerified = isset($oldData[$propertyName])
383
+                && isset($oldData[$propertyName]['verified'])
384
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
385
+            if ((!isset($oldData[$propertyName])
386
+                    || !isset($oldData[$propertyName]['value'])
387
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
388
+                && ($property->getVerified() !== self::NOT_VERIFIED
389
+                    || $wasVerified)
390
+            ) {
391
+                $property->setVerified(self::NOT_VERIFIED);
392
+            }
393
+        }
394
+    }
395
+
396
+    /**
397
+     * add new user to accounts table
398
+     */
399
+    protected function insertNewUser(IUser $user, array $data): void {
400
+        $uid = $user->getUID();
401
+        $jsonEncodedData = $this->prepareJson($data);
402
+        $query = $this->connection->getQueryBuilder();
403
+        $query->insert($this->table)
404
+            ->values(
405
+                [
406
+                    'uid' => $query->createNamedParameter($uid),
407
+                    'data' => $query->createNamedParameter($jsonEncodedData),
408
+                ]
409
+            )
410
+            ->executeStatement();
411
+
412
+        $this->deleteUserData($user);
413
+        $this->writeUserData($user, $data);
414
+    }
415
+
416
+    protected function prepareJson(array $data): string {
417
+        $preparedData = [];
418
+        foreach ($data as $dataRow) {
419
+            $propertyName = $dataRow['name'];
420
+            unset($dataRow['name']);
421
+
422
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
423
+                // do not write default value, save DB space
424
+                unset($dataRow['locallyVerified']);
425
+            }
426
+
427
+            if (!$this->isCollection($propertyName)) {
428
+                $preparedData[$propertyName] = $dataRow;
429
+                continue;
430
+            }
431
+            if (!isset($preparedData[$propertyName])) {
432
+                $preparedData[$propertyName] = [];
433
+            }
434
+            $preparedData[$propertyName][] = $dataRow;
435
+        }
436
+        return json_encode($preparedData);
437
+    }
438
+
439
+    protected function importFromJson(string $json, string $userId): ?array {
440
+        $result = [];
441
+        $jsonArray = json_decode($json, true);
442
+        $jsonError = json_last_error();
443
+        if ($jsonError !== JSON_ERROR_NONE) {
444
+            $this->logger->critical(
445
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
446
+                [
447
+                    'uid' => $userId,
448
+                    'json_error' => $jsonError
449
+                ]
450
+            );
451
+            return null;
452
+        }
453
+        foreach ($jsonArray as $propertyName => $row) {
454
+            if (!$this->isCollection($propertyName)) {
455
+                $result[] = array_merge($row, ['name' => $propertyName]);
456
+                continue;
457
+            }
458
+            foreach ($row as $singleRow) {
459
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
460
+            }
461
+        }
462
+        return $result;
463
+    }
464
+
465
+    /**
466
+     * Update existing user in accounts table
467
+     */
468
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
469
+        $uid = $user->getUID();
470
+        $jsonEncodedData = $this->prepareJson($data);
471
+        $query = $this->connection->getQueryBuilder();
472
+        $query->update($this->table)
473
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
474
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
475
+            ->executeStatement();
476
+
477
+        $this->deleteUserData($user);
478
+        $this->writeUserData($user, $data);
479
+    }
480
+
481
+    protected function writeUserData(IUser $user, array $data): void {
482
+        $query = $this->connection->getQueryBuilder();
483
+        $query->insert($this->dataTable)
484
+            ->values(
485
+                [
486
+                    'uid' => $query->createNamedParameter($user->getUID()),
487
+                    'name' => $query->createParameter('name'),
488
+                    'value' => $query->createParameter('value'),
489
+                ]
490
+            );
491
+        $this->writeUserDataProperties($query, $data);
492
+    }
493
+
494
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
495
+        foreach ($data as $property) {
496
+            if ($property['name'] === self::PROPERTY_AVATAR) {
497
+                continue;
498
+            }
499
+
500
+            // the value col is limited to 255 bytes. It is used for searches only.
501
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
502
+
503
+            $query->setParameter('name', $property['name'])
504
+                ->setParameter('value', $value);
505
+            $query->executeStatement();
506
+        }
507
+    }
508
+
509
+    /**
510
+     * build default user record in case not data set exists yet
511
+     */
512
+    protected function buildDefaultUserRecord(IUser $user): array {
513
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
514
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
515
+        }, ARRAY_FILTER_USE_BOTH));
516
+
517
+        return [
518
+            [
519
+                'name' => self::PROPERTY_DISPLAYNAME,
520
+                'value' => $user->getDisplayName(),
521
+                // Display name must be at least SCOPE_LOCAL
522
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
523
+                'verified' => self::NOT_VERIFIED,
524
+            ],
525
+
526
+            [
527
+                'name' => self::PROPERTY_ADDRESS,
528
+                'value' => '',
529
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
530
+                'verified' => self::NOT_VERIFIED,
531
+            ],
532
+
533
+            [
534
+                'name' => self::PROPERTY_WEBSITE,
535
+                'value' => '',
536
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
537
+                'verified' => self::NOT_VERIFIED,
538
+            ],
539
+
540
+            [
541
+                'name' => self::PROPERTY_EMAIL,
542
+                'value' => $user->getEMailAddress(),
543
+                // Email must be at least SCOPE_LOCAL
544
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
545
+                'verified' => self::NOT_VERIFIED,
546
+            ],
547
+
548
+            [
549
+                'name' => self::PROPERTY_AVATAR,
550
+                'scope' => $scopes[self::PROPERTY_AVATAR],
551
+            ],
552
+
553
+            [
554
+                'name' => self::PROPERTY_PHONE,
555
+                'value' => '',
556
+                'scope' => $scopes[self::PROPERTY_PHONE],
557
+                'verified' => self::NOT_VERIFIED,
558
+            ],
559
+
560
+            [
561
+                'name' => self::PROPERTY_TWITTER,
562
+                'value' => '',
563
+                'scope' => $scopes[self::PROPERTY_TWITTER],
564
+                'verified' => self::NOT_VERIFIED,
565
+            ],
566
+
567
+            [
568
+                'name' => self::PROPERTY_BLUESKY,
569
+                'value' => '',
570
+                'scope' => $scopes[self::PROPERTY_BLUESKY],
571
+                'verified' => self::NOT_VERIFIED,
572
+            ],
573
+
574
+            [
575
+                'name' => self::PROPERTY_FEDIVERSE,
576
+                'value' => '',
577
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
578
+                'verified' => self::NOT_VERIFIED,
579
+            ],
580
+
581
+            [
582
+                'name' => self::PROPERTY_ORGANISATION,
583
+                'value' => '',
584
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
585
+            ],
586
+
587
+            [
588
+                'name' => self::PROPERTY_ROLE,
589
+                'value' => '',
590
+                'scope' => $scopes[self::PROPERTY_ROLE],
591
+            ],
592
+
593
+            [
594
+                'name' => self::PROPERTY_HEADLINE,
595
+                'value' => '',
596
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
597
+            ],
598
+
599
+            [
600
+                'name' => self::PROPERTY_BIOGRAPHY,
601
+                'value' => '',
602
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
603
+            ],
604
+
605
+            [
606
+                'name' => self::PROPERTY_BIRTHDATE,
607
+                'value' => '',
608
+                'scope' => $scopes[self::PROPERTY_BIRTHDATE],
609
+            ],
610
+
611
+            [
612
+                'name' => self::PROPERTY_PROFILE_ENABLED,
613
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
614
+            ],
615
+
616
+            [
617
+                'name' => self::PROPERTY_PRONOUNS,
618
+                'value' => '',
619
+                'scope' => $scopes[self::PROPERTY_PRONOUNS],
620
+            ],
621
+        ];
622
+    }
623
+
624
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
625
+        $collection = $account->getPropertyCollection($data['name']);
626
+
627
+        $p = new AccountProperty(
628
+            $data['name'],
629
+            $data['value'] ?? '',
630
+            $data['scope'] ?? self::SCOPE_LOCAL,
631
+            $data['verified'] ?? self::NOT_VERIFIED,
632
+            ''
633
+        );
634
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
635
+        $collection->addProperty($p);
636
+
637
+        return $collection;
638
+    }
639
+
640
+    private function parseAccountData(IUser $user, $data): Account {
641
+        $account = new Account($user);
642
+        foreach ($data as $accountData) {
643
+            if ($this->isCollection($accountData['name'])) {
644
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
645
+            } else {
646
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
647
+                if (isset($accountData['locallyVerified'])) {
648
+                    $property = $account->getProperty($accountData['name']);
649
+                    $property->setLocallyVerified($accountData['locallyVerified']);
650
+                }
651
+            }
652
+        }
653
+        return $account;
654
+    }
655
+
656
+    public function getAccount(IUser $user): IAccount {
657
+        $cached = $this->internalCache->get($user->getUID());
658
+        if ($cached !== null) {
659
+            return $cached;
660
+        }
661
+        $account = $this->parseAccountData($user, $this->getUser($user));
662
+        if ($user->getBackend() instanceof IGetDisplayNameBackend) {
663
+            $property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
664
+            $account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
665
+        }
666
+        $this->internalCache->set($user->getUID(), $account);
667
+        return $account;
668
+    }
669
+
670
+    /**
671
+     * Converts value (phone number) in E.164 format when it was a valid number
672
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
673
+     */
674
+    protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
675
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
676
+
677
+        if ($defaultRegion === '') {
678
+            // When no default region is set, only +49… numbers are valid
679
+            if (!str_starts_with($property->getValue(), '+')) {
680
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
+            }
682
+
683
+            $defaultRegion = 'EN';
684
+        }
685
+
686
+        $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
687
+        if ($phoneNumber === null) {
688
+            throw new InvalidArgumentException(self::PROPERTY_PHONE);
689
+        }
690
+        $property->setValue($phoneNumber);
691
+    }
692
+
693
+    /**
694
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
695
+     */
696
+    private function sanitizePropertyWebsite(IAccountProperty $property): void {
697
+        $parts = parse_url($property->getValue());
698
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
699
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
700
+        }
701
+
702
+        if (!isset($parts['host']) || $parts['host'] === '') {
703
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
704
+        }
705
+    }
706
+
707
+    /**
708
+     * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
709
+     */
710
+    private function sanitizePropertyTwitter(IAccountProperty $property): void {
711
+        if ($property->getName() === self::PROPERTY_TWITTER) {
712
+            $matches = [];
713
+            // twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
714
+            if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
715
+                throw new InvalidArgumentException(self::PROPERTY_TWITTER);
716
+            }
717
+
718
+            // drop the leading @ if any to make it the valid handle
719
+            $property->setValue($matches[1]);
720
+
721
+        }
722
+    }
723
+
724
+    private function validateBlueSkyHandle(string $text): bool {
725
+        if ($text === '') {
726
+            return true;
727
+        }
728
+
729
+        $lowerText = strtolower($text);
730
+
731
+        if ($lowerText === 'bsky.social') {
732
+            // "bsky.social" itself is not a valid handle
733
+            return false;
734
+        }
735
+
736
+        if (str_ends_with($lowerText, '.bsky.social')) {
737
+            $parts = explode('.', $lowerText);
738
+
739
+            // Must be exactly: username.bsky.social → 3 parts
740
+            if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
741
+                return false;
742
+            }
743
+
744
+            $username = $parts[0];
745
+
746
+            // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
747
+            return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
748
+        }
749
+
750
+        // Allow custom domains (Bluesky handle via personal domain)
751
+        return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
752
+    }
753
+
754
+
755
+    private function sanitizePropertyBluesky(IAccountProperty $property): void {
756
+        if ($property->getName() === self::PROPERTY_BLUESKY) {
757
+            if (!$this->validateBlueSkyHandle($property->getValue())) {
758
+                throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
759
+            }
760
+
761
+            $property->setValue($property->getValue());
762
+        }
763
+    }
764
+
765
+    /**
766
+     * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
767
+     */
768
+    private function sanitizePropertyFediverse(IAccountProperty $property): void {
769
+        if ($property->getName() === self::PROPERTY_FEDIVERSE) {
770
+            $matches = [];
771
+            if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
772
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
773
+            }
774
+
775
+            [, $username, $instance] = $matches;
776
+            $validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
777
+            if ($validated !== $instance) {
778
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
779
+            }
780
+
781
+            if ($this->config->getSystemValueBool('has_internet_connection', true)) {
782
+                $client = $this->clientService->newClient();
783
+
784
+                try {
785
+                    // try the public account lookup API of mastodon
786
+                    $response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
787
+                    // should be a json response with account information
788
+                    $data = $response->getBody();
789
+                    if (is_resource($data)) {
790
+                        $data = stream_get_contents($data);
791
+                    }
792
+                    $decoded = json_decode($data, true);
793
+                    // ensure the username is the same the user passed
794
+                    // in this case we can assume this is a valid fediverse server and account
795
+                    if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
796
+                        throw new InvalidArgumentException();
797
+                    }
798
+                    // check for activitypub link
799
+                    if (is_array($decoded['links']) && isset($decoded['links'])) {
800
+                        $found = false;
801
+                        foreach ($decoded['links'] as $link) {
802
+                            // have application/activity+json or application/ld+json
803
+                            if (isset($link['type']) && (
804
+                                $link['type'] === 'application/activity+json'
805
+                                || $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
806
+                            )) {
807
+                                $found = true;
808
+                                break;
809
+                            }
810
+                        }
811
+                        if (!$found) {
812
+                            throw new InvalidArgumentException();
813
+                        }
814
+                    }
815
+                } catch (InvalidArgumentException) {
816
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
817
+                } catch (\Exception $error) {
818
+                    $this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
819
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
820
+                }
821
+            }
822
+
823
+            $property->setValue("$username@$instance");
824
+        }
825
+    }
826
+
827
+    public function updateAccount(IAccount $account): void {
828
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
829
+        try {
830
+            $property = $account->getProperty(self::PROPERTY_PHONE);
831
+            if ($property->getValue() !== '') {
832
+                $this->sanitizePropertyPhoneNumber($property);
833
+            }
834
+        } catch (PropertyDoesNotExistException $e) {
835
+            //  valid case, nothing to do
836
+        }
837
+
838
+        try {
839
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
840
+            if ($property->getValue() !== '') {
841
+                $this->sanitizePropertyWebsite($property);
842
+            }
843
+        } catch (PropertyDoesNotExistException $e) {
844
+            //  valid case, nothing to do
845
+        }
846
+
847
+        try {
848
+            $property = $account->getProperty(self::PROPERTY_TWITTER);
849
+            if ($property->getValue() !== '') {
850
+                $this->sanitizePropertyTwitter($property);
851
+            }
852
+        } catch (PropertyDoesNotExistException $e) {
853
+            //  valid case, nothing to do
854
+        }
855
+
856
+        try {
857
+            $property = $account->getProperty(self::PROPERTY_BLUESKY);
858
+            if ($property->getValue() !== '') {
859
+                $this->sanitizePropertyBluesky($property);
860
+            }
861
+        } catch (PropertyDoesNotExistException $e) {
862
+            //  valid case, nothing to do
863
+        }
864
+
865
+        try {
866
+            $property = $account->getProperty(self::PROPERTY_FEDIVERSE);
867
+            if ($property->getValue() !== '') {
868
+                $this->sanitizePropertyFediverse($property);
869
+            }
870
+        } catch (PropertyDoesNotExistException $e) {
871
+            //  valid case, nothing to do
872
+        }
873
+
874
+        foreach ($account->getAllProperties() as $property) {
875
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
876
+        }
877
+
878
+        $oldData = $this->getUser($account->getUser(), false);
879
+        $this->updateVerificationStatus($account, $oldData);
880
+        $this->checkEmailVerification($account, $oldData);
881
+        $this->checkLocalEmailVerification($account, $oldData);
882
+
883
+        $data = [];
884
+        foreach ($account->getAllProperties() as $property) {
885
+            /** @var IAccountProperty $property */
886
+            $data[] = [
887
+                'name' => $property->getName(),
888
+                'value' => $property->getValue(),
889
+                'scope' => $property->getScope(),
890
+                'verified' => $property->getVerified(),
891
+                'locallyVerified' => $property->getLocallyVerified(),
892
+            ];
893
+        }
894
+
895
+        $this->updateUser($account->getUser(), $data, $oldData, true);
896
+        $this->internalCache->set($account->getUser()->getUID(), $account);
897
+    }
898 898
 }
Please login to merge, or discard this patch.