Issues (2553)

lib/private/User/User.php (1 issue)

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author John Molakvoæ <[email protected]>
11
 * @author Jörn Friedrich Dreyer <[email protected]>
12
 * @author Julius Härtl <[email protected]>
13
 * @author Leon Klingele <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 *
20
 * @license AGPL-3.0
21
 *
22
 * This code is free software: you can redistribute it and/or modify
23
 * it under the terms of the GNU Affero General Public License, version 3,
24
 * as published by the Free Software Foundation.
25
 *
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
 * GNU Affero General Public License for more details.
30
 *
31
 * You should have received a copy of the GNU Affero General Public License, version 3,
32
 * along with this program. If not, see <http://www.gnu.org/licenses/>
33
 *
34
 */
35
namespace OC\User;
36
37
use InvalidArgumentException;
38
use OC\Accounts\AccountManager;
39
use OC\Avatar\AvatarManager;
40
use OC\Hooks\Emitter;
41
use OC_Helper;
42
use OCP\Accounts\IAccountManager;
43
use OCP\EventDispatcher\IEventDispatcher;
44
use OCP\Group\Events\BeforeUserRemovedEvent;
45
use OCP\Group\Events\UserRemovedEvent;
46
use OCP\IAvatarManager;
47
use OCP\IConfig;
48
use OCP\IImage;
49
use OCP\IURLGenerator;
50
use OCP\IUser;
51
use OCP\IUserBackend;
52
use OCP\User\Events\BeforeUserDeletedEvent;
53
use OCP\User\Events\UserDeletedEvent;
54
use OCP\User\GetQuotaEvent;
55
use OCP\User\Backend\ISetDisplayNameBackend;
56
use OCP\User\Backend\ISetPasswordBackend;
57
use OCP\User\Backend\IProvideAvatarBackend;
58
use OCP\User\Backend\IGetHomeBackend;
59
use OCP\UserInterface;
60
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
61
use Symfony\Component\EventDispatcher\GenericEvent;
62
use function json_decode;
63
use function json_encode;
64
65
class User implements IUser {
66
	private const CONFIG_KEY_MANAGERS = 'manager';
67
68
	/** @var IAccountManager */
69
	protected $accountManager;
70
	/** @var string */
71
	private $uid;
72
73
	/** @var string|null */
74
	private $displayName;
75
76
	/** @var UserInterface|null */
77
	private $backend;
78
	/** @var EventDispatcherInterface */
79
	private $legacyDispatcher;
80
81
	/** @var IEventDispatcher */
82
	private $dispatcher;
83
84
	/** @var bool|null */
85
	private $enabled;
86
87
	/** @var Emitter|Manager */
88
	private $emitter;
89
90
	/** @var string */
91
	private $home;
92
93
	/** @var int|null */
94
	private $lastLogin;
95
96
	/** @var \OCP\IConfig */
97
	private $config;
98
99
	/** @var IAvatarManager */
100
	private $avatarManager;
101
102
	/** @var IURLGenerator */
103
	private $urlGenerator;
104
105
	public function __construct(string $uid, ?UserInterface $backend, EventDispatcherInterface $dispatcher, $emitter = null, IConfig $config = null, $urlGenerator = null) {
106
		$this->uid = $uid;
107
		$this->backend = $backend;
108
		$this->legacyDispatcher = $dispatcher;
109
		$this->emitter = $emitter;
110
		if (is_null($config)) {
111
			$config = \OC::$server->getConfig();
112
		}
113
		$this->config = $config;
114
		$this->urlGenerator = $urlGenerator;
115
		if (is_null($this->urlGenerator)) {
116
			$this->urlGenerator = \OC::$server->getURLGenerator();
117
		}
118
		// TODO: inject
119
		$this->dispatcher = \OC::$server->query(IEventDispatcher::class);
120
	}
121
122
	/**
123
	 * get the user id
124
	 *
125
	 * @return string
126
	 */
127
	public function getUID() {
128
		return $this->uid;
129
	}
130
131
	/**
132
	 * get the display name for the user, if no specific display name is set it will fallback to the user id
133
	 *
134
	 * @return string
135
	 */
136
	public function getDisplayName() {
137
		if ($this->displayName === null) {
138
			$displayName = '';
139
			if ($this->backend && $this->backend->implementsActions(Backend::GET_DISPLAYNAME)) {
140
				// get display name and strip whitespace from the beginning and end of it
141
				$backendDisplayName = $this->backend->getDisplayName($this->uid);
142
				if (is_string($backendDisplayName)) {
143
					$displayName = trim($backendDisplayName);
144
				}
145
			}
146
147
			if (!empty($displayName)) {
148
				$this->displayName = $displayName;
149
			} else {
150
				$this->displayName = $this->uid;
151
			}
152
		}
153
		return $this->displayName;
154
	}
155
156
	/**
157
	 * set the displayname for the user
158
	 *
159
	 * @param string $displayName
160
	 * @return bool
161
	 *
162
	 * @since 25.0.0 Throw InvalidArgumentException
163
	 * @throws \InvalidArgumentException
164
	 */
165
	public function setDisplayName($displayName) {
166
		$displayName = trim($displayName);
167
		$oldDisplayName = $this->getDisplayName();
168
		if ($this->backend->implementsActions(Backend::SET_DISPLAYNAME) && !empty($displayName) && $displayName !== $oldDisplayName) {
0 ignored issues
show
The method implementsActions() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
		if ($this->backend->/** @scrutinizer ignore-call */ implementsActions(Backend::SET_DISPLAYNAME) && !empty($displayName) && $displayName !== $oldDisplayName) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
169
			/** @var ISetDisplayNameBackend $backend */
170
			$backend = $this->backend;
171
			$result = $backend->setDisplayName($this->uid, $displayName);
172
			if ($result) {
173
				$this->displayName = $displayName;
174
				$this->triggerChange('displayName', $displayName, $oldDisplayName);
175
			}
176
			return $result !== false;
177
		}
178
		return false;
179
	}
180
181
	/**
182
	 * @inheritDoc
183
	 */
184
	public function setEMailAddress($mailAddress) {
185
		$this->setSystemEMailAddress($mailAddress);
186
	}
187
188
	/**
189
	 * @inheritDoc
190
	 */
191
	public function setSystemEMailAddress(string $mailAddress): void {
192
		$oldMailAddress = $this->getSystemEMailAddress();
193
194
		if ($mailAddress === '') {
195
			$this->config->deleteUserValue($this->uid, 'settings', 'email');
196
		} else {
197
			$this->config->setUserValue($this->uid, 'settings', 'email', $mailAddress);
198
		}
199
200
		$primaryAddress = $this->getPrimaryEMailAddress();
201
		if ($primaryAddress === $mailAddress) {
202
			// on match no dedicated primary settings is necessary
203
			$this->setPrimaryEMailAddress('');
204
		}
205
206
		if ($oldMailAddress !== strtolower($mailAddress)) {
207
			$this->triggerChange('eMailAddress', $mailAddress, $oldMailAddress);
208
		}
209
	}
210
211
	/**
212
	 * @inheritDoc
213
	 */
214
	public function setPrimaryEMailAddress(string $mailAddress): void {
215
		if ($mailAddress === '') {
216
			$this->config->deleteUserValue($this->uid, 'settings', 'primary_email');
217
			return;
218
		}
219
220
		$this->ensureAccountManager();
221
		$account = $this->accountManager->getAccount($this);
222
		$property = $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)
223
			->getPropertyByValue($mailAddress);
224
225
		if ($property === null || $property->getLocallyVerified() !== IAccountManager::VERIFIED) {
226
			throw new InvalidArgumentException('Only verified emails can be set as primary');
227
		}
228
		$this->config->setUserValue($this->uid, 'settings', 'primary_email', $mailAddress);
229
	}
