ProfileManager   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 215
dl 0
loc 420
rs 3.6
c 0
b 0
f 0
wmc 60

10 Methods

Rating   Name   Duplication   Size   Complexity  
B registerAction() 0 31 7
A getProfileConfig() 0 24 3
A filterNotStoredProfileConfig() 0 10 2
A __construct() 0 21 1
A isProfileEnabled() 0 12 3
B getActions() 0 30 7
C isParameterVisible() 0 43 17
B getProfileConfigWithMetadata() 0 67 6
B getProfileParams() 0 52 11
A getDefaultProfileConfig() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like ProfileManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ProfileManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright 2021 Christopher Ng <[email protected]>
7
 *
8
 * @author Christopher Ng <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OC\Profile;
28
29
use function Safe\array_flip;
30
use function Safe\usort;
31
use OC\AppFramework\Bootstrap\Coordinator;
32
use OC\Core\Db\ProfileConfig;
33
use OC\Core\Db\ProfileConfigMapper;
34
use OC\KnownUser\KnownUserService;
35
use OC\Profile\Actions\EmailAction;
36
use OC\Profile\Actions\PhoneAction;
37
use OC\Profile\Actions\TwitterAction;
38
use OC\Profile\Actions\FediverseAction;
39
use OC\Profile\Actions\WebsiteAction;
40
use OCP\Accounts\IAccountManager;
41
use OCP\Accounts\PropertyDoesNotExistException;
42
use OCP\App\IAppManager;
43
use OCP\AppFramework\Db\DoesNotExistException;
44
use OCP\IConfig;
45
use OCP\IUser;
46
use OCP\L10N\IFactory;
47
use OCP\Profile\ILinkAction;
48
use OCP\Cache\CappedMemoryCache;
49
use Psr\Container\ContainerInterface;
50
use Psr\Log\LoggerInterface;
51
52
class ProfileManager {
53
	/** @var IAccountManager */
54
	private $accountManager;
55
56
	/** @var IAppManager */
57
	private $appManager;
58
59
	/** @var IConfig */
60
	private $config;
61
62
	/** @var ProfileConfigMapper */
63
	private $configMapper;
64
65
	/** @var ContainerInterface */
66
	private $container;
67
68
	/** @var KnownUserService */
69
	private $knownUserService;
70
71
	/** @var IFactory */
72
	private $l10nFactory;
73
74
	/** @var LoggerInterface */
75
	private $logger;
76
77
	/** @var Coordinator */
78
	private $coordinator;
79
80
	/** @var ILinkAction[] */
81
	private $actions = [];
82
83
	/** @var null|ILinkAction[] */
84
	private $sortedActions = null;
85
	/** @var CappedMemoryCache<ProfileConfig> */
86
	private CappedMemoryCache $configCache;
87
88
	private const CORE_APP_ID = 'core';
89
90
	/**
91
	 * Array of account property actions
92
	 */
93
	private const ACCOUNT_PROPERTY_ACTIONS = [
94
		EmailAction::class,
95
		PhoneAction::class,
96
		WebsiteAction::class,
97
		TwitterAction::class,
98
		FediverseAction::class,
99
	];
100
101
	/**
102
	 * Array of account properties displayed on the profile
103
	 */
104
	private const PROFILE_PROPERTIES = [
105
		IAccountManager::PROPERTY_ADDRESS,
106
		IAccountManager::PROPERTY_AVATAR,
107
		IAccountManager::PROPERTY_BIOGRAPHY,
108
		IAccountManager::PROPERTY_DISPLAYNAME,
109
		IAccountManager::PROPERTY_HEADLINE,
110
		IAccountManager::PROPERTY_ORGANISATION,
111
		IAccountManager::PROPERTY_ROLE,
112
	];
113
114
	public function __construct(
115
		IAccountManager $accountManager,
116
		IAppManager $appManager,
117
		IConfig $config,
118
		ProfileConfigMapper $configMapper,
119
		ContainerInterface $container,
120
		KnownUserService $knownUserService,
121
		IFactory $l10nFactory,
122
		LoggerInterface $logger,
123
		Coordinator $coordinator
124
	) {
125
		$this->accountManager = $accountManager;
126
		$this->appManager = $appManager;
127
		$this->config = $config;
128
		$this->configMapper = $configMapper;
129
		$this->container = $container;
130
		$this->knownUserService = $knownUserService;
131
		$this->l10nFactory = $l10nFactory;
132
		$this->logger = $logger;
133
		$this->coordinator = $coordinator;
134
		$this->configCache = new CappedMemoryCache();
135
	}
136
137
	/**
138
	 * If no user is passed as an argument return whether profile is enabled globally in `config.php`
139
	 */
140
	public function isProfileEnabled(?IUser $user = null): ?bool {
141
		$profileEnabledGlobally = $this->config->getSystemValueBool('profile.enabled', true);
142
143
		if (empty($user) || !$profileEnabledGlobally) {
144
			return $profileEnabledGlobally;
145
		}
146
147
		$account = $this->accountManager->getAccount($user);
148
		return filter_var(
149
			$account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(),
150
			FILTER_VALIDATE_BOOLEAN,
151
			FILTER_NULL_ON_FAILURE,
152
		);
153
	}
154
155
	/**
156
	 * Register an action for the user
157
	 */
158
	private function registerAction(ILinkAction $action, IUser $targetUser, ?IUser $visitingUser): void {
159
		$action->preload($targetUser);
160
161
		if ($action->getTarget() === null) {
162
			// Actions without a target are not registered
163
			return;
164
		}
165
166
		if ($action->getAppId() !== self::CORE_APP_ID) {
167
			if (!$this->appManager->isEnabledForUser($action->getAppId(), $targetUser)) {
168
				$this->logger->notice('App: ' . $action->getAppId() . ' cannot register actions as it is not enabled for the target user: ' . $targetUser->getUID());
169
				return;
170
			}
171
			if (!$this->appManager->isEnabledForUser($action->getAppId(), $visitingUser)) {
172
				$this->logger->notice('App: ' . $action->getAppId() . ' cannot register actions as it is not enabled for the visiting user: ' . $visitingUser->getUID());
0 ignored issues
show
Bug introduced by
The method getUID() 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

172
				$this->logger->notice('App: ' . $action->getAppId() . ' cannot register actions as it is not enabled for the visiting user: ' . $visitingUser->/** @scrutinizer ignore-call */ getUID());

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...
173
				return;
174
			}
175
		}
176
177
		if (in_array($action->getId(), self::PROFILE_PROPERTIES, true)) {
178
			$this->logger->error('Cannot register action with ID: ' . $action->getId() . ', as it is used by a core account property.');
179
			return;
180
		}
181
182
		if (isset($this->actions[$action->getId()])) {
183
			$this->logger->error('Cannot register duplicate action: ' . $action->getId());
184
			return;
185
		}
186
187
		// Add action to associative array of actions
188
		$this->actions[$action->getId()] = $action;
189
	}
190
191
	/**
192
	 * Return an array of registered profile actions for the user
193
	 *
194
	 * @return ILinkAction[]
195
	 */
196
	private function getActions(IUser $targetUser, ?IUser $visitingUser): array {
197
		// If actions are already registered and sorted, return them
198
		if ($this->sortedActions !== null) {
199
			return $this->sortedActions;
200
		}
201
202
		foreach (self::ACCOUNT_PROPERTY_ACTIONS as $actionClass) {
203
			/** @var ILinkAction $action */
204
			$action = $this->container->get($actionClass);
205
			$this->registerAction($action, $targetUser, $visitingUser);
206
		}
207
208
		$context = $this->coordinator->getRegistrationContext();
209
210
		if ($context !== null) {
211
			foreach ($context->getProfileLinkActions() as $registration) {
212
				/** @var ILinkAction $action */
213
				$action = $this->container->get($registration->getService());
214
				$this->registerAction($action, $targetUser, $visitingUser);
215
			}
216
		}
217
218
		$actionsClone = $this->actions;
219
		// Sort associative array into indexed array in ascending order of priority
220
		usort($actionsClone, function (ILinkAction $a, ILinkAction $b) {
221
			return $a->getPriority() === $b->getPriority() ? 0 : ($a->getPriority() < $b->getPriority() ? -1 : 1);
222
		});
223
224
		$this->sortedActions = $actionsClone;
225
		return $this->sortedActions;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sortedActions returns the type null which is incompatible with the type-hinted return array.
Loading history...
226
	}
227
228
	/**
229
	 * Return whether the profile parameter of the target user
230
	 * is visible to the visiting user
231
	 */
232
	private function isParameterVisible(string $paramId, IUser $targetUser, ?IUser $visitingUser): bool {
233
		try {
234
			$account = $this->accountManager->getAccount($targetUser);
235
			$scope = $account->getProperty($paramId)->getScope();
236
		} catch (PropertyDoesNotExistException $e) {
237
			// Allow the exception as not all profile parameters are account properties
238
		}
239
240
		$visibility = $this->getProfileConfig($targetUser, $visitingUser)[$paramId]['visibility'];
241
		// Handle profile visibility and account property scope
242
		switch ($visibility) {
243
			case ProfileConfig::VISIBILITY_HIDE:
244
				return false;
245
			case ProfileConfig::VISIBILITY_SHOW_USERS_ONLY:
246
				if (!empty($scope)) {
247
					switch ($scope) {
248
						case IAccountManager::SCOPE_PRIVATE:
249
							return $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID());
250
						case IAccountManager::SCOPE_LOCAL:
251
						case IAccountManager::SCOPE_FEDERATED:
252
						case IAccountManager::SCOPE_PUBLISHED:
253
							return $visitingUser !== null;
254
						default:
255
							return false;
256
					}
257
				}
258
				return $visitingUser !== null;
259
			case ProfileConfig::VISIBILITY_SHOW:
260
				if (!empty($scope)) {
261
					switch ($scope) {
262
						case IAccountManager::SCOPE_PRIVATE:
263
							return $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID());
264
						case IAccountManager::SCOPE_LOCAL:
265
						case IAccountManager::SCOPE_FEDERATED:
266
						case IAccountManager::SCOPE_PUBLISHED:
267
							return true;
268
						default:
269
							return false;
270
					}
271
				}
272
				return true;
273
			default:
274
				return false;
275
		}
