yeswiki-custom-reseau.s-mar.../fields/YunohostUserField.php
2024-02-05 14:12:59 +03:00

482 lines
17 KiB
PHP

<?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
{
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];
}
}
return $users;
}
protected function addYunohostUser($userValues)
{
$users = $this->getYunohostUsernames();
if (in_array($userValues['name'], $users)) {
throw new UserNameAlreadyUsedException();
}
$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]);
}
}
$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')
);
}
if ($value !== $loggedUser['name'] && $this->getWiki()->UserIsAdmin()) {
$associatedUser = $userManager->getOneByName($value);
}
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')
) : '');
}
}
}
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;
}
}
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,
])
);
}
}
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");
}
if ($entry['mot_de_passe_wikini'] !== $entry['mot_de_passe_repete_wikini']) {
throw new UserFieldException(_t('USER_PASSWORDS_NOT_IDENTICAL'));
}
}
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);
}
} 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);
}
}
}
// 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')
]);
}
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);
}
}
}
}
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;
}
}
}
} else {
$groupName = substr($groupName, 1);
if ($this->userMustBeAddedToGroup($wikiName, $groupName, $forceGroupCreation, $userManager, $existingsGroups)) {
$groupsNames[] = $groupName;
}
}
}
$groupsNames = array_unique($groupsNames);
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;
}
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;
}
}
// 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 !');
}
}