230
231
	private function ensureAccountManager() {
232
		if (!$this->accountManager instanceof IAccountManager) {
233
			$this->accountManager = \OC::$server->get(IAccountManager::class);
234
		}
235
	}
236
237
	/**
238
	 * returns the timestamp of the user's last login or 0 if the user did never
239
	 * login
240
	 *
241
	 * @return int
242
	 */
243
	public function getLastLogin() {
244
		if ($this->lastLogin === null) {
245
			$this->lastLogin = (int) $this->config->getUserValue($this->uid, 'login', 'lastLogin', 0);
246
		}
247
		return (int) $this->lastLogin;
248
	}
249
250
	/**
251
	 * updates the timestamp of the most recent login of this user
252
	 */
253
	public function updateLastLoginTimestamp() {
254
		$previousLogin = $this->getLastLogin();
255
		$now = time();
256
		$firstTimeLogin = $previousLogin === 0;
257
258
		if ($now - $previousLogin > 60) {
259
			$this->lastLogin = time();
260
			$this->config->setUserValue(
261
				$this->uid, 'login', 'lastLogin', (string)$this->lastLogin);
262
		}
263
264
		return $firstTimeLogin;
265
	}
266
267
	/**
268
	 * Delete the user
269
	 *
270
	 * @return bool
271
	 */