276
	}
277
278
	/**
279
	 * Return the profile parameters of the target user that are visible to the visiting user
280
	 * in an associative array
281
	 */
282
	public function getProfileParams(IUser $targetUser, ?IUser $visitingUser): array {
283
		$account = $this->accountManager->getAccount($targetUser);
284
285
		// Initialize associative array of profile parameters
286
		$profileParameters = [
287
			'userId' => $account->getUser()->getUID(),
288
		];
289
290
		// Add account properties
291
		foreach (self::PROFILE_PROPERTIES as $property) {
292
			switch ($property) {
293
				case IAccountManager::PROPERTY_ADDRESS:
294
				case IAccountManager::PROPERTY_BIOGRAPHY:
295
				case IAccountManager::PROPERTY_DISPLAYNAME:
296
				case IAccountManager::PROPERTY_HEADLINE:
297
				case IAccountManager::PROPERTY_ORGANISATION:
298
				case IAccountManager::PROPERTY_ROLE:
299
					$profileParameters[$property] =
300
						$this->isParameterVisible($property, $targetUser, $visitingUser)
301
						// Explicitly set to null when value is empty string
302
						? ($account->getProperty($property)->getValue() ?: null)
303
						: null;
304
					break;
305
				case IAccountManager::PROPERTY_AVATAR:
306
					// Add avatar visibility
307
					$profileParameters['isUserAvatarVisible'] = $this->isParameterVisible($property, $targetUser, $visitingUser);
308
					break;
309
			}
310
		}
311
312
		// Add actions
313
		$profileParameters['actions'] = array_map(
314
			function (ILinkAction $action) {
315
				return [
316
					'id' => $action->getId(),
317
					'icon' => $action->getIcon(),
318
					'title' => $action->getTitle(),
319
					'target' => $action->getTarget(),
320
				];
321
			},
322
			// This is needed to reindex the array after filtering
323
			array_values(
324
				array_filter(
325
					$this->getActions($targetUser, $visitingUser),
326
					function (ILinkAction $action) use ($targetUser, $visitingUser) {
327
						return $this->isParameterVisible($action->getId(), $targetUser, $visitingUser);
328
					}
329
				),
330
			)
331
		);
332
333
		return $profileParameters;
334
	}
