__('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; if($this->wire()->modules->isInstalled('LanguageSupportPageNames')) { foreach($this->wire()->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; } }