272
	public function delete() {
273
		/** @deprecated 21.0.0 use BeforeUserDeletedEvent event with the IEventDispatcher instead */
274
		$this->legacyDispatcher->dispatch(IUser::class . '::preDelete', new GenericEvent($this));
275
		if ($this->emitter) {
276
			/** @deprecated 21.0.0 use BeforeUserDeletedEvent event with the IEventDispatcher instead */
277
			$this->emitter->emit('\OC\User', 'preDelete', [$this]);
278
		}
279
		$this->dispatcher->dispatchTyped(new BeforeUserDeletedEvent($this));
280
		$result = $this->backend->deleteUser($this->uid);
281
		if ($result) {
282
			// FIXME: Feels like an hack - suggestions?
283
284
			$groupManager = \OC::$server->getGroupManager();
285
			// We have to delete the user from all groups
286
			foreach ($groupManager->getUserGroupIds($this) as $groupId) {
287
				$group = $groupManager->get($groupId);
288
				if ($group) {
289
					$this->dispatcher->dispatchTyped(new BeforeUserRemovedEvent($group, $this));
290
					$group->removeUser($this);
291
					$this->dispatcher->dispatchTyped(new UserRemovedEvent($group, $this));
292
				}
293
			}
294
			// Delete the user's keys in preferences
295
			\OC::$server->getConfig()->deleteAllUserValues($this->uid);
296
297
			\OC::$server->getCommentsManager()->deleteReferencesOfActor('users', $this->uid);
298
			\OC::$server->getCommentsManager()->deleteReadMarksFromUser($this);
299
300
			/** @var AvatarManager $avatarManager */
301
			$avatarManager = \OC::$server->query(AvatarManager::class);
302
			$avatarManager->deleteUserAvatar($this->uid);
303
304
			$notification = \OC::$server->getNotificationManager()->createNotification();
305
			$notification->setUser($this->uid);
306
			\OC::$server->getNotificationManager()->markProcessed($notification);
307
308
			/** @var AccountManager $accountManager */
309
			$accountManager = \OC::$server->query(AccountManager::class);
310
			$accountManager->deleteUser($this);
311
312
			/** @deprecated 21.0.0 use UserDeletedEvent event with the IEventDispatcher instead */
313
			$this->legacyDispatcher->dispatch(IUser::class . '::postDelete', new GenericEvent($this));
314
			if ($this->emitter) {
315
				/** @deprecated 21.0.0 use UserDeletedEvent event with the IEventDispatcher instead */
316
				$this->emitter->emit('\OC\User', 'postDelete', [$this]);
317
			}
318
			$this->dispatcher->dispatchTyped(new UserDeletedEvent($this));
319
		}
320
		return !($result === false);
321
	}
322
323
	/**
324
	 * Set the password of the user
325
	 *
326
	 * @param string $password
327
	 * @param string $recoveryPassword for the encryption app to reset encryption keys
328
	 * @return bool
329
	 */
330
	public function setPassword($password, $recoveryPassword = null) {
331
		$this->legacyDispatcher->dispatch(IUser::class . '::preSetPassword', new GenericEvent($this, [
332
			'password' => $password,
333
			'recoveryPassword' => $recoveryPassword,
334
		]));
335
		if ($this->emitter) {
336
			$this->emitter->emit('\OC\User', 'preSetPassword', [$this, $password, $recoveryPassword]);
337
		}
338
		if ($this->backend->implementsActions(Backend::SET_PASSWORD)) {
339
			/** @var ISetPasswordBackend $backend */
340
			$backend = $this->backend;
341
			$result = $backend->setPassword($this->uid, $password);
342
343
			if ($result !== false) {
344
				$this->legacyDispatcher->dispatch(IUser::class . '::postSetPassword', new GenericEvent($this, [
345
					'password' => $password,
346
					'recoveryPassword' => $recoveryPassword,
347
				]));
348
				if ($this->emitter) {
349
					$this->emitter->emit('\OC\User', 'postSetPassword', [$this, $password, $recoveryPassword]);
350
				}
351
			}
352
353
			return !($result === false);
354
		} else {
355
			return false;
356
		}
357
	}
