From 714764d96611a343a95f1ef62e6ff3de1dd59b87 Mon Sep 17 00:00:00 2001 From: Matt Porritt Date: Tue, 23 Nov 2021 04:31:07 +0000 Subject: [PATCH 1/2] MDL-77436 auth_oauth2: Allow admin to choose profile fields for mapping Update oauth2 to allow mapping of provider attributes against user profile fields. Fields can also be locked to prevent user changes. Co-Authored-By: Michael Milette --- .../classes/form/user_field_mapping.php | 2 +- .../tests/behat/user_profile_fields.feature | 39 ++++++++++++++++++ auth/oauth2/classes/api.php | 6 ++- auth/oauth2/classes/auth.php | 26 +++++++++--- auth/oauth2/settings.php | 3 +- lib/classes/oauth2/user_field_mapping.php | 41 ++++++++++++++++++- 6 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 admin/tool/oauth2/tests/behat/user_profile_fields.feature diff --git a/admin/tool/oauth2/classes/form/user_field_mapping.php b/admin/tool/oauth2/classes/form/user_field_mapping.php index aa118e82a34..c6f5280aa89 100644 --- a/admin/tool/oauth2/classes/form/user_field_mapping.php +++ b/admin/tool/oauth2/classes/form/user_field_mapping.php @@ -60,7 +60,7 @@ class user_field_mapping extends persistent { // Internal. $choices = $userfieldmapping->get_internalfield_list(); - $mform->addElement('select', 'internalfield', get_string('userfieldinternalfield', 'tool_oauth2'), $choices); + $mform->addElement('selectgroups', 'internalfield', get_string('userfieldinternalfield', 'tool_oauth2'), $choices); $mform->addHelpButton('internalfield', 'userfieldinternalfield', 'tool_oauth2'); $mform->addElement('hidden', 'action', 'edit'); diff --git a/admin/tool/oauth2/tests/behat/user_profile_fields.feature b/admin/tool/oauth2/tests/behat/user_profile_fields.feature new file mode 100644 index 00000000000..fcdded60891 --- /dev/null +++ b/admin/tool/oauth2/tests/behat/user_profile_fields.feature @@ -0,0 +1,39 @@ +@tool @tool_oauth2 @javascript + +Feature: OAuth2 user profile fields functionality + In order to use them later for authentication or repository plugins + As an administrator + I need to be able to map data fields provided by an Oauth2 provider + to custom user profile fields defined by an administrator. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | userwithinformation | userwithinformation | 1 | userwithinformation@example.com | + And I log in as "admin" + And I navigate to "Users > Accounts > User profile fields" in site administration + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" + And I set the following fields to these values: + | Short name | test_shortname | + | Name | test field name | + And I click on "Save changes" "button" + And I navigate to "Server > OAuth 2 services" in site administration + + Scenario: Verify custom user profile field mapping + Given I press "Microsoft" + And I should see "Create new service: Microsoft" + And I set the following fields to these values: + | Name | Testing service | + | Client ID | thisistheclientid | + | Client secret | supersecret | + When I press "Save changes" + Then I should see "Changes saved" + And I should see "Testing service" + And I click on "Configure user field mappings" "link" in the "Testing service" "table_row" + And I click on "Create new user field mapping for issuer \"Testing service\"" "button" + And I set the following fields to these values: + | External field name | sub | + | Internal field name | test field name | + And I click on "Save changes" "button" + And I should see "test_shortname" diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index cc160d38e2d..f26855741ff 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -260,7 +260,8 @@ class api { // Map supplied issuer user info to Moodle user fields. $userfieldmapping = new \core\oauth2\user_field_mapping(); - foreach ($userfieldmapping->get_internalfield_list() as $field) { + $userfieldlist = $userfieldmapping->get_internalfield_list(); + foreach (reset($userfieldlist) as $field) { if (isset($userinfo[$field]) && $userinfo[$field]) { $user->$field = $userinfo[$field]; } @@ -309,7 +310,8 @@ class api { // Map supplied issuer user info to Moodle user fields. $userfieldmapping = new \core\oauth2\user_field_mapping(); - foreach ($userfieldmapping->get_internalfield_list() as $field) { + $userfieldlist = $userfieldmapping->get_internalfield_list(); + foreach (reset($userfieldlist) as $field) { if (isset($userinfo[$field]) && $userinfo[$field]) { $user->$field = $userinfo[$field]; } diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 50ff36d3afa..fd2343f6bea 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -63,6 +63,7 @@ class auth extends \auth_plugin_base { public function __construct() { $this->authtype = 'oauth2'; $this->config = get_config('auth_oauth2'); + $this->customfields = $this->get_custom_user_profile_fields(); } /** @@ -309,23 +310,35 @@ class auth extends \auth_plugin_base { return $userdata; } + $allfields = array_merge($this->userfields, $this->customfields); + // Go through each field from the external data. foreach ($externaldata as $fieldname => $value) { - if (!in_array($fieldname, $this->userfields)) { + if (!in_array($fieldname, $allfields)) { // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer. continue; } - if (!property_exists($userdata, $fieldname)) { - // Just in case this field is on the list, but not part of the user data. This shouldn't happen though. + $userhasfield = property_exists($userdata, $fieldname); + // Find out if it is a profile field. + $isprofilefield = strpos($fieldname, 'profile_field_') === 0; + $profilefieldname = str_replace('profile_field_', '', $fieldname); + $userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile); + + // Just in case this field is on the list, but not part of the user data. This shouldn't happen though. + if (!($userhasfield || $userhasprofilefield)) { continue; } // Get the old value. - $oldvalue = (string)$userdata->$fieldname; + $oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname; // Get the lock configuration of the field. - $lockvalue = $this->config->{'field_lock_' . $fieldname}; + if (!empty($this->config->{'field_lock_' . $fieldname})) { + $lockvalue = $this->config->{'field_lock_' . $fieldname}; + } else { + $lockvalue = 'unlocked'; + } // We should update fields that meet the following criteria: // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty. @@ -525,6 +538,9 @@ class auth extends \auth_plugin_base { exit(); } else { \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true); + // We dont have profile loaded on $moodleuser, so load it. + require_once($CFG->dirroot.'/user/profile/lib.php'); + profile_load_custom_fields($moodleuser); $userinfo = $this->update_user($userinfo, $moodleuser); // No redirect, we will complete this login. } diff --git a/auth/oauth2/settings.php b/auth/oauth2/settings.php index cecdae54e9e..cbf7397ea3e 100644 --- a/auth/oauth2/settings.php +++ b/auth/oauth2/settings.php @@ -31,5 +31,6 @@ if ($ADMIN->fulltree) { $authplugin = get_auth_plugin('oauth2'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, - get_string('auth_fieldlocks_help', 'auth'), false, false); + get_string('auth_fieldlocks_help', 'auth'), false, false, + $authplugin->customfields); } diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index edc24bd8fc1..20d4561964d 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -43,7 +43,7 @@ class user_field_mapping extends persistent { * @return array */ private static function get_user_fields() { - return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); + return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username'], self::get_profile_field_names()); } /** @@ -72,7 +72,9 @@ class user_field_mapping extends persistent { * @return array */ public function get_internalfield_list() { - return array_combine(self::get_user_fields(), self::get_user_fields()); + $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); + $internalfields = array_combine($userfields, $userfields); + return array_merge(['' => $internalfields], self::get_profile_field_list()); } /** @@ -87,4 +89,39 @@ class user_field_mapping extends persistent { } return true; } + + /** + * Return the list of valid custom profile user fields. + * + * @return array array of profile field names + */ + private static function get_profile_field_names(): array { + $profilefields = profile_get_user_fields_with_data(0); + $profilefieldnames = []; + foreach ($profilefields as $field) { + $profilefieldnames[] = $field->inputname; + } + return $profilefieldnames; + } + + /** + * Return the list of profile fields + * in a format they can be used for choices in a group select menu. + * + * @return array array of category name with its profile fields + */ + private function get_profile_field_list(): array { + $customfields = profile_get_user_fields_with_data_by_category(0); + $data = []; + foreach ($customfields as $category) { + foreach ($category as $field) { + $categoryname = $field->get_category_name(); + if (!isset($data[$categoryname])) { + $data[$categoryname] = []; + } + $data[$categoryname][$field->inputname] = $field->field->name; + } + } + return $data; + } } From 110ec6a703d1014dfb3f595205c5eea86259b45e Mon Sep 17 00:00:00 2001 From: Meirza Date: Fri, 25 Nov 2022 22:22:44 +0700 Subject: [PATCH 2/2] MDL-77436 auth_oauth2: Update profile fields based on data mapping. After the user creation, the system must call an update function to update profile_fields_*. We also provided two functions into user/profile/lib.php to get available from other areas. We added PHP unit testing for new public functions and the Behat tests for custom profile fields with locked and unlocked statuses. Co-authored-by: Matt Porritt --- .../tests/behat/user_profile_fields.feature | 30 +-- auth/oauth2/classes/api.php | 54 ++++-- lib/classes/oauth2/user_field_mapping.php | 57 +++--- lib/tests/oauth2_test.php | 175 ++++++++++++++++++ user/profile/lib.php | 35 ++++ user/upgrade.txt | 9 + 6 files changed, 291 insertions(+), 69 deletions(-) diff --git a/admin/tool/oauth2/tests/behat/user_profile_fields.feature b/admin/tool/oauth2/tests/behat/user_profile_fields.feature index fcdded60891..bcb7d2da3c3 100644 --- a/admin/tool/oauth2/tests/behat/user_profile_fields.feature +++ b/admin/tool/oauth2/tests/behat/user_profile_fields.feature @@ -7,17 +7,11 @@ Feature: OAuth2 user profile fields functionality to custom user profile fields defined by an administrator. Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | userwithinformation | userwithinformation | 1 | userwithinformation@example.com | + Given the following "custom profile fields" exist: + | datatype | shortname | name | locked | + | text | unlocked_field | Unlocked field | 0 | + | text | locked_field | Locked field | 1 | And I log in as "admin" - And I navigate to "Users > Accounts > User profile fields" in site administration - And I click on "Create a new profile field" "link" - And I click on "Text input" "link" - And I set the following fields to these values: - | Short name | test_shortname | - | Name | test field name | - And I click on "Save changes" "button" And I navigate to "Server > OAuth 2 services" in site administration Scenario: Verify custom user profile field mapping @@ -31,9 +25,19 @@ Feature: OAuth2 user profile fields functionality Then I should see "Changes saved" And I should see "Testing service" And I click on "Configure user field mappings" "link" in the "Testing service" "table_row" + + # Create unlocked field And I click on "Create new user field mapping for issuer \"Testing service\"" "button" And I set the following fields to these values: - | External field name | sub | - | Internal field name | test field name | + | External field name | External unlocked | + | Internal field name | Unlocked field | And I click on "Save changes" "button" - And I should see "test_shortname" + And I should see "unlocked_field" + + # Create locked field + And I click on "Create new user field mapping for issuer \"Testing service\"" "button" + And I set the following fields to these values: + | External field name | External locked | + | Internal field name | Locked field | + And I click on "Save changes" "button" + And I should see "locked_field" diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index f26855741ff..50bc4f16756 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -258,16 +258,7 @@ class api { $user->password = ''; $user->confirmed = 1; // Set the user to confirmed. - // Map supplied issuer user info to Moodle user fields. - $userfieldmapping = new \core\oauth2\user_field_mapping(); - $userfieldlist = $userfieldmapping->get_internalfield_list(); - foreach (reset($userfieldlist) as $field) { - if (isset($userinfo[$field]) && $userinfo[$field]) { - $user->$field = $userinfo[$field]; - } - } - - $user->id = user_create_user($user, false, true); + $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); @@ -308,16 +299,7 @@ class api { $user->password = ''; $user->confirmed = 0; // The user is not yet confirmed. - // Map supplied issuer user info to Moodle user fields. - $userfieldmapping = new \core\oauth2\user_field_mapping(); - $userfieldlist = $userfieldmapping->get_internalfield_list(); - foreach (reset($userfieldlist) as $field) { - if (isset($userinfo[$field]) && $userinfo[$field]) { - $user->$field = $userinfo[$field]; - } - } - - $user->id = user_create_user($user, false, true); + $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); @@ -406,4 +388,36 @@ class api { public static function is_enabled() { return is_enabled_auth('oauth2'); } + + /** + * Create a new user & update the profile fields + * + * @param array $userinfo + * @param object $user + * @return object + */ + private static function save_user(array $userinfo, object $user): object { + // Map supplied issuer user info to Moodle user fields. + $userfieldmapping = new \core\oauth2\user_field_mapping(); + $userfieldlist = $userfieldmapping->get_internalfields(); + $hasprofilefield = false; + foreach ($userfieldlist as $field) { + if (isset($userinfo[$field]) && $userinfo[$field]) { + $user->$field = $userinfo[$field]; + + // Check whether the profile fields exist or not. + $hasprofilefield = $hasprofilefield || strpos($field, \core_user\fields::PROFILE_FIELD_PREFIX) === 0; + } + } + + // Create a new user. + $user->id = user_create_user($user, false, true); + + // If profile fields exist then save custom profile fields data. + if ($hasprofilefield) { + profile_save_data($user); + } + + return $user; + } } diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index 20d4561964d..cecbc9e84df 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -43,7 +43,7 @@ class user_field_mapping extends persistent { * @return array */ private static function get_user_fields() { - return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username'], self::get_profile_field_names()); + return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username'], get_profile_field_names()); } /** @@ -74,7 +74,26 @@ class user_field_mapping extends persistent { public function get_internalfield_list() { $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); $internalfields = array_combine($userfields, $userfields); - return array_merge(['' => $internalfields], self::get_profile_field_list()); + return array_merge(['' => $internalfields], get_profile_field_list()); + } + + /** + * Return the list of internal fields with flat array + * + * Profile fields element has its array based on profile category. + * These elements need to be turned flat to make it easier to read. + * + * @return array + */ + public function get_internalfields() { + $userfieldlist = $this->get_internalfield_list(); + $userfields = []; + array_walk_recursive($userfieldlist, + function($value, $key) use (&$userfields) { + $userfields[] = $key; + } + ); + return $userfields; } /** @@ -90,38 +109,4 @@ class user_field_mapping extends persistent { return true; } - /** - * Return the list of valid custom profile user fields. - * - * @return array array of profile field names - */ - private static function get_profile_field_names(): array { - $profilefields = profile_get_user_fields_with_data(0); - $profilefieldnames = []; - foreach ($profilefields as $field) { - $profilefieldnames[] = $field->inputname; - } - return $profilefieldnames; - } - - /** - * Return the list of profile fields - * in a format they can be used for choices in a group select menu. - * - * @return array array of category name with its profile fields - */ - private function get_profile_field_list(): array { - $customfields = profile_get_user_fields_with_data_by_category(0); - $data = []; - foreach ($customfields as $category) { - foreach ($category as $field) { - $categoryname = $field->get_category_name(); - if (!isset($data[$categoryname])) { - $data[$categoryname] = []; - } - $data[$categoryname][$field->inputname] = $field->field->name; - } - } - return $data; - } } diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index 8b707375f9e..b54766439ff 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -21,6 +21,7 @@ use core\oauth2\api; use core\oauth2\endpoint; use core\oauth2\issuer; use core\oauth2\system_account; +use \core\oauth2\user_field_mapping; /** * Tests for oauth2 apis (\core\oauth2\*). @@ -442,4 +443,178 @@ class oauth2_test extends \advanced_testcase { $this->assertFalse($googleissuer->is_available_for_login()); } + + /** + * Data provider for test_get_internalfield_list and test_get_internalfields. + * + * @return array + */ + public function create_custom_profile_fields(): array { + return [ + 'data' => + [ + 'given' => [ + 'Hobbies' => [ + 'shortname' => 'hobbies', + 'name' => 'Hobbies', + ] + ], + 'expected' => [ + 'Hobbies' => [ + 'shortname' => 'hobbies', + 'name' => 'Hobbies', + ] + ] + ], + [ + 'given' => [ + 'Billing' => [ + 'shortname' => 'billingaddress', + 'name' => 'Billing Address', + ], + 'Payment' => [ + 'shortname' => 'creditcardnumber', + 'name' => 'Credit Card Number', + ] + ], + 'expected' => [ + 'Billing' => [ + 'shortname' => 'billingaddress', + 'name' => 'Billing Address', + ], + 'Payment' => [ + 'shortname' => 'creditcardnumber', + 'name' => 'Credit Card Number', + ] + ] + ] + ]; + } + + /** + * Test getting the list of internal fields. + * + * @dataProvider create_custom_profile_fields + * @covers ::get_internalfield_list + * @param array $given Categories and profile fields. + * @param array $expected Expected value. + */ + public function test_get_internalfield_list(array $given, array $expected): void { + $this->resetAfterTest(); + self::generate_custom_profile_fields($given); + + $userfieldmapping = new user_field_mapping(); + $internalfieldlist = $userfieldmapping->get_internalfield_list(); + + foreach ($expected as $category => $value) { + // Custom profile fields must exist. + $this->assertNotEmpty($internalfieldlist[$category]); + + // Category must have the custom profile fields with expected value. + $this->assertEquals( + $internalfieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']], + $value['name'] + ); + } + } + + /** + * Test getting the list of internal fields with flat array. + * + * @dataProvider create_custom_profile_fields + * @covers ::get_internalfields + * @param array $given Categories and profile fields. + * @param array $expected Expected value. + */ + public function test_get_internalfields(array $given, array $expected): void { + $this->resetAfterTest(); + self::generate_custom_profile_fields($given); + + $userfieldmapping = new user_field_mapping(); + $internalfields = $userfieldmapping->get_internalfields(); + + // Custom profile fields must exist. + foreach ($expected as $category => $value) { + $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $internalfields); + } + } + + /** + * Test getting the list of empty external/custom profile fields. + * + * @covers ::get_internalfields + */ + public function test_get_empty_internalfield_list(): void { + + // Get internal (profile) fields. + $userfieldmapping = new user_field_mapping(); + $internalfieldlist = $userfieldmapping->get_internalfields(); + + // Get user fields. + $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); + + // Internal fields and user fields must exact same. + $this->assertEquals($userfields, $internalfieldlist); + } + + /** + * Test getting Return the list of profile fields. + * + * @dataProvider create_custom_profile_fields + * @covers ::get_profile_field_list + * @param array $given Categories and profile fields. + * @param array $expected Expected value. + */ + public function test_get_profile_field_list(array $given, array $expected): void { + $this->resetAfterTest(); + self::generate_custom_profile_fields($given); + + $profilefieldlist = get_profile_field_list(); + + foreach ($expected as $category => $value) { + $this->assertEquals( + $profilefieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']], + $value['name'] + ); + } + } + + /** + * Test getting the list of valid custom profile user fields. + * + * @dataProvider create_custom_profile_fields + * @covers ::get_profile_field_names + * @param array $given Categories and profile fields. + * @param array $expected Expected value. + */ + public function test_get_profile_field_names(array $given, array $expected): void { + $this->resetAfterTest(); + self::generate_custom_profile_fields($given); + + $profilefieldnames = get_profile_field_names(); + + // Custom profile fields must exist. + foreach ($expected as $category => $value) { + $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $profilefieldnames); + } + } + + /** + * Generate data into DB for Testing getting user fields mapping. + * + * @param array $given Categories and profile fields. + */ + private function generate_custom_profile_fields(array $given): void { + // Create a profile category and the profile fields. + foreach ($given as $category => $value) { + $customprofilefieldcategory = ['name' => $category, 'sortorder' => 1]; + $category = $this->getDataGenerator()->create_custom_profile_field_category($customprofilefieldcategory); + $this->getDataGenerator()->create_custom_profile_field( + ['shortname' => $value['shortname'], + 'name' => $value['name'], + 'categoryid' => $category->id, + 'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]); + } + } + } diff --git a/user/profile/lib.php b/user/profile/lib.php index d27bdf5e9aa..b6a2cc21812 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -980,3 +980,38 @@ function profile_has_required_custom_fields_set($userid) { return true; } + +/** + * Return the list of valid custom profile user fields. + * + * @return array array of profile field names + */ +function get_profile_field_names(): array { + $profilefields = profile_get_user_fields_with_data(0); + $profilefieldnames = []; + foreach ($profilefields as $field) { + $profilefieldnames[] = $field->inputname; + } + return $profilefieldnames; +} + +/** + * Return the list of profile fields + * in a format they can be used for choices in a group select menu. + * + * @return array array of category name with its profile fields + */ +function get_profile_field_list(): array { + $customfields = profile_get_user_fields_with_data_by_category(0); + $data = []; + foreach ($customfields as $category) { + foreach ($category as $field) { + $categoryname = $field->get_category_name(); + if (!isset($data[$categoryname])) { + $data[$categoryname] = []; + } + $data[$categoryname][$field->inputname] = $field->field->name; + } + } + return $data; +} diff --git a/user/upgrade.txt b/user/upgrade.txt index 290a5d92d64..74feef93571 100644 --- a/user/upgrade.txt +++ b/user/upgrade.txt @@ -5,6 +5,15 @@ This files describes API changes for code that uses the user API. * New method `core_user::is_current_user`, useful for components implementing permission callbacks for their preferences * New `profile_get_user_field` method for returning profile field instance of given type * The `profile_field_base::is_visible` method now accepts an optional `$context` argument +* Added get_internalfield_list() and get_internalfields() in the user_field_mapping class. + The get_internalfield_list() returns data in an array by grouping profile fields based on field categories, + used for internal field name dropdown in the user field mapping of Oauth2 services + The get_internalfields() converts the result from get_internalfield_list() into flat array, + used to save/update the profile data when a user uses OAuth2 services. +* Added get_profile_field_names() and get_profile_field_list() in the profile_field_base class. + The get_profile_field_names() returns the list of valid custom profile user fields. + The get_profile_field_list() returns the profile fields + in a format that can be used for choices in a group select menu. === 4.1 ===