477 lines
13 KiB
Text
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;
|
|
}
|
|
|
|
|
|
}
|
|
|