358
359
	/**
360
	 * get the users home folder to mount
361
	 *
362
	 * @return string
363
	 */
364
	public function getHome() {
365
		if (!$this->home) {
366
			/** @psalm-suppress UndefinedInterfaceMethod Once we get rid of the legacy implementsActions, psalm won't complain anymore */
367
			if (($this->backend instanceof IGetHomeBackend || $this->backend->implementsActions(Backend::GET_HOME)) && $home = $this->backend->getHome($this->uid)) {
368
				$this->home = $home;
369
			} elseif ($this->config) {
370
				$this->home = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $this->uid;
371
			} else {
372
				$this->home = \OC::$SERVERROOT . '/data/' . $this->uid;
373
			}
374
		}
375
		return $this->home;
376
	}
377
378
	/**
379
	 * Get the name of the backend class the user is connected with
380
	 *
381
	 * @return string
382
	 */
383
	public function getBackendClassName() {
384
		if ($this->backend instanceof IUserBackend) {
385
			return $this->backend->getBackendName();
386
		}
387
		return get_class($this->backend);
388
	}
389
390
	public function getBackend(): ?UserInterface {
391
		return $this->backend;
392
	}
393
394
	/**
395
	 * Check if the backend allows the user to change his avatar on Personal page
396
	 *
397
	 * @return bool
398
	 */
399
	public function canChangeAvatar() {
400
		if ($this->backend instanceof IProvideAvatarBackend || $this->backend->implementsActions(Backend::PROVIDE_AVATAR)) {
401
			/** @var IProvideAvatarBackend $backend */
402
			$backend = $this->backend;
403
			return $backend->canChangeAvatar($this->uid);
404
		}
405
		return true;
406
	}
407
408
	/**
409
	 * check if the backend supports changing passwords
410
	 *
411
	 * @return bool
412
	 */
413
	public function canChangePassword() {
414
		return $this->backend->implementsActions(Backend::SET_PASSWORD);
415
	}
416
417
	/**
418
	 * check if the backend supports changing display names
419
	 *
420
	 * @return bool
421
	 */
422
	public function canChangeDisplayName() {
423
		if (!$this->config->getSystemValueBool('allow_user_to_change_display_name', true)) {
424
			return false;
425
		}
426
		return $this->backend->implementsActions(Backend::SET_DISPLAYNAME);
427
	}
428
429
	/**
430
	 * check if the user is enabled
431
	 *
432
	 * @return bool
433
	 */
434
	public function isEnabled() {
435
		if ($this->enabled === null) {
436
			$enabled = $this->config->getUserValue($this->uid, 'core', 'enabled', 'true');
437
			$this->enabled = $enabled === 'true';
438
		}
439
		return (bool) $this->enabled;
440
	}
441
442
	/**
443
	 * set the enabled status for the user
444
	 *
445
	 * @param bool $enabled
446
	 */
447
	public function setEnabled(bool $enabled = true) {
448
		$oldStatus = $this->isEnabled();
449
		$this->enabled = $enabled;
450
		if ($oldStatus !== $this->enabled) {
451
			// TODO: First change the value, then trigger the event as done for all other properties.
452
			$this->triggerChange('enabled', $enabled, $oldStatus);
453
			$this->config->setUserValue($this->uid, 'core', 'enabled', $enabled ? 'true' : 'false');
454
		}
455
	}
456
457
	/**
458
	 * get the users email address
459
	 *
460
	 * @return string|null
461
	 * @since 9.0.0
462
	 */
463
	public function getEMailAddress() {
464
		return $this->getPrimaryEMailAddress() ?? $this->getSystemEMailAddress();
465
	}
466
467
	/**
468
	 * @inheritDoc
469
	 */
470
	public function getSystemEMailAddress(): ?string {
471
		return $this->config->getUserValue($this->uid, 'settings', 'email', null);
472
	}
473
474
	/**
475
	 * @inheritDoc
476
	 */
477
	public function getPrimaryEMailAddress(): ?string {
478
		return $this->config->getUserValue($this->uid, 'settings', 'primary_email', null);
479
	}
480
481
	/**
482
	 * get the users' quota
483
	 *
484
	 * @return string
485
	 * @since 9.0.0
486
	 */
