ProfileManager.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Profile;
  8. use OC\AppFramework\Bootstrap\Coordinator;
  9. use OC\Core\Db\ProfileConfig;
  10. use OC\Core\Db\ProfileConfigMapper;
  11. use OC\KnownUser\KnownUserService;
  12. use OC\Profile\Actions\EmailAction;
  13. use OC\Profile\Actions\FediverseAction;
  14. use OC\Profile\Actions\PhoneAction;
  15. use OC\Profile\Actions\TwitterAction;
  16. use OC\Profile\Actions\WebsiteAction;
  17. use OCP\Accounts\IAccountManager;
  18. use OCP\Accounts\PropertyDoesNotExistException;
  19. use OCP\App\IAppManager;
  20. use OCP\AppFramework\Db\DoesNotExistException;
  21. use OCP\Cache\CappedMemoryCache;
  22. use OCP\IConfig;
  23. use OCP\IUser;
  24. use OCP\L10N\IFactory;
  25. use OCP\Profile\ILinkAction;
  26. use OCP\Profile\IProfileManager;
  27. use Psr\Container\ContainerInterface;
  28. use Psr\Log\LoggerInterface;
  29. use function array_flip;
  30. use function usort;
  31. class ProfileManager implements IProfileManager {
  32. /** @var ILinkAction[] */
  33. private array $actions = [];
  34. /** @var null|ILinkAction[] */
  35. private ?array $sortedActions = null;
  36. /** @var CappedMemoryCache<ProfileConfig> */
  37. private CappedMemoryCache $configCache;
  38. private const CORE_APP_ID = 'core';
  39. /**
  40. * Array of account property actions
  41. */
  42. private const ACCOUNT_PROPERTY_ACTIONS = [
  43. EmailAction::class,
  44. PhoneAction::class,
  45. WebsiteAction::class,
  46. TwitterAction::class,
  47. FediverseAction::class,
  48. ];
  49. /**
  50. * Array of account properties displayed on the profile
  51. */
  52. private const PROFILE_PROPERTIES = [
  53. IAccountManager::PROPERTY_ADDRESS,
  54. IAccountManager::PROPERTY_AVATAR,
  55. IAccountManager::PROPERTY_BIOGRAPHY,
  56. IAccountManager::PROPERTY_DISPLAYNAME,
  57. IAccountManager::PROPERTY_HEADLINE,
  58. IAccountManager::PROPERTY_ORGANISATION,
  59. IAccountManager::PROPERTY_ROLE,
  60. ];
  61. public function __construct(
  62. private IAccountManager $accountManager,
  63. private IAppManager $appManager,
  64. private IConfig $config,
  65. private ProfileConfigMapper $configMapper,
  66. private ContainerInterface $container,
  67. private KnownUserService $knownUserService,
  68. private IFactory $l10nFactory,
  69. private LoggerInterface $logger,
  70. private Coordinator $coordinator,
  71. ) {
  72. $this->configCache = new CappedMemoryCache();
  73. }
  74. /**
  75. * If no user is passed as an argument return whether profile is enabled globally in `config.php`
  76. */
  77. public function isProfileEnabled(?IUser $user = null): bool {
  78. $profileEnabledGlobally = $this->config->getSystemValueBool('profile.enabled', true);
  79. if (empty($user) || !$profileEnabledGlobally) {
  80. return $profileEnabledGlobally;
  81. }
  82. $account = $this->accountManager->getAccount($user);
  83. return (bool) filter_var(
  84. $account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(),
  85. FILTER_VALIDATE_BOOLEAN,
  86. FILTER_NULL_ON_FAILURE,
  87. );
  88. }
  89. /**
  90. * Register an action for the user
  91. */
  92. private function registerAction(ILinkAction $action, IUser $targetUser, ?IUser $visitingUser): void {
  93. $action->preload($targetUser);
  94. if ($action->getTarget() === null) {
  95. // Actions without a target are not registered
  96. return;
  97. }
  98. if ($action->getAppId() !== self::CORE_APP_ID) {
  99. if (!$this->appManager->isEnabledForUser($action->getAppId(), $targetUser)) {
  100. $this->logger->notice('App: ' . $action->getAppId() . ' cannot register actions as it is not enabled for the target user: ' . $targetUser->getUID());
  101. return;
  102. }
  103. if (!$this->appManager->isEnabledForUser($action->getAppId(), $visitingUser)) {
  104. $this->logger->notice('App: ' . $action->getAppId() . ' cannot register actions as it is not enabled for the visiting user: ' . ($visitingUser ? $visitingUser->getUID() : '(user not connected)'));
  105. return;
  106. }
  107. }
  108. if (in_array($action->getId(), self::PROFILE_PROPERTIES, true)) {
  109. $this->logger->error('Cannot register action with ID: ' . $action->getId() . ', as it is used by a core account property.');
  110. return;
  111. }
  112. if (isset($this->actions[$action->getId()])) {
  113. $this->logger->error('Cannot register duplicate action: ' . $action->getId());
  114. return;
  115. }
  116. // Add action to associative array of actions
  117. $this->actions[$action->getId()] = $action;
  118. }
  119. /**
  120. * Return an array of registered profile actions for the user
  121. *
  122. * @return ILinkAction[]
  123. */
  124. private function getActions(IUser $targetUser, ?IUser $visitingUser): array {
  125. // If actions are already registered and sorted, return them
  126. if ($this->sortedActions !== null) {
  127. return $this->sortedActions;
  128. }
  129. foreach (self::ACCOUNT_PROPERTY_ACTIONS as $actionClass) {
  130. /** @var ILinkAction $action */
  131. $action = $this->container->get($actionClass);
  132. $this->registerAction($action, $targetUser, $visitingUser);
  133. }
  134. $context = $this->coordinator->getRegistrationContext();
  135. if ($context !== null) {
  136. foreach ($context->getProfileLinkActions() as $registration) {
  137. /** @var ILinkAction $action */
  138. $action = $this->container->get($registration->getService());
  139. $this->registerAction($action, $targetUser, $visitingUser);
  140. }
  141. }
  142. $actionsClone = $this->actions;
  143. // Sort associative array into indexed array in ascending order of priority
  144. usort($actionsClone, function (ILinkAction $a, ILinkAction $b) {
  145. return $a->getPriority() === $b->getPriority() ? 0 : ($a->getPriority() < $b->getPriority() ? -1 : 1);
  146. });
  147. $this->sortedActions = $actionsClone;
  148. return $this->sortedActions;
  149. }
  150. /**
  151. * Return whether the profile parameter of the target user
  152. * is visible to the visiting user
  153. */
  154. public function isProfileFieldVisible(string $profileField, IUser $targetUser, ?IUser $visitingUser): bool {
  155. try {
  156. $account = $this->accountManager->getAccount($targetUser);
  157. $scope = $account->getProperty($profileField)->getScope();
  158. } catch (PropertyDoesNotExistException $e) {
  159. // Allow the exception as not all profile parameters are account properties
  160. }
  161. $visibility = $this->getProfileConfig($targetUser, $visitingUser)[$profileField]['visibility'];
  162. // Handle profile visibility and account property scope
  163. if ($visibility === self::VISIBILITY_SHOW_USERS_ONLY) {
  164. if (empty($scope)) {
  165. return $visitingUser !== null;
  166. }
  167. return match ($scope) {
  168. IAccountManager::SCOPE_PRIVATE => $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()),
  169. IAccountManager::SCOPE_LOCAL,
  170. IAccountManager::SCOPE_FEDERATED,
  171. IAccountManager::SCOPE_PUBLISHED => $visitingUser !== null,
  172. default => false,
  173. };
  174. }
  175. if ($visibility === self::VISIBILITY_SHOW) {
  176. if (empty($scope)) {
  177. return true;
  178. }
  179. return match ($scope) {
  180. IAccountManager::SCOPE_PRIVATE => $visitingUser !== null && $this->knownUserService->isKnownToUser($targetUser->getUID(), $visitingUser->getUID()),
  181. IAccountManager::SCOPE_LOCAL,
  182. IAccountManager::SCOPE_FEDERATED,
  183. IAccountManager::SCOPE_PUBLISHED => true,
  184. default => false,
  185. };
  186. }
  187. return false;
  188. }
  189. /**
  190. * Return the profile parameters of the target user that are visible to the visiting user
  191. * in an associative array
  192. * @return array{userId: string, address?: string|null, biography?: string|null, displayname?: string|null, headline?: string|null, isUserAvatarVisible?: bool, organisation?: string|null, role?: string|null, actions: list<array{id: string, icon: string, title: string, target: ?string}>}
  193. */
  194. public function getProfileFields(IUser $targetUser, ?IUser $visitingUser): array {
  195. $account = $this->accountManager->getAccount($targetUser);
  196. // Initialize associative array of profile parameters
  197. $profileParameters = [
  198. 'userId' => $account->getUser()->getUID(),
  199. ];
  200. // Add account properties
  201. foreach (self::PROFILE_PROPERTIES as $property) {
  202. switch ($property) {
  203. case IAccountManager::PROPERTY_ADDRESS:
  204. case IAccountManager::PROPERTY_BIOGRAPHY:
  205. case IAccountManager::PROPERTY_DISPLAYNAME:
  206. case IAccountManager::PROPERTY_HEADLINE:
  207. case IAccountManager::PROPERTY_ORGANISATION:
  208. case IAccountManager::PROPERTY_ROLE:
  209. $profileParameters[$property] =
  210. $this->isProfileFieldVisible($property, $targetUser, $visitingUser)
  211. // Explicitly set to null when value is empty string
  212. ? ($account->getProperty($property)->getValue() ?: null)
  213. : null;
  214. break;
  215. case IAccountManager::PROPERTY_AVATAR:
  216. // Add avatar visibility
  217. $profileParameters['isUserAvatarVisible'] = $this->isProfileFieldVisible($property, $targetUser, $visitingUser);
  218. break;
  219. }
  220. }
  221. // Add actions
  222. $profileParameters['actions'] = array_map(
  223. function (ILinkAction $action) {
  224. return [
  225. 'id' => $action->getId(),
  226. 'icon' => $action->getIcon(),
  227. 'title' => $action->getTitle(),
  228. 'target' => $action->getTarget(),
  229. ];
  230. },
  231. // This is needed to reindex the array after filtering
  232. array_values(
  233. array_filter(
  234. $this->getActions($targetUser, $visitingUser),
  235. function (ILinkAction $action) use ($targetUser, $visitingUser) {
  236. return $this->isProfileFieldVisible($action->getId(), $targetUser, $visitingUser);
  237. }
  238. ),
  239. )
  240. );
  241. return $profileParameters;
  242. }
  243. /**
  244. * Return the filtered profile config containing only
  245. * the properties to be stored on the database
  246. */
  247. private function filterNotStoredProfileConfig(array $profileConfig): array {
  248. $dbParamConfigProperties = [
  249. 'visibility',
  250. ];
  251. foreach ($profileConfig as $paramId => $paramConfig) {
  252. $profileConfig[$paramId] = array_intersect_key($paramConfig, array_flip($dbParamConfigProperties));
  253. }
  254. return $profileConfig;
  255. }
  256. /**
  257. * Return the default profile config
  258. */
  259. private function getDefaultProfileConfig(IUser $targetUser, ?IUser $visitingUser): array {
  260. // Construct the default config for actions
  261. $actionsConfig = [];
  262. foreach ($this->getActions($targetUser, $visitingUser) as $action) {
  263. $actionsConfig[$action->getId()] = ['visibility' => self::DEFAULT_VISIBILITY];
  264. }
  265. // Construct the default config for account properties
  266. $propertiesConfig = [];
  267. foreach (self::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) {
  268. $propertiesConfig[$property] = ['visibility' => $visibility];
  269. }
  270. return array_merge($actionsConfig, $propertiesConfig);
  271. }
  272. /**
  273. * Return the profile config of the target user,
  274. * if a config does not already exist a default config is created and returned
  275. */
  276. public function getProfileConfig(IUser $targetUser, ?IUser $visitingUser): array {
  277. $defaultProfileConfig = $this->getDefaultProfileConfig($targetUser, $visitingUser);
  278. try {
  279. if (($config = $this->configCache[$targetUser->getUID()]) === null) {
  280. $config = $this->configMapper->get($targetUser->getUID());
  281. $this->configCache[$targetUser->getUID()] = $config;
  282. }
  283. // Merge defaults with the existing config in case the defaults are missing
  284. $config->setConfigArray(array_merge(
  285. $defaultProfileConfig,
  286. $this->filterNotStoredProfileConfig($config->getConfigArray()),
  287. ));
  288. $this->configMapper->update($config);
  289. $configArray = $config->getConfigArray();
  290. } catch (DoesNotExistException $e) {
  291. // Create a new default config if it does not exist
  292. $config = new ProfileConfig();
  293. $config->setUserId($targetUser->getUID());
  294. $config->setConfigArray($defaultProfileConfig);
  295. $this->configMapper->insert($config);
  296. $configArray = $config->getConfigArray();
  297. }
  298. return $configArray;
  299. }
  300. /**
  301. * Return the profile config of the target user with additional medatata,
  302. * if a config does not already exist a default config is created and returned
  303. */
  304. public function getProfileConfigWithMetadata(IUser $targetUser, ?IUser $visitingUser): array {
  305. $configArray = $this->getProfileConfig($targetUser, $visitingUser);
  306. $actionsMetadata = [];
  307. foreach ($this->getActions($targetUser, $visitingUser) as $action) {
  308. $actionsMetadata[$action->getId()] = [
  309. 'appId' => $action->getAppId(),
  310. 'displayId' => $action->getDisplayId(),
  311. ];
  312. }
  313. // Add metadata for account property actions which are always configurable
  314. foreach (self::ACCOUNT_PROPERTY_ACTIONS as $actionClass) {
  315. /** @var ILinkAction $action */
  316. $action = $this->container->get($actionClass);
  317. if (!isset($actionsMetadata[$action->getId()])) {
  318. $actionsMetadata[$action->getId()] = [
  319. 'appId' => $action->getAppId(),
  320. 'displayId' => $action->getDisplayId(),
  321. ];
  322. }
  323. }
  324. $propertiesMetadata = [
  325. IAccountManager::PROPERTY_ADDRESS => [
  326. 'appId' => self::CORE_APP_ID,
  327. 'displayId' => $this->l10nFactory->get('lib')->t('Address'),
  328. ],
  329. IAccountManager::PROPERTY_AVATAR => [
  330. 'appId' => self::CORE_APP_ID,
  331. 'displayId' => $this->l10nFactory->get('lib')->t('Profile picture'),
  332. ],
  333. IAccountManager::PROPERTY_BIOGRAPHY => [
  334. 'appId' => self::CORE_APP_ID,
  335. 'displayId' => $this->l10nFactory->get('lib')->t('About'),
  336. ],
  337. IAccountManager::PROPERTY_DISPLAYNAME => [
  338. 'appId' => self::CORE_APP_ID,
  339. 'displayId' => $this->l10nFactory->get('lib')->t('Display name'),
  340. ],
  341. IAccountManager::PROPERTY_HEADLINE => [
  342. 'appId' => self::CORE_APP_ID,
  343. 'displayId' => $this->l10nFactory->get('lib')->t('Headline'),
  344. ],
  345. IAccountManager::PROPERTY_ORGANISATION => [
  346. 'appId' => self::CORE_APP_ID,
  347. 'displayId' => $this->l10nFactory->get('lib')->t('Organisation'),
  348. ],
  349. IAccountManager::PROPERTY_ROLE => [
  350. 'appId' => self::CORE_APP_ID,
  351. 'displayId' => $this->l10nFactory->get('lib')->t('Role'),
  352. ],
  353. ];
  354. $paramMetadata = array_merge($actionsMetadata, $propertiesMetadata);
  355. $configArray = array_intersect_key($configArray, $paramMetadata);
  356. foreach ($configArray as $paramId => $paramConfig) {
  357. if (isset($paramMetadata[$paramId])) {
  358. $configArray[$paramId] = array_merge(
  359. $paramConfig,
  360. $paramMetadata[$paramId],
  361. );
  362. }
  363. }
  364. return $configArray;
  365. }
  366. }