artabro/wire/modules/Process/ProcessProfile/ProcessProfile.module
2024-08-27 11:35:37 +02:00

477 lines
13 KiB
Text

<?php namespace ProcessWire;
/**
* ProcessWire User Profile Editor
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @property array $profileFields Names of fields user is allowed to edit in their profile
* @method bool|string isDisallowedUserName($value)
*
*/
class ProcessProfile extends Process implements ConfigurableModule, WirePageEditor {
public static function getModuleInfo() {
return array(
'title' => __('User Profile', __FILE__), // getModuleInfo title
'summary' => __('Enables user to change their password, email address and other settings that you define.', __FILE__), // getModuleInfo summary
'version' => 105,
'permanent' => true,
'permission' => 'profile-edit',
);
}
/**
* @var User
*
*/
protected $user;
/**
* Label for user “name”
*
* @var string
*
*/
protected $userNameLabel = '';
/**
* Password required for changes to these field names
*
* @var array
*
*/
protected $passRequiredNames = array();
/**
* Construct/establish initial module configuration
*
*/
public function __construct() {
$this->set('profileFields', array());
$this->userNameLabel = $this->_('User Login Name'); // Label for user login name
parent::__construct();
}
/**
* Execute/render profile edit form
*
* @return string
* @throws WireException
*
*/
public function ___execute() {
$fieldName = '';
if(isset($_SERVER['HTTP_X_FIELDNAME'])) {
$fieldName = $this->wire()->sanitizer->fieldName($_SERVER['HTTP_X_FIELDNAME']);
}
$this->user = $this->wire()->user;
$this->headline($this->_("Profile:") . ' ' . $this->user->name); // Primary Headline (precedes the username)
$form = $this->buildForm($fieldName);
if($this->wire()->input->post('submit_save_profile') || $fieldName) {
$this->processInput($form, $fieldName);
if($fieldName) {
// no need to redirect
} else {
$this->wire()->session->redirect("./");
}
} else {
return $form->render();
}
return '';
}
/**
* Build the form fields for adding a page
*
* @param string $fieldName
* @return InputfieldForm
*
*/
protected function buildForm($fieldName = '') {
/** @var User $user */
$user = $this->user;
$modules = $this->wire()->modules;
/** @var InputfieldForm $form */
$form = $modules->get('InputfieldForm');
$form->attr('id', 'ProcessProfile');
$form->attr('action', './');
$form->attr('method', 'post');
$form->attr('enctype', 'multipart/form-data');
$form->attr('autocomplete', 'off');
$form->addClass('InputfieldFormConfirm');
$fieldset = new InputfieldWrapper();
$this->wire($fieldset);
$form->add($fieldset);
// is password required to change some Inputfields?
$passRequired = false;
// Inputfields where password is required to change
$passRequiredInputfields = array();
$this->wire()->config->js('ProcessProfile', array(
'passRequiredAlert' => $this->_('For security, please enter your current password to save these changes:')
));
/** @var JqueryUI $jQueryUI */
$jQueryUI = $modules->get('JqueryUI');
$jQueryUI->use('vex');
if(in_array('name', $this->profileFields) && empty($fieldName)) {
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('id+name', '_user_name');
$f->label = $this->userNameLabel;
$f->description = $this->_('User name may contain lowercase a-z, 0-9, hyphen or underscore.');
$f->icon = 'sign-in';
$f->attr('value', $user->name);
$f->attr('pattern', '^[-_a-z0-9]+$');
$f->required = true;
$fieldset->add($f);
$f->setTrackChanges(true);
$passRequiredInputfields[] = $f;
}
foreach($user->fields as $field) {
if($field->name == 'roles' || !in_array($field->name, $this->profileFields)) continue;
if($fieldName && $field->name !== $fieldName) continue;
/** @var Field $field */
$field = $user->fields->getFieldContext($field);
/** @var Inputfield $inputfield */
$inputfield = $field->getInputfield($user);
if(!$inputfield) continue;
$inputfield->value = $user->get($field->name);
if($field->name === 'admin_theme') {
if(!$inputfield->value) $inputfield->value = $this->wire('config')->defaultAdminTheme;
} else if($field->type instanceof FieldtypeImage) {
if(!$user->hasPermission('page-edit-images', $user)) {
$inputfield->set('useImageEditor', false);
}
} else if($field->type instanceof FieldtypePassword && $field->name == 'pass') {
$inputfield->attr('autocomplete', 'off');
if($inputfield->getSetting('requireOld') == InputfieldPassword::requireOldAuto) {
$inputfield->set('requireOld', InputfieldPassword::requireOldYes);
}
if($inputfield->getSetting('requireOld') == InputfieldPassword::requireOldYes) {
$passRequired = true;
}
if(!$inputfield->getSetting('icon')) $inputfield->set('icon', 'key');
} else if($field->name === 'email') {
if(!$inputfield->getSetting('icon')) $inputfield->set('icon', 'envelope-o');
if(strlen($inputfield->value)) {
$passRequiredInputfields[] = $inputfield;
}
} else if($field->name === 'tfa_type') {
$passRequiredInputfields[] = $inputfield;
if(!$inputfield->val()) {
// initialize manually so it can add hooks (it just does some visual/wording tweaks)
$tfa = $this->wire(new Tfa()); /** @var Tfa $tfa */
$tfa->init();
}
}
$fieldset->add($inputfield);
}
/** @var InputfieldHidden $f */
// note used for processing, present only for front-end JS compatibility with ProcessPageEdit
$f = $modules->get('InputfieldHidden');
$f->attr('id', 'Inputfield_id');
$f->attr('name', 'id');
$f->attr('value', $user->id);
$f->addClass('InputfieldAllowAjaxUpload');
$fieldset->add($f);
/** @var InputfieldSubmit $field */
$field = $modules->get('InputfieldSubmit');
$field->attr('id+name', 'submit_save_profile');
$field->showInHeader();
$form->add($field);
if($passRequired && count($passRequiredInputfields)) {
foreach($passRequiredInputfields as $f) {
$f->addClass('InputfieldPassRequired', 'wrapClass');
$this->passRequiredNames[$f->name] = $f->name;
}
}
return $form;
}
/**
* Save the submitted page add form
*
* @param Inputfield $form
* @param string $fieldName
* @throws WireException
*
*/
protected function processInput(Inputfield $form, $fieldName = '') {
/** @var InputfieldForm $form */
$user = $this->user;
$input = $this->wire()->input;
$languages = $this->wire()->languages;
$form->processInput($input->post);
if(count($form->getErrors())) {
$this->error($this->_("Profile not saved"));
return;
}
$passValue = $input->post->string('_old_pass');
if(strlen($passValue)) {
$passAuthenticated = $user->pass->matches($passValue);
$passFailedMessage = $this->_('Required password was provided but is not correct');
} else {
$passAuthenticated = false;
$passFailedMessage = $this->_('Required password was not provided');
}
$user->of(false);
$user->setTrackChanges(true);
if(in_array('name', $this->profileFields) && empty($fieldName)) {
$f = $form->getChildByName('_user_name');
if($f && $f->isChanged()) {
if(isset($this->passRequiredNames[$f->name]) && !$passAuthenticated) {
$f->error($passFailedMessage);
} else {
$this->processInputUsername($f);
}
}
}
foreach($user->fields as $field) {
if($field->name == 'roles' || !in_array($field->name, $this->profileFields)) continue;
if($fieldName && $field->name !== $fieldName) continue;
$field = $user->fields->getFieldContext($field);
$inputfield = $form->getChildByName($field->name);
$value = $inputfield->attr('value');
if(empty($value) && in_array($field->name, array('pass', 'email'))) continue;
if($field->name == 'email' && strlen($value)) {
$selector = "id!=$user->id, include=all, email=" . $this->sanitizer->selectorValue($value);
if(count($this->users->find($selector))) {
$this->error(sprintf($this->_('Email address "%s" already in use by another user.'), $value));
continue;
}
}
$userValue = $user->get($field->name);
if($field->type instanceof FieldtypeModule) $userValue = "$userValue";
$changed = false;
if($inputfield->isChanged()) {
$changed = true;
} else if(is_array($value) && $userValue instanceof WireData) { // i.e. Combo
$userValueArray = $userValue->getArray();
$changed = $userValueArray != $value;
} else if($userValue !== $value) {
$changed = true;
}
if(!$changed) continue;
if(isset($this->passRequiredNames[$inputfield->name]) && !$passAuthenticated) {
$inputfield->error($passFailedMessage);
continue;
}
if($languages && $inputfield->getSetting('useLanguages')) {
if(is_object($userValue)) {
$userValue->setFromInputfield($inputfield);
$user->set($field->name, $userValue);
$user->trackChange($field->name);
} else {
$user->set($field->name, $value);
}
} else {
$user->set($field->name, $value);
}
}
if($user->isChanged()) {
$changes = implode(', ', array_unique($user->getChanges()));
$message = $this->_('Profile saved') . ' - ' . $changes;
$this->message($message);
$this->wire()->log->message($message);
$this->wire()->users->save($user);
}
$user->of(true);
}
/**
* Process username inputfield
*
* @param Inputfield $f The _user_name Inputfield
* @return bool Returns true if username changed allowed, false if not
*
*/
protected function processInputUsername(Inputfield $f) {
$user = $this->user;
$userName = $this->wire()->sanitizer->pageName($f->val());
if(empty($userName)) return false;
if($f->val() === $user->name) return false; // no change
if($userName === $user->name) return false; // no change after sanitization
/* at this point we know that user changed their name */
$error = $this->isDisallowedUserName($f->val());
if($error !== false) {
$f->error($error);
return false;
}
$user->name = $userName;
$languages = $this->wire()->languages;
if($languages && $languages->hasPageNames()) {
foreach($languages as $language) {
if(!$language->isDefault()) $user->set("name$language->id", $userName);
}
}
return true;
}
/**
* Return error message if user name is not allowed (to change to) or boolean false if it is
*
* @param string $value User name
* @return bool|string
*
*/
public function ___isDisallowedUserName($value) {
$disallowedNames = array(
'superuser',
'admin',
'administrator',
'root',
'guest',
'nobody',
);
$languages = $this->wire()->languages;
$notAllowedLabel = $this->_('Not allowed');
$userName = $this->wire()->sanitizer->pageName($value);
if($userName !== $value) {
return sprintf($this->_('Sanitized to “%s”, which differs from what you entered'), $userName);
}
if(strlen($userName) < 3) {
return $this->_('Too short');
} else if(strlen($userName) > 64) {
return $this->_('Too long');
}
if(in_array($userName, $disallowedNames)) {
return "$notAllowedLabel (#1)";
}
// check if user name is already in use
if($languages) $languages->setDefault();
$u = $this->wire()->users->get("name='$userName', include=all");
if($languages) $languages->unsetDefault();
if($u->id) {
return $this->_('Already in use');
}
$role = $this->wire()->roles->get("name='$userName', include=all");
if($role->id) {
return "$notAllowedLabel (#2)";
}
if(!ctype_alnum(substr($userName, 0, 1)) || !ctype_alnum(substr($userName, -1))) {
return $this->_('May not start or end with non-alpha, non-digit characters');
}
if(preg_match('/[-_.]{2,}/', $userName)) {
return $this->_('May not contain adjacent hyphens, underscores or periods');
}
return false;
}
/**
* Module configuration
*
* @param array $data
* @return InputfieldWrapper
* @throws WireException
*
*/
public function getModuleConfigInputfields(array $data) {
$profileFields = isset($data['profileFields']) ? $data['profileFields'] : array();
$fieldOptions = array();
foreach($this->wire()->users->getTemplates() as $template) {
foreach($template->fieldgroup as $field) {
$fieldOptions[$field->name] = $field;
}
}
ksort($fieldOptions);
$inputfields = $this->wire(new InputfieldWrapper());
/** @var InputfieldCheckboxes $f */
$f = $this->wire()->modules->get('InputfieldCheckboxes');
$f->label = $this->_("What fields can a user edit in their own profile?");
$f->attr('id+name', 'profileFields');
$f->icon = 'user-circle';
$f->table = true;
$f->thead =
$this->_('Name') . '|' .
$this->_('Label') . '|' .
$this->_('Type');
$f->addOption('name', "name|$this->userNameLabel|System");
foreach($fieldOptions as $name => $field) {
if($name == 'roles') continue;
$f->addOption($name, $name . '|' . str_replace('|', ' ', $field->getLabel()) . '|' . $field->type->shortName);
}
$f->attr('value', $profileFields);
$inputfields->add($f);
return $inputfields;
}
/**
* For WirePageEditor interface
*
* @return Page
*
*/
public function getPage() {
return $this->wire()->user;
}
}