MDL-64570 accessibility: autocomplete user profile fields
authorDamyon Wiese <damyon@moodle.com>
Mon, 14 Jan 2019 08:14:45 +0000 (16:14 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 19 Feb 2019 01:44:15 +0000 (09:44 +0800)
Only if editing your own user details and not logged in as someone else.

lib/behat/form_field/behat_form_field.php
login/change_password_form.php
login/forgot_password_form.php
theme/boost/templates/core_form/element-password.mustache
user/editadvanced_form.php
user/editlib.php
user/language_form.php
user/lib.php
user/tests/behat/behat_user.php
user/tests/behat/input-purpose.feature [new file with mode: 0644]

index 56d7124..f5b87ab 100644 (file)
@@ -136,6 +136,16 @@ class behat_form_field {
         return $instance->matches($expectedvalue);
     }
 
+    /**
+     * Get the value of an attribute set on this field.
+     *
+     * @param string $name The attribute name
+     * @return string The attribute value
+     */
+    public function get_attribute($name) {
+        return $this->field->getAttribute($name);
+    }
+
     /**
      * Guesses the element type we are dealing with in case is not a text-based element.
      *
index 5064690..4a25979 100644 (file)
@@ -26,7 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once $CFG->libdir.'/formslib.php';
+require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 class login_change_password_form extends moodleform {
 
@@ -51,7 +52,8 @@ class login_change_password_form extends moodleform {
         if ($policies) {
             $mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
         }
-        $mform->addElement('password', 'password', get_string('oldpassword'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'password');
+        $mform->addElement('password', 'password', get_string('oldpassword'), $purpose);
         $mform->addRule('password', get_string('required'), 'required', null, 'client');
         $mform->setType('password', PARAM_RAW);
 
index 32e5310..67a4d9f 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Reset forgotten password form definition.
@@ -40,12 +41,15 @@ class login_forgot_password_form extends moodleform {
      * Define the forgot password form.
      */
     function definition() {
+        global $USER;
+
         $mform    = $this->_form;
         $mform->setDisableShortforms(true);
 
         $mform->addElement('header', 'searchbyusername', get_string('searchbyusername'), '');
 
-        $mform->addElement('text', 'username', get_string('username'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'username');
+        $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose);
         $mform->setType('username', PARAM_RAW);
 
         $submitlabel = get_string('search');
@@ -53,7 +57,8 @@ class login_forgot_password_form extends moodleform {
 
         $mform->addElement('header', 'searchbyemail', get_string('searchbyemail'), '');
 
-        $mform->addElement('text', 'email', get_string('email'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'email');
+        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"' . $purpose);
         $mform->setType('email', PARAM_RAW_TRIMMED);
 
         $submitlabel = get_string('search');
index b2532fb..09f779f 100644 (file)
@@ -9,7 +9,7 @@
                 size="{{element.size}}"
                 {{#error}}
                     autofocus aria-describedby="id_error_{{element.name}}"
-                {{/error}} {{{attributes}}}>
+                {{/error}} {{{element.attributes}}}>
         {{/element.frozen}}
     {{/element}}
 {{/ core_form/element-template }}
index fe2fd31..31dd330 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
 }
 
 require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Class user_editadvanced_form.
@@ -96,7 +97,8 @@ class user_editadvanced_form extends moodleform {
             }
         }
 
-        $mform->addElement('text', 'username', get_string('username'), 'size="20"');
+        $purpose = user_edit_map_field_purpose($userid, 'username');
+        $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose);
         $mform->addHelpButton('username', 'username', 'auth');
         $mform->setType('username', PARAM_RAW);
 
@@ -116,7 +118,9 @@ class user_editadvanced_form extends moodleform {
         if (!empty($CFG->passwordpolicy)) {
             $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
         }
-        $mform->addElement('passwordunmask', 'newpassword', get_string('newpassword'), 'size="20"');
+
+        $purpose = user_edit_map_field_purpose($userid, 'password');
+        $mform->addElement('passwordunmask', 'newpassword', get_string('newpassword'), 'size="20"' . $purpose);
         $mform->addHelpButton('newpassword', 'newpassword');
         $mform->setType('newpassword', core_user::get_property_type('password'));
         $mform->disabledIf('newpassword', 'createpassword', 'checked');
index b071e25..c8d2f68 100644 (file)
@@ -22,6 +22,8 @@
  * @package core_user
  */
 
+require_once($CFG->dirroot . '/user/lib.php');
+
 /**
  * Cancels the requirement for a user to update their email address.
  *
@@ -258,7 +260,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
 
     // Add the necessary names.
     foreach (useredit_get_required_name_fields() as $fullname) {
-        $mform->addElement('text', $fullname,  get_string($fullname),  'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, $fullname);
+        $mform->addElement('text', $fullname,  get_string($fullname),  'maxlength="100" size="30"' . $purpose);
         if ($stringman->string_exists('missing'.$fullname, 'core')) {
             $strmissingfield = get_string('missing'.$fullname, 'core');
         } else {
@@ -271,7 +274,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     $enabledusernamefields = useredit_get_enabled_name_fields();
     // Add the enabled additional name fields.
     foreach ($enabledusernamefields as $addname) {
-        $mform->addElement('text', $addname,  get_string($addname), 'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, $addname);
+        $mform->addElement('text', $addname,  get_string($addname), 'maxlength="100" size="30"' . $purpose);
         $mform->setType($addname, PARAM_NOTAGS);
     }
 
@@ -282,7 +286,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
                 . get_string('emailchangecancel', 'auth') . '</a>';
         $mform->addElement('static', 'emailpending', get_string('email'), $notice);
     } else {
-        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, 'email');
+        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"' . $purpose);
         $mform->addRule('email', $strrequired, 'required', null, 'client');
         $mform->setType('email', PARAM_RAW_TRIMMED);
     }
@@ -301,9 +306,10 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
         $mform->setDefault('city', $CFG->defaultcity);
     }
 
+    $purpose = user_edit_map_field_purpose($user->id, 'country');
     $choices = get_string_manager()->get_list_of_countries();
     $choices = array('' => get_string('selectacountry') . '...') + $choices;
-    $mform->addElement('select', 'country', get_string('selectacountry'), $choices);
+    $mform->addElement('select', 'country', get_string('selectacountry'), $choices, $purpose);
     if (!empty($CFG->country)) {
         $mform->setDefault('country', core_user::get_property_default('country'));
     }
@@ -319,7 +325,9 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     }
 
     if ($user->id < 0) {
-        $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
+        $purpose = user_edit_map_field_purpose($user->id, 'lang');
+        $translations = get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose);
         $lang = empty($user->lang) ? $CFG->lang : $user->lang;
         $mform->setDefault('lang', $lang);
     }
@@ -366,7 +374,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     if (count($disabledusernamefields) > 0) {
         $mform->addElement('header', 'moodle_additional_names', get_string('additionalnames'));
         foreach ($disabledusernamefields as $allname) {
-            $mform->addElement('text', $allname, get_string($allname), 'maxlength="100" size="30"');
+            $purpose = user_edit_map_field_purpose($user->id, $allname);
+            $mform->addElement('text', $allname, get_string($allname), 'maxlength="100" size="30"' . $purpose);
             $mform->setType($allname, PARAM_NOTAGS);
         }
     }
index 4dab6be..50d0911 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
 }
 
 require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Class user_edit_form.
@@ -57,7 +58,9 @@ class user_edit_language_form extends moodleform {
         $mform->addElement('hidden', 'course', $COURSE->id);
         $mform->setType('course', PARAM_INT);
 
-        $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
+        $purpose = user_edit_map_field_purpose($userid, 'lang');
+        $translations = get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose);
         $mform->setDefault('lang', core_user::get_property_default('lang'));
 
         $this->add_action_buttons(true, get_string('savechanges'));
index 21b0abe..e170a23 100644 (file)
@@ -1572,3 +1572,42 @@ function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
         return \core_user\output\user_roles_editable::update($itemid, $newvalue);
     }
 }
+
+/**
+ * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
+ *
+ * @param integer $userid
+ * @param string $fieldname
+ * @return string $purpose (empty string if there is no mapping).
+ */
+function user_edit_map_field_purpose($userid, $fieldname) {
+    global $USER;
+
+    $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
+    // These are the fields considered valid to map and auto fill from a browser.
+    // We do not include fields that are in a collapsed section by default because
+    // the browser could auto-fill the field and cause a new value to be saved when
+    // that field was never visible.
+    $validmappings = array(
+        'username' => 'username',
+        'password' => 'current-password',
+        'firstname' => 'given-name',
+        'lastname' => 'family-name',
+        'middlename' => 'additional-name',
+        'email' => 'email',
+        'country' => 'country',
+        'lang' => 'language'
+    );
+
+    $purpose = '';
+    if (!$currentuser) {
+        // Do not set a purpose.
+        $purpose = '';
+    }
+    if (isset($validmappings[$fieldname])) {
+        $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
+    }
+
+    return $purpose;
+}
+
index 36339e6..2a6f3da 100644 (file)
@@ -53,4 +53,36 @@ class behat_user extends behat_base {
         $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']" .
                                                           "/option[contains(., " . $nodetext . ")]", "xpath_element"));
     }
+
+    /**
+     * The input field should have autocomplete set to this value.
+     *
+     * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     * @param string $field The field to select.
+     * @param string $purpose The expected purpose.
+     */
+    public function the_field_should_have_purpose($field, $purpose) {
+        $fld = behat_field_manager::get_form_field_from_label($field, $this);
+
+        $value = $fld->get_attribute('autocomplete');
+        if ($value != $purpose) {
+            throw new ExpectationException('The "' . $field . '" field does not have purposea "' . $purpose . '"', $this->getSession());
+        }
+    }
+
+    /**
+     * The input field should not have autocomplete set to this value.
+     *
+     * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should not have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     * @param string $field The field to select.
+     * @param string $purpose The expected purpose we do not want.
+     */
+    public function the_field_should_not_have_purpose($field, $purpose) {
+        $fld = behat_field_manager::get_form_field_from_label($field, $this);
+
+        $value = $fld->get_attribute('autocomplete');
+        if ($value == $purpose) {
+            throw new ExpectationException('The "' . $field . '" field does have purposea "' . $purpose . '"', $this->getSession());
+        }
+    }
 }
diff --git a/user/tests/behat/input-purpose.feature b/user/tests/behat/input-purpose.feature
new file mode 100644 (file)
index 0000000..f466f65
--- /dev/null
@@ -0,0 +1,43 @@
+@core @core_user
+Feature: The purpose of each input field collecting information about the user can be determined
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname          | lastname | email                           |
+      | unicorn  | unicorn | 1        | unicorn@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "course enrolments" exist:
+      | user                | course | role    |
+      | unicorn             | C1     | student |
+
+  @javascript
+  Scenario: Fields for other users are not auto filled
+    When I log in as "admin"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I click on ".icon[title=Edit]" "css_element" in the "unicorn@example.com" "table_row"
+    And I expand all fieldsets
+    Then the field "Username" should not have purpose "username"
+    And the field "First name" should not have purpose "given-name"
+    And the field "Surname" should not have purpose "family-name"
+    And the field "Email" should not have purpose "email"
+    And the field "Select a country" should not have purpose "country"
+    And I press "Cancel"
+    And I follow "Preferred language"
+    And the field "Preferred language" should not have purpose "language"
+
+  @javascript
+  Scenario: My own user fields are auto filled
+    When I log in as "unicorn"
+    And I follow "Profile" in the user menu
+    And I click on "Edit profile" "link" in the "region-main" "region"
+    And I expand all fieldsets
+    Then the field "First name" should have purpose "given-name"
+    And the field "Surname" should have purpose "family-name"
+    And the field "Email" should have purpose "email"
+    And the field "Select a country" should have purpose "country"
+    And I press "Cancel"
+    And I follow "Preferences" in the user menu
+    And I follow "Preferred language"
+    And the field "Preferred language" should have purpose "language"