Rev 1 | Blame | Compare with Previous | Last modification | View Log | Download
<?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 nameparent::__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 changeif($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 useif($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;}}