335
336
	/**
337
	 * Return the filtered profile config containing only
338
	 * the properties to be stored on the database
339
	 */
340
	private function filterNotStoredProfileConfig(array $profileConfig): array {
341
		$dbParamConfigProperties = [
342
			'visibility',
343
		];
344
345
		foreach ($profileConfig as $paramId => $paramConfig) {
346
			$profileConfig[$paramId] = array_intersect_key($paramConfig, array_flip($dbParamConfigProperties));
347
		}
348
349
		return $profileConfig;
350
	}
351
352
	/**
353
	 * Return the default profile config
354
	 */
355
	private function getDefaultProfileConfig(IUser $targetUser, ?IUser $visitingUser): array {
356
		// Construct the default config for actions
357
		$actionsConfig = [];
358
		foreach ($this->getActions($targetUser, $visitingUser) as $action) {
359
			$actionsConfig[$action->getId()] = ['visibility' => ProfileConfig::DEFAULT_VISIBILITY];
360
		}
361
362
		// Construct the default config for account properties
363
		$propertiesConfig = [];
364
		foreach (ProfileConfig::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) {
365
			$propertiesConfig[$property] = ['visibility' => $visibility];
366
		}
367
368
		return array_merge($actionsConfig, $propertiesConfig);
369
	}
370
371
	/**
372
	 * Return the profile config of the target user,
373
	 * if a config does not already exist a default config is created and returned
374
	 */
