2021-10-14 08:19:40 +00:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-23 09:26:56 +02:00
* SPDX - FileCopyrightText : 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2021-10-14 08:19:40 +00:00
*/
namespace OC\Profile ;
use OC\AppFramework\Bootstrap\Coordinator ;
use OC\Core\Db\ProfileConfig ;
use OC\Core\Db\ProfileConfigMapper ;
2025-04-23 10:16:06 +02:00
use OC\Core\ResponseDefinitions ;
2021-10-14 08:19:40 +00:00
use OC\KnownUser\KnownUserService ;
use OC\Profile\Actions\EmailAction ;
2022-11-21 16:44:55 +01:00
use OC\Profile\Actions\FediverseAction ;
2021-10-14 08:19:40 +00:00
use OC\Profile\Actions\PhoneAction ;
use OC\Profile\Actions\TwitterAction ;
use OC\Profile\Actions\WebsiteAction ;
use OCP\Accounts\IAccountManager ;
use OCP\Accounts\PropertyDoesNotExistException ;
use OCP\App\IAppManager ;
use OCP\AppFramework\Db\DoesNotExistException ;
2022-08-18 11:12:04 +02:00
use OCP\Cache\CappedMemoryCache ;
2022-03-11 02:11:28 +00:00
use OCP\IConfig ;
2021-10-14 08:19:40 +00:00
use OCP\IUser ;
use OCP\L10N\IFactory ;
use OCP\Profile\ILinkAction ;
2022-08-18 11:12:04 +02:00
use OCP\Profile\IProfileManager ;
2021-10-14 08:19:40 +00:00
use Psr\Container\ContainerInterface ;
use Psr\Log\LoggerInterface ;
2023-10-23 12:28:48 +02:00
use function array_flip ;
use function usort ;
2021-10-14 08:19:40 +00:00
2025-04-23 10:16:06 +02:00
/**
* @ psalm - import - type CoreProfileFields from ResponseDefinitions
*/
2023-10-23 12:28:48 +02:00
class ProfileManager implements IProfileManager {
2021-10-14 08:19:40 +00:00
/** @var ILinkAction[] */
2023-06-29 22:51:54 +03:30
private array $actions = [];
2021-10-14 08:19:40 +00:00
2021-10-25 21:55:29 +00:00
/** @var null|ILinkAction[] */
2023-06-29 22:51:54 +03:30
private ? array $sortedActions = null ;
2022-08-18 11:12:04 +02:00
/** @var CappedMemoryCache<ProfileConfig> */
private CappedMemoryCache $configCache ;
2021-10-25 21:55:29 +00:00
2021-10-28 17:59:26 +00:00
private const CORE_APP_ID = 'core' ;
2021-10-20 00:17:22 +00:00
/**
* Array of account property actions
*/
2021-10-14 08:19:40 +00:00
private const ACCOUNT_PROPERTY_ACTIONS = [
EmailAction :: class ,
PhoneAction :: class ,
WebsiteAction :: class ,
TwitterAction :: class ,
2022-11-21 16:44:55 +01:00
FediverseAction :: class ,
2021-10-14 08:19:40 +00:00
];
/**
* Array of account properties displayed on the profile
*/
private const PROFILE_PROPERTIES = [
IAccountManager :: PROPERTY_ADDRESS ,
2021-10-28 00:39:09 +00:00
IAccountManager :: PROPERTY_AVATAR ,
2021-10-14 08:19:40 +00:00
IAccountManager :: PROPERTY_BIOGRAPHY ,
IAccountManager :: PROPERTY_DISPLAYNAME ,
IAccountManager :: PROPERTY_HEADLINE ,
IAccountManager :: PROPERTY_ORGANISATION ,
IAccountManager :: PROPERTY_ROLE ,
2024-09-17 21:41:45 +02:00
IAccountManager :: PROPERTY_PRONOUNS ,
2021-10-14 08:19:40 +00:00
];
public function __construct (
2023-06-29 22:51:54 +03:30
private IAccountManager $accountManager ,
private IAppManager $appManager ,
private IConfig $config ,
private ProfileConfigMapper $configMapper ,
private ContainerInterface $container ,
private KnownUserService $knownUserService ,
private IFactory $l10nFactory ,
private LoggerInterface $logger ,
private Coordinator $coordinator ,
2021-10-14 08:19:40 +00:00
) {
2022-08-18 11:12:04 +02:00
$this -> configCache = new CappedMemoryCache ();
2021-10-14 08:19:40 +00:00
}
2022-03-11 02:11:28 +00:00
/**
* If no user is passed as an argument return whether profile is enabled globally in `config.php`
*/
2023-10-23 12:28:48 +02:00
public function isProfileEnabled ( ? IUser $user = null ) : bool {
2022-03-11 02:11:28 +00:00
$profileEnabledGlobally = $this -> config -> getSystemValueBool ( 'profile.enabled' , true );
if ( empty ( $user ) || ! $profileEnabledGlobally ) {
return $profileEnabledGlobally ;
}
$account = $this -> accountManager -> getAccount ( $user );
2023-10-23 12:28:48 +02:00
return ( bool ) filter_var (
2022-03-11 02:11:28 +00:00
$account -> getProperty ( IAccountManager :: PROPERTY_PROFILE_ENABLED ) -> getValue (),
FILTER_VALIDATE_BOOLEAN ,
FILTER_NULL_ON_FAILURE ,
);
}
2021-10-14 08:19:40 +00:00
/**
* Register an action for the user
*/
2021-10-28 17:59:26 +00:00
private function registerAction ( ILinkAction $action , IUser $targetUser , ? IUser $visitingUser ) : void {
2021-10-14 08:19:40 +00:00
$action -> preload ( $targetUser );
if ( $action -> getTarget () === null ) {
// Actions without a target are not registered
return ;
}
2021-10-28 17:59:26 +00:00
if ( $action -> getAppId () !== self :: CORE_APP_ID ) {
2021-10-14 08:19:40 +00:00
if ( ! $this -> appManager -> isEnabledForUser ( $action -> getAppId (), $targetUser )) {
2021-10-28 17:59:26 +00:00
$this -> logger -> notice ( 'App: ' . $action -> getAppId () . ' cannot register actions as it is not enabled for the target user: ' . $targetUser -> getUID ());
2021-10-14 08:19:40 +00:00
return ;
}
if ( ! $this -> appManager -> isEnabledForUser ( $action -> getAppId (), $visitingUser )) {
2023-07-11 14:40:47 +02:00
$this -> logger -> notice ( 'App: ' . $action -> getAppId () . ' cannot register actions as it is not enabled for the visiting user: ' . ( $visitingUser ? $visitingUser -> getUID () : '(user not connected)' ));
2021-10-14 08:19:40 +00:00
return ;
}
}
2021-10-28 00:39:09 +00:00
if ( in_array ( $action -> getId (), self :: PROFILE_PROPERTIES , true )) {
$this -> logger -> error ( 'Cannot register action with ID: ' . $action -> getId () . ', as it is used by a core account property.' );
return ;
}
2021-10-28 17:59:26 +00:00
if ( isset ( $this -> actions [ $action -> getId ()])) {
$this -> logger -> error ( 'Cannot register duplicate action: ' . $action -> getId ());
return ;
}
2021-10-14 08:19:40 +00:00
// Add action to associative array of actions
$this -> actions [ $action -> getId ()] = $action ;
}
/**
* Return an array of registered profile actions for the user
*
* @ return ILinkAction []
*/
private function getActions ( IUser $targetUser , ? IUser $visitingUser ) : array {
2021-10-25 21:55:29 +00:00
// If actions are already registered and sorted, return them
if ( $this -> sortedActions !== null ) {
return $this -> sortedActions ;
2021-10-14 08:19:40 +00:00
}
foreach ( self :: ACCOUNT_PROPERTY_ACTIONS as $actionClass ) {
2021-10-25 21:55:29 +00:00
/** @var ILinkAction $action */
$action = $this -> container -> get ( $actionClass );
2021-10-28 17:59:26 +00:00
$this -> registerAction ( $action , $targetUser , $visitingUser );
2021-10-14 08:19:40 +00:00
}
2021-10-25 21:55:29 +00:00
$context = $this -> coordinator -> getRegistrationContext ();
if ( $context !== null ) {
foreach ( $context -> getProfileLinkActions () as $registration ) {
/** @var ILinkAction $action */
$action = $this -> container -> get ( $registration -> getService ());
2021-10-28 17:59:26 +00:00
$this -> registerAction ( $action , $targetUser , $visitingUser );
2021-10-25 21:55:29 +00:00
}
2021-10-14 08:19:40 +00:00
}
$actionsClone = $this -> actions ;
// Sort associative array into indexed array in ascending order of priority
usort ( $actionsClone , function ( ILinkAction $a , ILinkAction $b ) {
return $a -> getPriority () === $b -> getPriority () ? 0 : ( $a -> getPriority () < $b -> getPriority () ? - 1 : 1 );
});
2021-10-25 21:55:29 +00:00
$this -> sortedActions = $actionsClone ;
return $this -> sortedActions ;
2021-10-14 08:19:40 +00:00
}
/**
2021-10-28 00:56:55 +00:00
* Return whether the profile parameter of the target user
* is visible to the visiting user
2021-10-14 08:19:40 +00:00
*/
2023-10-23 12:28:48 +02:00
public function isProfileFieldVisible ( string $profileField , IUser $targetUser , ? IUser $visitingUser ) : bool {
2021-10-14 08:19:40 +00:00
try {
$account = $this -> accountManager -> getAccount ( $targetUser );
2023-10-23 12:28:48 +02:00
$scope = $account -> getProperty ( $profileField ) -> getScope ();
2021-10-14 08:19:40 +00:00
} catch ( PropertyDoesNotExistException $e ) {
// Allow the exception as not all profile parameters are account properties
}
2023-10-23 12:28:48 +02:00
$visibility = $this -> getProfileConfig ( $targetUser , $visitingUser )[ $profileField ][ 'visibility' ];
2021-10-14 08:19:40 +00:00
// Handle profile visibility and account property scope
2023-06-29 22:51:54 +03:30
2023-10-23 12:31:51 +02:00
if ( $visibility === self :: VISIBILITY_SHOW_USERS_ONLY ) {
2023-06-29 22:51:54 +03:30
if ( empty ( $scope )) {
2021-10-14 08:19:40 +00:00
return $visitingUser !== null ;
2023-06-29 22:51:54 +03:30
}
return match ( $scope ) {
IAccountManager :: SCOPE_PRIVATE => $visitingUser !== null && $this -> knownUserService -> isKnownToUser ( $targetUser -> getUID (), $visitingUser -> getUID ()),
IAccountManager :: SCOPE_LOCAL ,
IAccountManager :: SCOPE_FEDERATED ,
IAccountManager :: SCOPE_PUBLISHED => $visitingUser !== null ,
default => false ,
};
}
2023-10-23 12:31:51 +02:00
if ( $visibility === self :: VISIBILITY_SHOW ) {
2023-06-29 22:51:54 +03:30
if ( empty ( $scope )) {
2021-10-14 08:19:40 +00:00
return true ;
2023-10-23 12:28:48 +02:00
}
2023-06-29 22:51:54 +03:30
return match ( $scope ) {
IAccountManager :: SCOPE_PRIVATE => $visitingUser !== null && $this -> knownUserService -> isKnownToUser ( $targetUser -> getUID (), $visitingUser -> getUID ()),
IAccountManager :: SCOPE_LOCAL ,
IAccountManager :: SCOPE_FEDERATED ,
IAccountManager :: SCOPE_PUBLISHED => true ,
default => false ,
};
2021-10-14 08:19:40 +00:00
}
2023-06-29 22:51:54 +03:30
return false ;
2021-10-14 08:19:40 +00:00
}
/**
2021-10-28 00:56:55 +00:00
* Return the profile parameters of the target user that are visible to the visiting user
* in an associative array
2025-04-23 10:16:06 +02:00
* @ psalm - return CoreProfileFields
2021-10-14 08:19:40 +00:00
*/
2023-10-23 12:28:48 +02:00
public function getProfileFields ( IUser $targetUser , ? IUser $visitingUser ) : array {
2021-10-14 08:19:40 +00:00
$account = $this -> accountManager -> getAccount ( $targetUser );
2021-10-28 17:59:26 +00:00
2021-10-14 08:19:40 +00:00
// Initialize associative array of profile parameters
$profileParameters = [
'userId' => $account -> getUser () -> getUID (),
];
// Add account properties
foreach ( self :: PROFILE_PROPERTIES as $property ) {
2021-10-28 00:39:09 +00:00
switch ( $property ) {
case IAccountManager :: PROPERTY_ADDRESS :
case IAccountManager :: PROPERTY_BIOGRAPHY :
case IAccountManager :: PROPERTY_DISPLAYNAME :
case IAccountManager :: PROPERTY_HEADLINE :
case IAccountManager :: PROPERTY_ORGANISATION :
case IAccountManager :: PROPERTY_ROLE :
2024-09-17 21:41:45 +02:00
case IAccountManager :: PROPERTY_PRONOUNS :
2021-10-28 00:39:09 +00:00
$profileParameters [ $property ] =
2023-10-23 12:28:48 +02:00
$this -> isProfileFieldVisible ( $property , $targetUser , $visitingUser )
2021-10-28 00:39:09 +00:00
// Explicitly set to null when value is empty string
? ( $account -> getProperty ( $property ) -> getValue () ? : null )
: null ;
break ;
case IAccountManager :: PROPERTY_AVATAR :
// Add avatar visibility
2023-10-23 12:28:48 +02:00
$profileParameters [ 'isUserAvatarVisible' ] = $this -> isProfileFieldVisible ( $property , $targetUser , $visitingUser );
2021-10-28 00:39:09 +00:00
break ;
}
2021-10-14 08:19:40 +00:00
}
// Add actions
$profileParameters [ 'actions' ] = array_map (
function ( ILinkAction $action ) {
return [
'id' => $action -> getId (),
'icon' => $action -> getIcon (),
'title' => $action -> getTitle (),
'target' => $action -> getTarget (),
];
},
// This is needed to reindex the array after filtering
array_values (
array_filter (
$this -> getActions ( $targetUser , $visitingUser ),
function ( ILinkAction $action ) use ( $targetUser , $visitingUser ) {
2023-10-23 12:28:48 +02:00
return $this -> isProfileFieldVisible ( $action -> getId (), $targetUser , $visitingUser );
2021-10-14 08:19:40 +00:00
}
),
)
);
return $profileParameters ;
}
2021-10-28 17:59:26 +00:00
/**
* Return the filtered profile config containing only
* the properties to be stored on the database
*/
private function filterNotStoredProfileConfig ( array $profileConfig ) : array {
$dbParamConfigProperties = [
'visibility' ,
];
foreach ( $profileConfig as $paramId => $paramConfig ) {
$profileConfig [ $paramId ] = array_intersect_key ( $paramConfig , array_flip ( $dbParamConfigProperties ));
}
return $profileConfig ;
}
2021-10-14 08:19:40 +00:00
/**
2021-10-20 00:27:37 +00:00
* Return the default profile config
*/
private function getDefaultProfileConfig ( IUser $targetUser , ? IUser $visitingUser ) : array {
2022-07-27 08:51:42 -04:00
// Construct the default config for actions
2021-10-20 00:27:37 +00:00
$actionsConfig = [];
foreach ( $this -> getActions ( $targetUser , $visitingUser ) as $action ) {
2023-10-23 12:31:51 +02:00
$actionsConfig [ $action -> getId ()] = [ 'visibility' => self :: DEFAULT_VISIBILITY ];
2021-10-20 00:27:37 +00:00
}
2022-07-27 08:51:42 -04:00
// Construct the default config for account properties
2021-10-20 00:27:37 +00:00
$propertiesConfig = [];
2023-10-23 12:31:51 +02:00
foreach ( self :: DEFAULT_PROPERTY_VISIBILITY as $property => $visibility ) {
2021-10-28 17:59:26 +00:00
$propertiesConfig [ $property ] = [ 'visibility' => $visibility ];
2021-10-20 00:27:37 +00:00
}
return array_merge ( $actionsConfig , $propertiesConfig );
}
/**
2021-10-28 17:59:26 +00:00
* Return the profile config of the target user ,
* if a config does not already exist a default config is created and returned
2021-10-14 08:19:40 +00:00
*/
public function getProfileConfig ( IUser $targetUser , ? IUser $visitingUser ) : array {
2021-10-20 00:27:37 +00:00
$defaultProfileConfig = $this -> getDefaultProfileConfig ( $targetUser , $visitingUser );
2021-10-14 08:19:40 +00:00
try {
2022-08-18 11:12:04 +02:00
if (( $config = $this -> configCache [ $targetUser -> getUID ()]) === null ) {
$config = $this -> configMapper -> get ( $targetUser -> getUID ());
$this -> configCache [ $targetUser -> getUID ()] = $config ;
}
2021-10-20 00:27:37 +00:00
// Merge defaults with the existing config in case the defaults are missing
2021-10-28 17:59:26 +00:00
$config -> setConfigArray ( array_merge (
$defaultProfileConfig ,
$this -> filterNotStoredProfileConfig ( $config -> getConfigArray ()),
));
2021-10-20 00:27:37 +00:00
$this -> configMapper -> update ( $config );
$configArray = $config -> getConfigArray ();
2021-10-14 08:19:40 +00:00
} catch ( DoesNotExistException $e ) {
2021-10-20 00:27:37 +00:00
// Create a new default config if it does not exist
2021-10-14 08:19:40 +00:00
$config = new ProfileConfig ();
$config -> setUserId ( $targetUser -> getUID ());
2021-10-20 00:27:37 +00:00
$config -> setConfigArray ( $defaultProfileConfig );
2021-10-14 08:19:40 +00:00
$this -> configMapper -> insert ( $config );
$configArray = $config -> getConfigArray ();
}
return $configArray ;
}
2021-10-28 17:59:26 +00:00
/**
* Return the profile config of the target user with additional medatata ,
* if a config does not already exist a default config is created and returned
*/
public function getProfileConfigWithMetadata ( IUser $targetUser , ? IUser $visitingUser ) : array {
$configArray = $this -> getProfileConfig ( $targetUser , $visitingUser );
$actionsMetadata = [];
foreach ( $this -> getActions ( $targetUser , $visitingUser ) as $action ) {
$actionsMetadata [ $action -> getId ()] = [
'appId' => $action -> getAppId (),
'displayId' => $action -> getDisplayId (),
];
}
// Add metadata for account property actions which are always configurable
foreach ( self :: ACCOUNT_PROPERTY_ACTIONS as $actionClass ) {
/** @var ILinkAction $action */
$action = $this -> container -> get ( $actionClass );
if ( ! isset ( $actionsMetadata [ $action -> getId ()])) {
$actionsMetadata [ $action -> getId ()] = [
'appId' => $action -> getAppId (),
'displayId' => $action -> getDisplayId (),
];
}
}
$propertiesMetadata = [
IAccountManager :: PROPERTY_ADDRESS => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Address' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_AVATAR => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Profile picture' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_BIOGRAPHY => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'About' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_DISPLAYNAME => [
'appId' => self :: CORE_APP_ID ,
2023-04-05 10:15:03 +02:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Display name' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_HEADLINE => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Headline' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_ORGANISATION => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Organisation' ),
2021-10-28 17:59:26 +00:00
],
IAccountManager :: PROPERTY_ROLE => [
'appId' => self :: CORE_APP_ID ,
2021-11-26 02:00:40 +00:00
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Role' ),
2021-10-28 17:59:26 +00:00
],
2024-09-17 21:41:45 +02:00
IAccountManager :: PROPERTY_PRONOUNS => [
'appId' => self :: CORE_APP_ID ,
'displayId' => $this -> l10nFactory -> get ( 'lib' ) -> t ( 'Pronouns' ),
],
2021-10-28 17:59:26 +00:00
];
$paramMetadata = array_merge ( $actionsMetadata , $propertiesMetadata );
2021-11-25 05:01:18 +00:00
$configArray = array_intersect_key ( $configArray , $paramMetadata );
2021-10-28 17:59:26 +00:00
foreach ( $configArray as $paramId => $paramConfig ) {
if ( isset ( $paramMetadata [ $paramId ])) {
$configArray [ $paramId ] = array_merge (
$paramConfig ,
$paramMetadata [ $paramId ],
);
}
}
return $configArray ;
}
2021-10-14 08:19:40 +00:00
}