487
	public function getQuota() {
488
		// allow apps to modify the user quota by hooking into the event
489
		$event = new GetQuotaEvent($this);
490
		$this->dispatcher->dispatchTyped($event);
491
		$overwriteQuota = $event->getQuota();
492
		if ($overwriteQuota) {
493
			$quota = $overwriteQuota;
494
		} else {
495
			$quota = $this->config->getUserValue($this->uid, 'files', 'quota', 'default');
496
		}
497
		if ($quota === 'default') {
498
			$quota = $this->config->getAppValue('files', 'default_quota', 'none');
499
500
			// if unlimited quota is not allowed => avoid getting 'unlimited' as default_quota fallback value
501
			// use the first preset instead
502
			$allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
503
			if (!$allowUnlimitedQuota) {
504
				$presets = $this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB');
505
				$presets = array_filter(array_map('trim', explode(',', $presets)));
506
				$quotaPreset = array_values(array_diff($presets, ['default', 'none']));
507
				if (count($quotaPreset) > 0) {
508
					$quota = $this->config->getAppValue('files', 'default_quota', $quotaPreset[0]);
509
				}
510
			}
511
		}
512
		return $quota;
513
	}
514
515
	/**
516
	 * set the users' quota
517
	 *
518
	 * @param string $quota
519
	 * @return void
520
	 * @throws InvalidArgumentException
521
	 * @since 9.0.0
522
	 */
523
	public function setQuota($quota) {
524
		$oldQuota = $this->config->getUserValue($this->uid, 'files', 'quota', '');
525
		if ($quota !== 'none' and $quota !== 'default') {
526
			$bytesQuota = OC_Helper::computerFileSize($quota);
527
			if ($bytesQuota === false) {
528
				throw new InvalidArgumentException('Failed to set quota to invalid value '.$quota);
529
			}
530
			$quota = OC_Helper::humanFileSize($bytesQuota);
531
		}
532
		if ($quota !== $oldQuota) {
533
			$this->config->setUserValue($this->uid, 'files', 'quota', $quota);
534
			$this->triggerChange('quota', $quota, $oldQuota);
535
		}
536
		\OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
537
	}
538
539
	public function getManagerUids(): array {
540
		$encodedUids = $this->config->getUserValue(
541
			$this->uid,
542
			'settings',
543
			self::CONFIG_KEY_MANAGERS,
544
			'[]'
545
		);
546
		return json_decode($encodedUids, false, 512, JSON_THROW_ON_ERROR);
547
	}
548
549
	public function setManagerUids(array $uids): void {
550
		$oldUids = $this->getManagerUids();
551
		$this->config->setUserValue(
552
			$this->uid,
553
			'settings',
554
			self::CONFIG_KEY_MANAGERS,
555
			json_encode($uids, JSON_THROW_ON_ERROR)
556
		);
557
		$this->triggerChange('managers', $uids, $oldUids);
558
	}
559
560
	/**
561
	 * get the avatar image if it exists
562
	 *
563
	 * @param int $size
564
	 * @return IImage|null
565
	 * @since 9.0.0
566
	 */
567
	public function getAvatarImage($size) {
568
		// delay the initialization
569
		if (is_null($this->avatarManager)) {
570
			$this->avatarManager = \OC::$server->getAvatarManager();
571
		}
572
573
		$avatar = $this->avatarManager->getAvatar($this->uid);
574
		$image = $avatar->get($size);
575
		if ($image) {
576
			return $image;
577
		}
578
579
		return null;
580
	}
581
582
	/**
583
	 * get the federation cloud id
584
	 *
585
	 * @return string
586
	 * @since 9.0.0
587
	 */
588
	public function getCloudId() {
589
		$uid = $this->getUID();
590
		$server = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
591
		if (substr($server, -10) === '/index.php') {
592
			$server = substr($server, 0, -10);
593
		}
594
		$server = $this->removeProtocolFromUrl($server);
595
		return $uid . '@' . $server;
596
	}
597
598
	private function removeProtocolFromUrl(string $url): string {
599
		if (str_starts_with($url, 'https://')) {
600
			return substr($url, strlen('https://'));
601
		}
602
603
		return $url;
604
	}
605
606
	public function triggerChange($feature, $value = null, $oldValue = null) {
607
		$this->legacyDispatcher->dispatch(IUser::class . '::changeUser', new GenericEvent($this, [
608
			'feature' => $feature,
609
			'value' => $value,
610
			'oldValue' => $oldValue,
611
		]));
612
		if ($this->emitter) {
613
			$this->emitter->emit('\OC\User', 'changeUser', [$this, $feature, $value, $oldValue]);
614
		}
615
	}
616
}
617