2023-12-21 19:06:49 +03:00
< ? php
namespace YesWiki\Custom\Field ;
use Exception ;
use Psr\Container\ContainerInterface ;
use YesWiki\Bazar\Exception\UserFieldException ;
use YesWiki\Bazar\Service\FormManager ;
use YesWiki\Bazar\Field\BazarField ;
use YesWiki\Core\Controller\AuthController ;
use YesWiki\Core\Controller\UserController ;
use YesWiki\Core\Exception\UserNameAlreadyUsedException ;
use YesWiki\Core\Service\ConfigurationService ;
use YesWiki\Core\Service\Mailer ;
use YesWiki\Core\Service\UserManager ;
use YesWiki\Wiki ;
/**
* @ Field ({ " yunohost_user " })
*/
class YunohostUserField extends BazarField
{
2024-02-05 14:12:59 +03:00
protected $nameField ;
protected $emailField ;
protected $usernameField ;
protected $userMailDomain ;
protected $mailingList ;
protected $autoUpdateMail ;
protected $autoAddToGroup ;
protected $yunohostOptions ;
protected $cmdPrefix ;
protected const FIELD_NAME_FIELD = 1 ;
protected const FIELD_EMAIL_FIELD = 2 ;
protected const FIELD_USERNAME_FIELD = 3 ;
protected const FIELD_USER_MAIL_DOMAIN_FIELD = 4 ;
protected const FIELD_MAILING_LIST = 5 ;
protected const FIELD_AUTO_ADD_TO_GROUP = 6 ;
protected const FIELD_AUTO_UPDATE_MAIL = 9 ;
private const CONFIRM_NAME_SUFFIX = '_confirmNewName' ;
private const FORCE_LABEL = '_force_label' ;
public function __construct ( array $values , ContainerInterface $services )
{
parent :: __construct ( $values , $services );
$this -> yunohostOptions = $this -> getValuesFromConfig ();
$this -> nameField = $values [ self :: FIELD_NAME_FIELD ] ? : 'bf_titre' ;
$this -> emailField = $values [ self :: FIELD_EMAIL_FIELD ] ? : 'bf_mail' ;
$this -> usernameField = $values [ self :: FIELD_USERNAME_FIELD ] ? : '' ; // empty usernameField will use nameField as username
$this -> userMailDomain = $values [ self :: FIELD_USER_MAIL_DOMAIN_FIELD ] ? : $this -> yunohostOptions [ 'mail_domain' ];
$this -> mailingList = $values [ self :: FIELD_MAILING_LIST ];
$this -> autoUpdateMail = in_array ( $values [ self :: FIELD_AUTO_UPDATE_MAIL ], [ true , " 1 " , 1 ], true );
$this -> autoAddToGroup = trim ( strval ( $values [ self :: FIELD_AUTO_ADD_TO_GROUP ]));
$this -> cmdPrefix = $this -> yunohostOptions [ 'cmd_prefix' ] ? ? 'sudo' ;
// We have no default value
$this -> default = null ;
// not searchable
$this -> searchable = null ;
$this -> propertyName = 'yunohost_username' ;
$this -> label = _t ( 'BAZ_USER_FIELD_LABEL' );
$this -> maxChars = 60 ;
}
protected function getValuesFromConfig ()
{
return $this -> getWiki () -> config [ 'yunohost' ] ? ? [];
}
protected function getYunohostUsernames ()
{
$output = $retval = null ;
$cmd = $this -> cmdPrefix . ' yunohost user list --fields username 2>&1' ;
exec ( $cmd , $output , $retval );
$users = [];
foreach ( $output as $outputline ) {
preg_match ( '/^\s*username:\s*(.*)$/s' , $outputline , $matches , PREG_OFFSET_CAPTURE , 0 );
if ( ! empty ( $matches [ 1 ][ 0 ])) {
$users [] = $matches [ 1 ][ 0 ];
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
return $users ;
}
protected function addYunohostUser ( $userValues )
{
$users = $this -> getYunohostUsernames ();
if ( in_array ( $userValues [ 'name' ], $users )) {
throw new UserNameAlreadyUsedException ();
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
$output = $retval = null ;
// To be able to sudo use yunohost with your php user without password,
// you need to add in /etc/sudoers (with visudo) something like
// <myunixuser> ALL=(root) NOPASSWD: /usr/bin/yunohost
// utiliser proc_open avec des arguments plutot que de echaper / concatener
$cmd = $this -> cmdPrefix . ' yunohost user create ' . $userValues [ 'name' ]
. ' -F \'' . $userValues [ 'fullname' ] . '\''
. ' -d ' . $this -> userMailDomain
. ' -p \'' . $userValues [ 'password' ] . '\''
. ' 2>&1 &' ;
exec ( $cmd , $output , $retval );
// handle errors
if ( $retval == 1 ) {
// TODO : LC_ALL=fr_FR.UTF-8
if ( preg_match ( '/^This password is among the most used passwords in the world.*$/s' , $output [ 0 ], $matches , PREG_OFFSET_CAPTURE , 0 )) {
throw new Exception ( 'Le mot de passe utilisé est trop courant, il fait parti de la liste des mots de passe les plus utilisés, et pose des problème de sécurité, veuillez le changer.' );
} elseif ( preg_match ( '/^The password needs to be at least 8 characters long.*$/s' , $output [ 0 ], $matches , PREG_OFFSET_CAPTURE , 0 )) {
throw new Exception ( 'Le mot de passe utilisé est trop court, il doit faire au moins 8 caractères.' );
} else {
throw new Exception ( $output [ 0 ]);
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
$cmd = $this -> cmdPrefix . ' yunohost user update ' . $userValues [ 'name' ]
. ' --add-mailforward ' . $userValues [ $this -> emailField ]
. ' 2>&1 &' ;
exec ( $cmd , $output , $retval );
//dump($output);
// handle errors
if ( $retval == 1 ) {
throw new Exception ( $output [ 0 ]);
}
}
protected function modifyYunohostUser ( $userId , $userValues )
{
#yunohost user update <user> -F '<Prenom Nom>' -d $this->userMailDomain -p '<pass>' --add-mailforward [MAIL ...]]
}
protected function deleteYunohostUser ( $userId )
{
#yunohost user delete <user>
}
protected function renderInput ( $entry )
{
$value = $this -> getValue ( $entry );
$authController = $this -> getService ( AuthController :: class );
$userManager = $this -> getService ( UserManager :: class );
$loggedUser = $authController -> getLoggedUser ();
if ( ! empty ( $loggedUser )) {
$associatedUser = $userManager -> getOneByName ( $loggedUser [ 'name' ]);
if ( ! empty ( $associatedUser [ 'name' ])) {
if ( empty ( $value ) || ! $this -> isUserByName ( $value )) {
$value = $associatedUser [ 'name' ];
$message = str_replace (
[ '{wikiname}' , '{email}' ],
[ $value , $associatedUser [ 'email' ]],
_t ( 'BAZ_USER_FIELD_ALREADY_CONNECTED' )
);
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
if ( $value !== $loggedUser [ 'name' ] && $this -> getWiki () -> UserIsAdmin ()) {
$associatedUser = $userManager -> getOneByName ( $value );
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
if ( $value === $loggedUser [ 'name' ] || ( $this -> getWiki () -> UserIsAdmin () && ! empty ( $associatedUser [ 'email' ]))) {
$message = ( ! empty ( $message ) ? $message . " \n " : '' ) . ( $this -> autoUpdateMail ? str_replace (
'{email}' ,
$associatedUser [ 'email' ],
_t ( 'BAZ_USER_FIELD_ALREADY_CONNECTED_AUTOUPDATE' )
) : '' );
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
return $this -> render ( " @bazar/inputs/user.twig " , [
'value' => $value ,
'creationMode' => empty ( $entry [ $this -> getPropertyName ()]),
'message' => $message ? ? null ,
'userIsAdmin' => $this -> getWiki () -> UserIsAdmin (),
'userName' => $loggedUser [ 'name' ] ? ? null ,
'userEmail' => $loggedUser [ 'email' ] ? ? null ,
'forceLabel' => $this -> propertyName . self :: FORCE_LABEL ,
'forceLabelChecked' => $_POST [ $this -> propertyName . self :: FORCE_LABEL ] ? ? false ,
]);
}
public function formatValuesBeforeSave ( $entry )
{
$userController = $this -> getService ( UserController :: class );
$userManager = $this -> getService ( UserManager :: class );
$mailer = $this -> getService ( Mailer :: class );
$value = $this -> getValue ( $entry );
$isImport = isset ( $GLOBALS [ '_BAZAR_' ][ 'provenance' ]) && $GLOBALS [ '_BAZAR_' ][ 'provenance' ] === 'import' ;
$wiki = $this -> getWiki ();
if (
$this -> getWiki () -> UserIsAdmin ()
&& in_array ( $_POST [ $this -> propertyName . self :: FORCE_LABEL ] ? ? false , [ true , " true " , 1 , " 1 " ], true )
) {
// force entry creation but do not create user if existing for this email
$userManager = $this -> getService ( UserManager :: class );
$existingUser = $userManager -> getOneByEmail ( $entry [ $this -> emailField ]);
if ( ! empty ( $existingUser )) {
$value = $existingUser [ 'name' ];
} else {
$value = null ;
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
if ( $value && $this -> isUserByName ( $value )) {
$wikiName = $value ;
// add in groups
$this -> addUserToGroups ( $wikiName , $entry );
$this -> updateEmailIfNeeded ( $wikiName , $entry [ $this -> emailField ] ? ? null );
} else {
$wikiName = strtolower ( $entry [ $this -> nameField ]);
if ( ! $wiki -> IsWikiName ( $wikiName )) {
// create a UserName from value that is a wikiname
// so the User could have a chance to have the same name as the created entry
// if the name is based on `bf_titre`
$wikiName = strtolower ( genere_nom_wiki ( $wikiName , 0 ));
}
if ( $this -> isUserByName ( $wikiName )) {
$currentWikiName = strtolower ( $wikiName );
$wikiName = $this -> findANewNotExistingUserName ( $currentWikiName );
if (
! $isImport
&& (
! isset ( $_POST [ $this -> propertyName . self :: CONFIRM_NAME_SUFFIX ])
|| ! in_array ( $_POST [ $this -> propertyName . self :: CONFIRM_NAME_SUFFIX ], [ true , 1 , " 1 " ], true )
)
) {
throw new UserFieldException (
$this -> render ( " @bazar/inputs/user-confirm.twig " , [
'confirmName' => $this -> propertyName . self :: CONFIRM_NAME_SUFFIX ,
'wikiName' => $currentWikiName ,
'newWikiName' => $wikiName ,
])
);
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
if ( ! isset ( $entry [ $this -> emailField ])) {
throw new Exception ( " Yunohost user : email not set " );
}
if ( ! $isImport ) {
if ( ! isset ( $entry [ 'mot_de_passe_repete_wikini' ])) {
throw new Exception ( " Yunohost user : password not confirmed second time " );
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
if ( $entry [ 'mot_de_passe_wikini' ] !== $entry [ 'mot_de_passe_repete_wikini' ]) {
throw new UserFieldException ( _t ( 'USER_PASSWORDS_NOT_IDENTICAL' ));
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
2023-12-21 19:06:49 +03:00
2024-02-05 14:12:59 +03:00
try {
$this -> addYunohostUser ([
'name' => $wikiName ,
'fullname' => $entry [ $this -> nameField ],
'password' => $entry [ 'mot_de_passe_wikini' ]
]);
try {
$userController -> create ([
'name' => $wikiName ,
'email' => $entry [ $this -> emailField ],
'password' => $entry [ 'mot_de_passe_wikini' ]
]);
} catch ( UserNameAlreadyUsedException $ex ) {
throw new UserFieldException ( 'yeswiki : ' . _t ( 'BAZ_USER_FIELD_EXISTING_USER_BY_EMAIL' ));
} catch ( Exception $ex ) {
throw new UserFieldException ( 'yeswiki : ' . $ex -> getMessage (), $ex -> getCode (), $ex );
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
} catch ( UserNameAlreadyUsedException $ex ) {
throw new UserFieldException ( _t ( 'L\'utilisateur.ice ' . $wikiName . ' est déjà utilisé, veuillez vous connecter si c\'est vous, ou utiliser un autre nom d\'utilisateur.ice.' ));
} catch ( Exception $ex ) {
throw new UserFieldException ( $ex -> getMessage (), $ex -> getCode (), $ex );
}
// add in groups
$this -> addUserToGroups ( $wikiName , $entry );
// Do not send mails if we are importing
// TODO improve import detection
if ( ! $isImport ) {
$mailer -> notifyNewUser ( $wikiName , $entry [ $this -> emailField ]);
// Check if we need to subscribe the user to a mailing list
if ( isset ( $this -> mailingList ) && $this -> mailingList != '' ) {
$mailer -> subscribeToMailingList ( $entry [ $this -> emailField ], $this -> mailingList );
}
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
// indicateur pour la gestion des droits associee a la fiche.
$GLOBALS [ 'utilisateur_wikini' ] = $wikiName ;
return [
$this -> propertyName => $wikiName ,
'fields-to-remove' => [
'mot_de_passe_wikini' ,
'mot_de_passe_repete_wikini' ,
$this -> propertyName . self :: CONFIRM_NAME_SUFFIX ,
$this -> propertyName . self :: FORCE_LABEL ,
]
];
}
protected function renderStatic ( $entry )
{
$value = $this -> getValue ( $entry );
$authController = $this -> getService ( AuthController :: class );
if ( $value ) {
return $this -> render ( " @bazar/fields/user.twig " , [
'value' => $value ,
'isLoggedUser' => $authController -> getLoggedUser () && $authController -> getLoggedUserName () === $value ,
'editUrl' => $this -> getWiki () -> href ( 'edit' , $value ),
'settingsUrl' => $this -> getWiki () -> href ( '' , 'ParametresUtilisateur' )
]);
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
return " " ;
}
// GETTERS. Needed to use them in the Twig syntax
public function getNameField ()
{
return $this -> nameField ;
}
public function getEmailField ()
{
return $this -> emailField ;
}
public function getMailingList ()
{
return $this -> mailingList ;
}
public function getAutoUpdateMail ()
{
return $this -> autoUpdateMail ;
}
public function getAutoAddToGroup ()
{
return $this -> autoAddToGroup ;
}
// change return of this method to keep compatible with php 7.3 (mixed is not managed)
#[\ReturnTypeWillChange]
public function jsonSerialize ()
{
return array_merge (
parent :: jsonSerialize (),
[
'nameField' => $this -> getNameField (),
'emailField' => $this -> getEmailField (),
'mailingList' => $this -> getMailingList (),
'autoUpdateMail' => $this -> getAutoUpdateMail (),
'autoAddToGroup' => $this -> getAutoAddToGroup (),
]
);
}
private function isUserByName ( string $userName ) : bool
{
$userManager = $this -> getService ( UserManager :: class );
return ! empty ( $userManager -> getOneByName ( $userName ));
}
private function isUserByEmail ( string $email ) : bool
{
$userManager = $this -> getService ( UserManager :: class );
return ! empty ( $userManager -> getOneByEmail ( $email ));
}
private function updateEmailIfNeeded ( string $userName , string $email )
{
if ( $this -> getAutoUpdateMail () && ! empty ( $userName ) && ! empty ( $email )) {
$authController = $this -> getService ( AuthController :: class );
$userController = $this -> getService ( UserController :: class );
$userManager = $this -> getService ( UserManager :: class );
$user = $userManager -> getOneByName ( $userName );
$loggedUser = $authController -> getLoggedUser ();
if (
! empty ( $user )
&& (
$this -> getWiki () -> UserIsAdmin ()
|| (
! empty ( $loggedUser )
&& $user [ 'name' ] === $loggedUser [ 'name' ]
)
)
&& $user [ 'email' ] !== $email
) {
try {
$userController -> update ( $user , [ 'email' => $email ]);
} catch ( UserNameAlreadyUsedException $ex ) {
throw new UserFieldException ( _t ( 'BAZ_USER_FIELD_EXISTING_USER_BY_EMAIL' ));
} catch ( Exception $ex ) {
throw new UserFieldException ( $ex -> getMessage (), $ex -> getCode (), $ex );
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
private function addUserToGroups ( string $wikiName , ? array $entry )
{
if ( ! empty ( $this -> autoAddToGroup )) {
$groups = explode ( ',' , $this -> autoAddToGroup );
$groupsNames = [];
$wiki = $this -> getWiki ();
$existingsGroups = $wiki -> GetGroupsList ();
$formManager = $this -> getService ( FormManager :: class );
$userManager = $this -> getService ( UserManager :: class );
foreach ( $groups as $group ) {
$group = trim ( $group );
$forceGroupCreation = ( substr ( $group , 0 , 1 ) === '+' );
$groupName = substr ( $group , ( $forceGroupCreation ? 1 : 0 ));
if ( substr ( $groupName , 0 , 1 ) !== '@' ) {
// field name
$field = $formManager -> findFieldFromNameOrPropertyName ( $groupName , $entry [ 'id_typeannonce' ]);
if ( ! empty ( $field ) && ! empty ( $entry [ $field -> getPropertyName ()])) {
$groupsNamesFromField = explode ( ',' , $entry [ $field -> getPropertyName ()]);
foreach ( $groupsNamesFromField as $groupNameFromField ) {
if ( $this -> userMustBeAddedToGroup ( $wikiName , $groupNameFromField , $forceGroupCreation , $userManager , $existingsGroups )) {
$groupsNames [] = $groupNameFromField ;
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
} else {
$groupName = substr ( $groupName , 1 );
if ( $this -> userMustBeAddedToGroup ( $wikiName , $groupName , $forceGroupCreation , $userManager , $existingsGroups )) {
$groupsNames [] = $groupName ;
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
}
2023-12-21 19:06:49 +03:00
2024-02-05 14:12:59 +03:00
$groupsNames = array_unique ( $groupsNames );
2023-12-21 19:06:49 +03:00
2024-02-05 14:12:59 +03:00
foreach ( $groupsNames as $groupName ) {
$previousACL = ! in_array ( $groupName , $existingsGroups , true )
? ''
: $wiki -> GetGroupACL ( $groupName ) . " \n " ;
$wiki -> SetGroupACL ( $groupName , $previousACL . $wikiName );
}
}
}
private function userMustBeAddedToGroup (
string $wikiName ,
string $groupName ,
bool $forceGroupCreation ,
UserManager $userManager ,
array $existingsGroups
) {
if ( ! preg_match ( '/^[A-Za-z0-9]+$/m' , $groupName )) {
return false ;
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
if ( in_array ( $groupName , $existingsGroups , true )) {
if ( ! $userManager -> isInGroup ( $groupName , $wikiName , false )) {
return true ;
}
} elseif ( $forceGroupCreation ) {
return true ;
}
return false ;
}
private function findANewNotExistingUserName ( string $firstWikiName ) : string
{
// remove last numbers
$baseWikiName = preg_replace ( " /[0-9]* $ / " , " " , $firstWikiName );
// a loop 1000 should be enough
for ( $i = 1 ; $i < 1000 ; $i ++ ) {
$newName = " $baseWikiName $i " ;
if ( ! $this -> isUserByName ( $newName )) {
return $newName ;
}
2023-12-21 19:06:49 +03:00
}
2024-02-05 14:12:59 +03:00
// if here, this is because all new usernames are existing
// it could be an error
throw new UserFieldException ( 'Impossible to find a new user name !' );
}
}