375
	public function getProfileConfig(IUser $targetUser, ?IUser $visitingUser): array {
376
		$defaultProfileConfig = $this->getDefaultProfileConfig($targetUser, $visitingUser);
377
		try {
378
			if (($config = $this->configCache[$targetUser->getUID()]) === null) {
379
				$config = $this->configMapper->get($targetUser->getUID());
380
				$this->configCache[$targetUser->getUID()] = $config;
381
			}
382
			// Merge defaults with the existing config in case the defaults are missing
383
			$config->setConfigArray(array_merge(
384
				$defaultProfileConfig,
385
				$this->filterNotStoredProfileConfig($config->getConfigArray()),
386
			));
387
			$this->configMapper->update($config);
388
			$configArray = $config->getConfigArray();
389
		} catch (DoesNotExistException $e) {
390
			// Create a new default config if it does not exist
391
			$config = new ProfileConfig();
392
			$config->setUserId($targetUser->getUID());
393
			$config->setConfigArray($defaultProfileConfig);
394
			$this->configMapper->insert($config);
395
			$configArray = $config->getConfigArray();
396
		}
397
398
		return $configArray;
399
	}
400
401
	/**
402
	 * Return the profile config of the target user with additional medatata,
403
	 * if a config does not already exist a default config is created and returned
404
	 */
405
	public function getProfileConfigWithMetadata(IUser $targetUser, ?IUser $visitingUser): array {
406
		$configArray = $this->getProfileConfig($targetUser, $visitingUser);
407
408
		$actionsMetadata = [];
409
		foreach ($this->getActions($targetUser, $visitingUser) as $action) {
410
			$actionsMetadata[$action->getId()] = [
411
				'appId' => $action->getAppId(),
412
				'displayId' => $action->getDisplayId(),
413
			];
414
		}
415
416
		// Add metadata for account property actions which are always configurable
417
		foreach (self::ACCOUNT_PROPERTY_ACTIONS as $actionClass) {
418
			/** @var ILinkAction $action */
419
			$action = $this->container->get($actionClass);
420
			if (!isset($actionsMetadata[$action->getId()])) {
421
				$actionsMetadata[$action->getId()] = [
422
					'appId' => $action->getAppId(),
423
					'displayId' => $action->getDisplayId(),
424
				];
425
			}
426
		}
427
428
		$propertiesMetadata = [
429
			IAccountManager::PROPERTY_ADDRESS => [
430
				'appId' => self::CORE_APP_ID,
431
				'displayId' => $this->l10nFactory->get('lib')->t('Address'),
432
			],
433
			IAccountManager::PROPERTY_AVATAR => [
434
				'appId' => self::CORE_APP_ID,
435
				'displayId' => $this->l10nFactory->get('lib')->t('Profile picture'),
436
			],
437
			IAccountManager::PROPERTY_BIOGRAPHY => [
438
				'appId' => self::CORE_APP_ID,
439
				'displayId' => $this->l10nFactory->get('lib')->t('About'),
440
			],
441
			IAccountManager::PROPERTY_DISPLAYNAME => [
442
				'appId' => self::CORE_APP_ID,
443
				'displayId' => $this->l10nFactory->get('lib')->t('Display name'),
444
			],
445
			IAccountManager::PROPERTY_HEADLINE => [
446
				'appId' => self::CORE_APP_ID,
447
				'displayId' => $this->l10nFactory->get('lib')->t('Headline'),
448
			],
449
			IAccountManager::PROPERTY_ORGANISATION => [
450
				'appId' => self::CORE_APP_ID,
451
				'displayId' => $this->l10nFactory->get('lib')->t('Organisation'),
452
			],
453
			IAccountManager::PROPERTY_ROLE => [
454
				'appId' => self::CORE_APP_ID,
455
				'displayId' => $this->l10nFactory->get('lib')->t('Role'),
456
			],
457
		];
458
459
		$paramMetadata = array_merge($actionsMetadata, $propertiesMetadata);
460
		$configArray = array_intersect_key($configArray, $paramMetadata);
461
462
		foreach ($configArray as $paramId => $paramConfig) {
463
			if (isset($paramMetadata[$paramId])) {
464
				$configArray[$paramId] = array_merge(
465
					$paramConfig,
466
					$paramMetadata[$paramId],
467
				);
468
			}
469
		}
470
471
		return $configArray;
472
	}
473
}
474