ProfileManager.php 14 KB

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