Subversion Repositories web.creative

Rev

Blame | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Self Profile
 *
 * ProcessWire 3.x, Copyright 2018 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' => 104, 
      '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->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->input->post('submit_save_profile') || $fieldName) {
      $this->processInput($form, $fieldName); 
      if($fieldName) {
        // no need to redirect
      } else {
        $this->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;

    /** @var InputfieldForm $form */
    $form = $this->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:')
    ));
    $this->wire('modules')->get('JqueryUI')->use('vex');
    
    if(in_array('name', $this->profileFields) && empty($fieldName)) {
      /** @var InputfieldText $f */
      $f = $this->wire('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 = $this->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 = $this->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;
        // $f->notes = ($f->notes ? "$f->notes\n" : "") . $passRequiredNote;
      }
    }
  
    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; 
    $languages = $this->wire('languages');
    $form->processInput($this->input->post);

    if(count($form->getErrors())) {
      $this->error($this->_("Profile not saved")); 
      return;
    }
    
    $passValue = $this->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 = $this->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; 
        }
      }
      
      $v = $user->get($field->name);
      if($field->type instanceof FieldtypeModule) $v = "$v";
      
      if($inputfield->isChanged() || $v !== $value) {
        
        if(isset($this->passRequiredNames[$inputfield->name]) && !$passAuthenticated) {
          $inputfield->error($passFailedMessage);
          continue; 
        }

        if($languages && $inputfield->getSetting('useLanguages')) {
          if(is_object($v)) {
            $v->setFromInputfield($inputfield);
            $user->set($field->name, $v);
            $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); 
  }
  
  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',
    );

    /** @var Languages $languages */
    $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());
    $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');
  }


}