ProfileManager.php 14 KB

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