ProfileManager.php 15 KB

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