Merge branch 'MDL-67748-master-managewebservices' of git://github.com/mudrd8mz/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2021-03-16 19:02:10 +01:00
commit f0733b2f41
28 changed files with 1422 additions and 480 deletions

View File

@ -401,55 +401,6 @@ if ($hassiteconfig) {
/** @var \core\plugininfo\repository $plugin */
$plugin->load_settings($ADMIN, 'repositorysettings', $hassiteconfig);
}
/// Web services
$ADMIN->add('modules', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
/// overview page
$temp = new admin_settingpage('webservicesoverview', new lang_string('webservicesoverview', 'webservice'));
$temp->add(new admin_setting_webservicesoverview());
$ADMIN->add('webservicesettings', $temp);
//API documentation
$ADMIN->add('webservicesettings', new admin_externalpage('webservicedocumentation', new lang_string('wsdocapi', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/documentation.php", 'moodle/site:config', false));
/// manage service
$temp = new admin_settingpage('externalservices', new lang_string('externalservices', 'webservice'));
$temp->add(new admin_setting_heading('manageserviceshelpexplaination', new lang_string('information', 'webservice'), new lang_string('servicehelpexplanation', 'webservice')));
$temp->add(new admin_setting_manageexternalservices());
$ADMIN->add('webservicesettings', $temp);
$ADMIN->add('webservicesettings', new admin_externalpage('externalservice', new lang_string('editaservice', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service.php", 'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalservicefunctions', new lang_string('externalservicefunctions', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_functions.php", 'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusers', new lang_string('externalserviceusers', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_users.php", 'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusersettings', new lang_string('serviceusersettings', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_user_settings.php", 'moodle/site:config', true));
/// manage protocol page link
$temp = new admin_settingpage('webserviceprotocols', new lang_string('manageprotocols', 'webservice'));
$temp->add(new admin_setting_managewebserviceprotocols());
if (empty($CFG->enablewebservices)) {
$temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
}
// We cannot use $OUTPUT this early, doing so means that we lose the ability
// to set the page layout on all admin pages.
// $wsdoclink = $OUTPUT->doc_link('How_to_get_a_security_key');
$url = new moodle_url(get_docs_url('How_to_get_a_security_key'));
$wsdoclink = html_writer::tag('a', new lang_string('supplyinfo', 'webservice'), array('href'=>$url));
$temp->add(new admin_setting_configcheckbox('enablewsdocumentation', new lang_string('enablewsdocumentation',
'admin'), new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
$ADMIN->add('webservicesettings', $temp);
/// links to protocol pages
$plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
core_collator::asort_objects_by_property($plugins, 'displayname');
foreach ($plugins as $plugin) {
/** @var \core\plugininfo\webservice $plugin */
$plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
}
/// manage token page link
$ADMIN->add('webservicesettings', new admin_externalpage('addwebservicetoken', new lang_string('managetokens', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/tokens.php", 'moodle/site:config', true));
$temp = new admin_settingpage('webservicetokens', new lang_string('managetokens', 'webservice'));
$temp->add(new admin_setting_managewebservicetokens());
if (empty($CFG->enablewebservices)) {
$temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
}
$ADMIN->add('webservicesettings', $temp);
}
// Question type settings

View File

@ -546,4 +546,67 @@ if ($hassiteconfig) {
new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
$ADMIN->add('server', $temp);
}
// Web services.
$ADMIN->add('server', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
// Web services > Overview.
$temp = new admin_settingpage('webservicesoverview', new lang_string('webservicesoverview', 'webservice'));
$temp->add(new admin_setting_webservicesoverview());
$ADMIN->add('webservicesettings', $temp);
// Web services > API documentation.
$ADMIN->add('webservicesettings', new admin_externalpage('webservicedocumentation', new lang_string('wsdocapi', 'webservice'),
"{$CFG->wwwroot}/{$CFG->admin}/webservice/documentation.php", 'moodle/site:config', false));
// Web services > External services.
$temp = new admin_settingpage('externalservices', new lang_string('externalservices', 'webservice'));
$temp->add(new admin_setting_heading('manageserviceshelpexplaination', new lang_string('information', 'webservice'),
new lang_string('servicehelpexplanation', 'webservice')));
$temp->add(new admin_setting_manageexternalservices());
$ADMIN->add('webservicesettings', $temp);
$ADMIN->add('webservicesettings', new admin_externalpage('externalservice', new lang_string('editaservice', 'webservice'),
"{$CFG->wwwroot}/{$CFG->admin}/webservice/service.php", 'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalservicefunctions',
new lang_string('externalservicefunctions', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_functions.php",
'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusers',
new lang_string('externalserviceusers', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_users.php",
'moodle/site:config', true));
$ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusersettings',
new lang_string('serviceusersettings', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_user_settings.php",
'moodle/site:config', true));
// Web services > Manage protocols.
$temp = new admin_settingpage('webserviceprotocols', new lang_string('manageprotocols', 'webservice'));
$temp->add(new admin_setting_managewebserviceprotocols());
if (empty($CFG->enablewebservices)) {
$temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
}
// We cannot use $OUTPUT->doc_link() this early, we would lose the ability to set the page layout on all admin pages.
$url = new moodle_url(get_docs_url('How_to_get_a_security_key'));
$wsdoclink = html_writer::link($url, new lang_string('supplyinfo', 'webservice'), ['target' => '_blank']);
$temp->add(new admin_setting_configcheckbox('enablewsdocumentation', new lang_string('enablewsdocumentation', 'admin'),
new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
$ADMIN->add('webservicesettings', $temp);
$plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
core_collator::asort_objects_by_property($plugins, 'displayname');
foreach ($plugins as $plugin) {
/** @var \core\plugininfo\webservice $plugin */
$plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
}
// Web services > Manage tokens.
$ADMIN->add('webservicesettings', new admin_externalpage('webservicetokens', new lang_string('managetokens', 'webservice'),
new moodle_url('/admin/webservice/tokens.php')));
}

View File

@ -1,27 +1,76 @@
@core @core_admin
Feature: Manage tokens
In order to manage webservice usage
Feature: Manage external services tokens
In order to manage external service usage
As an admin
I need to be able to create and delete tokens
I need to be able to create, filter and delete tokens
Background:
Given the following "users" exist:
| username | password | firstname | lastname |
| testuser | testuser | Joe | Bloggs |
| testuser2 | testuser2 | TestFirstname | TestLastname |
| username | password | firstname | lastname |
| user1 | user1 | Firstname1 | Lastname1 |
| user2 | user2 | Firstname2 | Lastname2 |
| user3 | user3 | Firstname3 | Lastname3 |
| user4 | user4 | Firstname4 | Lastname4 |
And I change window size to "small"
And I log in as "admin"
And I am on site homepage
@javascript
Scenario: Add & delete a token
Given I navigate to "Plugins > Web services > Manage tokens" in site administration
And I follow "Add"
And I set the field "User" to "Joe Bloggs"
Scenario: Add a token to user identified by name and then delete that token
Given I log in as "admin"
And I am on site homepage
And I navigate to "Server > Web services > Manage tokens" in site administration
And I press "Create token"
And I set the field "User" to "Firstname1 Lastname1"
And I set the field "Service" to "Moodle mobile web service"
And I set the field "IP restriction" to "127.0.0.1"
When I press "Save changes"
Then I should see "Joe Bloggs"
And I should see "127.0.0.1"
And I follow "Delete"
Then I should see "Moodle mobile web service" in the "Firstname1 Lastname1" "table_row"
And I should see "127.0.0.1" in the "Firstname1 Lastname1" "table_row"
And I click on "Delete" "link" in the "Firstname1 Lastname1" "table_row"
And I should see "Do you really want to delete this web service token for Firstname1 Lastname1 on the service Moodle mobile web service?"
And I press "Delete"
And I should not see "Joe Bloggs"
And "Firstname1 Lastname1" "table_row" should not exist
@javascript
Scenario: Tokens can be filtered by user and by service
Given the following "core_webservice > Service" exists:
| name | Site information |
| shortname | siteinfo |
| enabled | 1 |
And the following "core_webservice > Service function" exists:
| service | siteinfo |
| functions | core_webservice_get_site_info |
And the following "core_webservice > Tokens" exist:
| user | service |
| user2 | siteinfo |
| user3 | moodle_mobile_app |
| user4 | siteinfo |
When I log in as "admin"
And I navigate to "Server > Web services > Manage tokens" in site administration
# All created tokens are shown by default.
And "Firstname1 Lastname1" "table_row" should not exist
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
# Filter tokens by user (note we can select the user by the identity field here).
When I click on "Tokens filter" "link"
And I set the field "User" to "user2@example.com"
And I press "Show only matching tokens"
Then "Firstname3 Lastname3" "table_row" should not exist
And "Firstname4 Lastname4" "table_row" should not exist
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
# Reset the filter.
And I press "Show all tokens"
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
# Filter tokens by service.
And I click on "Tokens filter" "link"
And I set the field "Service" to "Site information"
And I press "Show only matching tokens"
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
And "Firstname3 Lastname3" "table_row" should not exist

View File

@ -4,6 +4,9 @@ This files describes API changes in /admin/*.
* New admin setting admin_setting_encryptedpassword allows passwords in admin settings to be
encrypted (with the new \core\encryption API) so that even the admin cannot read them.
* Web services administration has been moved from Plugins into the Server category. If you have
Behat tests containing steps like `Given I navigate to "Plugins > Web services > ..."`, you will
want to replace them with `Given I navigate to "Server > Web services > ..."`.
=== 3.9 ===

View File

@ -221,121 +221,3 @@ class external_service_functions_form extends moodleform {
}
}
class web_service_token_form extends moodleform {
function definition() {
global $USER, $DB, $CFG;
$mform = $this->_form;
$data = $this->_customdata;
$mform->addElement('header', 'token', get_string('token', 'webservice'));
if (empty($data->nouserselection)) {
//check if the number of user is reasonable to be displayed in a select box
$usertotal = $DB->count_records('user',
array('deleted' => 0, 'suspended' => 0, 'confirmed' => 1));
if ($usertotal < 500) {
list($sort, $params) = users_order_by_sql('u');
// User searchable selector - return users who are confirmed, not deleted, not suspended and not a guest.
$userfieldsapi = \core\user_fields::for_name();
$sql = 'SELECT u.id' . $userfieldsapi->get_sql('u')->selects . '
FROM {user} u
WHERE u.deleted = 0
AND u.confirmed = 1
AND u.suspended = 0
AND u.id != :siteguestid
ORDER BY ' . $sort;
$params['siteguestid'] = $CFG->siteguest;
$users = $DB->get_records_sql($sql, $params);
$options = array();
foreach ($users as $userid => $user) {
$options[$userid] = fullname($user);
}
$mform->addElement('searchableselector', 'user', get_string('user'), $options);
$mform->setType('user', PARAM_INT);
} else {
//simple text box for username or user id (if two username exists, a form error is displayed)
$mform->addElement('text', 'user', get_string('usernameorid', 'webservice'));
$mform->setType('user', PARAM_RAW_TRIMMED);
}
$mform->addRule('user', get_string('required'), 'required', null, 'client');
}
//service selector
$services = $DB->get_records('external_services');
$options = array();
$systemcontext = context_system::instance();
foreach ($services as $serviceid => $service) {
//check that the user has the required capability
//(only for generation by the profile page)
if (empty($data->nouserselection)
|| empty($service->requiredcapability)
|| has_capability($service->requiredcapability, $systemcontext, $USER->id)) {
$options[$serviceid] = $service->name;
}
}
$mform->addElement('select', 'service', get_string('service', 'webservice'), $options);
$mform->addRule('service', get_string('required'), 'required', null, 'client');
$mform->setType('service', PARAM_INT);
$mform->addElement('text', 'iprestriction', get_string('iprestriction', 'webservice'));
$mform->setType('iprestriction', PARAM_RAW_TRIMMED);
$mform->addElement('date_selector', 'validuntil',
get_string('validuntil', 'webservice'), array('optional' => true));
$mform->setType('validuntil', PARAM_INT);
$mform->addElement('hidden', 'action');
$mform->setType('action', PARAM_ALPHANUMEXT);
$this->add_action_buttons(true);
$this->set_data($data);
}
function get_data() {
global $DB;
$data = parent::get_data();
if (!empty($data) && !is_numeric($data->user)) {
//retrieve username
$user = $DB->get_record('user', array('username' => $data->user), 'id');
$data->user = $user->id;
}
return $data;
}
function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (is_numeric($data['user'])) {
$searchtype = 'id';
} else {
$searchtype = 'username';
//check the username is valid
if (clean_param($data['user'], PARAM_USERNAME) != $data['user']) {
$errors['user'] = get_string('invalidusername');
}
}
if (!isset($errors['user'])) {
$users = $DB->get_records('user', array($searchtype => $data['user']), '', 'id');
//check that the user exists in the database
if (count($users) == 0) {
$errors['user'] = get_string('usernameoridnousererror', 'webservice');
} else if (count($users) > 1) { //can only be a username search as id are unique
$errors['user'] = get_string('usernameoridoccurenceerror', 'webservice');
}
}
return $errors;
}
}

View File

@ -108,7 +108,7 @@ $usersmissingcaps = $webservicemanager->get_missing_capabilities_by_users($allow
//add the missing capabilities to the allowed users object to be displayed by renderer
foreach ($allowedusers as &$alloweduser) {
if (!is_siteadmin($alloweduser->id) and array_key_exists($alloweduser->id, $usersmissingcaps)) {
$alloweduser->missingcapabilities = implode(', ', $usersmissingcaps[$alloweduser->id]);
$alloweduser->missingcapabilities = $usersmissingcaps[$alloweduser->id];
}
}

View File

@ -1,6 +1,5 @@
<?php
// This file is part of Moodle - http://moodle.org/
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -16,112 +15,149 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Web services tokens admin UI
* Web services / external tokens management UI.
*
* @package webservice
* @author Jerome Mouneyrac
* @copyright 2009 Moodle Pty Ltd (http://moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package core_webservice
* @category admin
* @copyright 2009 Jerome Mouneyrac
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../../config.php');
require(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->dirroot . '/' . $CFG->admin . '/webservice/forms.php');
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/webservice/lib.php');
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
$tokenid = optional_param('tokenid', '', PARAM_SAFEDIR);
$confirm = optional_param('confirm', 0, PARAM_BOOL);
$ftoken = optional_param('ftoken', '', PARAM_ALPHANUM);
$fusers = optional_param_array('fusers', [], PARAM_INT);
$fservices = optional_param_array('fservices', [], PARAM_INT);
admin_externalpage_setup('addwebservicetoken');
admin_externalpage_setup('webservicetokens');
//Deactivate the second 'Manage token' navigation node, and use the main 'Manage token' navigation node
$node = $PAGE->settingsnav->find('addwebservicetoken', navigation_node::TYPE_SETTING);
$newnode = $PAGE->settingsnav->find('webservicetokens', navigation_node::TYPE_SETTING);
if ($node && $newnode) {
$node->display = false;
$newnode->make_active();
if ($action === 'create') {
$webservicemanager = new webservice();
$mform = new \core_webservice\token_form(null, ['action' => 'create']);
$data = $mform->get_data();
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data) {
ignore_user_abort(true);
// Check the user is allowed for the service.
$selectedservice = $webservicemanager->get_external_service_by_id($data->service);
if ($selectedservice->restrictedusers) {
$restricteduser = $webservicemanager->get_ws_authorised_user($data->service, $data->user);
if (empty($restricteduser)) {
$errormsg = $OUTPUT->notification(get_string('usernotallowed', 'webservice', $selectedservice->name));
}
}
$user = \core_user::get_user($data->user, '*', MUST_EXIST);
\core_user::require_active_user($user);
// Generate the token.
if (empty($errormsg)) {
external_generate_token(EXTERNAL_TOKEN_PERMANENT, $data->service, $data->user, context_system::instance(),
$data->validuntil, $data->iprestriction);
redirect($PAGE->url);
}
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('createtoken', 'webservice'));
if (!empty($errormsg)) {
echo $errormsg;
}
$mform->display();
echo $OUTPUT->footer();
die();
}
if ($action === 'delete') {
$webservicemanager = new webservice();
$token = $webservicemanager->get_token_by_id_with_details($tokenid);
$tokenlisturl = new moodle_url("/" . $CFG->admin . "/settings.php", array('section' => 'webservicetokens'));
if ($token->creatorid != $USER->id) {
require_capability('moodle/webservice:managealltokens', context_system::instance());
}
require_once($CFG->dirroot . "/webservice/lib.php");
$webservicemanager = new webservice();
if ($confirm && confirm_sesskey()) {
$webservicemanager->delete_user_ws_token($token->id);
redirect($PAGE->url);
}
switch ($action) {
echo $OUTPUT->header();
case 'create':
$mform = new web_service_token_form(null, array('action' => 'create'));
$data = $mform->get_data();
if ($mform->is_cancelled()) {
redirect($tokenlisturl);
} else if ($data and confirm_sesskey()) {
ignore_user_abort(true);
echo $OUTPUT->confirm(
get_string('deletetokenconfirm', 'webservice', [
'user' => $token->firstname . ' ' . $token->lastname,
'service' => $token->name,
]),
new single_button(new moodle_url('/admin/webservice/tokens.php', [
'tokenid' => $token->id,
'action' => 'delete',
'confirm' => 1,
'sesskey' => sesskey(),
]), get_string('delete')),
$PAGE->url
);
//check the the user is allowed for the service
$selectedservice = $webservicemanager->get_external_service_by_id($data->service);
if ($selectedservice->restrictedusers) {
$restricteduser = $webservicemanager->get_ws_authorised_user($data->service, $data->user);
if (empty($restricteduser)) {
$allowuserurl = new moodle_url('/' . $CFG->admin . '/webservice/service_users.php',
array('id' => $selectedservice->id));
$allowuserlink = html_writer::tag('a', $selectedservice->name , array('href' => $allowuserurl));
$errormsg = $OUTPUT->notification(get_string('usernotallowed', 'webservice', $allowuserlink));
}
}
//check if the user is deleted. unconfirmed, suspended or guest
$user = $DB->get_record('user', array('id' => $data->user));
if ($user->id == $CFG->siteguest or $user->deleted or !$user->confirmed or $user->suspended) {
throw new moodle_exception('forbiddenwsuser', 'webservice');
}
//process the creation
if (empty($errormsg)) {
//TODO improvement: either move this function from externallib.php to webservice/lib.php
// either move most of webservicelib.php functions into externallib.php
// (create externalmanager class) MDL-23523
external_generate_token(EXTERNAL_TOKEN_PERMANENT, $data->service,
$data->user, context_system::instance(),
$data->validuntil, $data->iprestriction);
redirect($tokenlisturl);
}
}
//OUTPUT: create token form
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('createtoken', 'webservice'));
if (!empty($errormsg)) {
echo $errormsg;
}
$mform->display();
echo $OUTPUT->footer();
die;
break;
case 'delete':
$token = $webservicemanager->get_token_by_id_with_details($tokenid);
if ($token->creatorid != $USER->id) {
require_capability("moodle/webservice:managealltokens", context_system::instance());
}
//Delete the token
if ($confirm and confirm_sesskey()) {
$webservicemanager->delete_user_ws_token($token->id);
redirect($tokenlisturl);
}
////OUTPUT: display delete token confirmation box
echo $OUTPUT->header();
$renderer = $PAGE->get_renderer('core', 'webservice');
echo $renderer->admin_delete_token_confirmation($token);
echo $OUTPUT->footer();
die;
break;
default:
//wrong url access
redirect($tokenlisturl);
break;
echo $OUTPUT->footer();
die();
}
// Pre-populate the form with the values that come as a part of the URL - typically when using the table_sql control
// links.
$filterdata = (object)[
'token' => $ftoken,
'users' => $fusers,
'services' => $fservices,
];
$filter = new \core_webservice\token_filter($PAGE->url, $filterdata);
$filter->set_data($filterdata);
if ($filter->is_submitted()) {
$filterdata = $filter->get_data();
if (isset($filterdata->resetbutton)) {
redirect($PAGE->url);
}
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('managetokens', 'core_webservice'));
echo html_writer::div($OUTPUT->render(new single_button(new moodle_url($PAGE->url, ['action' => 'create']),
get_string('createtoken', 'core_webservice'), 'get', true)), 'my-3');
$filter->display();
$table = new \core_webservice\token_table('webservicetokens', $filterdata);
// In order to not lose the filter form values by clicking the table control links, make them part of the table's baseurl.
$baseurl = new moodle_url($PAGE->url, ['ftoken' => $filterdata->token]);
foreach ($filterdata->users as $i => $userid) {
$baseurl->param("fusers[{$i}]", $userid);
}
foreach ($filterdata->services as $i => $serviceid) {
$baseurl->param("fservices[{$i}]", $serviceid);
}
$table->define_baseurl($baseurl);
$table->attributes['class'] = 'admintable generaltable';
$table->data = [];
$table->out(30, false);
echo $OUTPUT->footer();

View File

@ -129,3 +129,5 @@ hidepicture,core_group
hidepicture,core_moodle
sitebackpack,core_badges
sitebackpack_help,core_badges
usernameoridnousererror,core_webservice
usernameoridoccurenceerror,core_webservice

View File

@ -118,7 +118,7 @@ $string['loginrequired'] = 'Restricted to logged-in users';
$string['manageprotocols'] = 'Manage protocols';
$string['managetokens'] = 'Manage tokens';
$string['missingcaps'] = 'Missing capabilities';
$string['missingcaps_help'] = 'List of required capabilities for the service which the selected user does not have. Missing capabilities must be added to the user\'s role in order to use the service.';
$string['missingcaps_help'] = 'List of capabilities declared by the service which the user does not have. Some service functionality may not be available without these capabilities.';
$string['missingpassword'] = 'Missing password';
$string['missingrequiredcapability'] = 'The capability {$a} is required.';
$string['missingusername'] = 'Missing username';
@ -130,7 +130,7 @@ $string['norequiredcapability'] = 'No required capability';
$string['notoken'] = 'The token list is empty.';
$string['onesystemcontrolling'] = 'Allow an external system to control Moodle';
$string['onesystemcontrollingdescription'] = 'The following steps help you to set up the Moodle web services to allow an external system to interact with Moodle. This includes setting up a token (security key) authentication method.';
$string['onlyseecreatedtokens'] = 'Any token can be deleted, though you can only view tokens that you created.';
$string['onlyseecreatedtokens'] = 'You can only view tokens that you created.';
$string['operation'] = 'Operation';
$string['optional'] = 'Optional';
$string['passwordisexpired'] = 'Password is expired.';
@ -205,6 +205,9 @@ $string['token'] = 'Token';
$string['tokenauthlog'] = 'Token authentication';
$string['tokencreatedbyadmin'] = 'Can only be reset by administrator (*)';
$string['tokencreator'] = 'Creator';
$string['tokenfilter'] = 'Tokens filter';
$string['tokenfiltersubmit'] = 'Show only matching tokens';
$string['tokenfilterreset'] = 'Show all tokens';
$string['unknownoptionkey'] = 'Unknown option key ({$a})';
$string['unnamedstringparam'] = 'A string parameter is unnamed.';
$string['updateusersettings'] = 'Update';
@ -215,8 +218,6 @@ $string['userasclientsdescription'] = 'The following steps help you to set up th
$string['usermissingcaps'] = 'Missing capabilities: {$a}';
$string['usernameorid'] = 'Username / User id';
$string['usernameorid_help'] = 'Enter a username or a user id.';
$string['usernameoridnousererror'] = 'No users were found with this username/user id.';
$string['usernameoridoccurenceerror'] = 'More than one user was found with this username. Please enter the user id.';
$string['usernotallowed'] = 'The user is not allowed for this service. First you need to allow this user on the {$a}\'s allowed users administration page.';
$string['userservices'] = 'User services: {$a}';
$string['usersettingssaved'] = 'User settings saved';
@ -243,3 +244,7 @@ $string['wsusername'] = 'Web service username';
// Deprecated since Moodle 3.9.
$string['documentation'] = 'web service documentation';
// Deprecated since Moodle 3.11.
$string['usernameoridnousererror'] = 'No users were found with this username/user id.';
$string['usernameoridoccurenceerror'] = 'More than one user was found with this username. Please enter the user id.';

View File

@ -10395,91 +10395,6 @@ class admin_setting_managewebserviceprotocols extends admin_setting {
}
}
/**
* Special class for web service token administration.
*
* @author Jerome Mouneyrac
*/
class admin_setting_managewebservicetokens extends admin_setting {
/**
* Calls parent::__construct with specific arguments
*/
public function __construct() {
$this->nosave = true;
parent::__construct('webservicestokenui', get_string('managetokens', 'webservice'), '', '');
}
/**
* Always returns true, does nothing
*
* @return true
*/
public function get_setting() {
return true;
}
/**
* Always returns true, does nothing
*
* @return true
*/
public function get_defaultsetting() {
return true;
}
/**
* Always returns '', does not write anything
*
* @return string Always returns ''
*/
public function write_setting($data) {
// do not write any setting
return '';
}
/**
* Builds the XHTML to display the control
*
* @param string $data Unused
* @param string $query
* @return string
*/
public function output_html($data, $query='') {
global $CFG, $OUTPUT;
require_once($CFG->dirroot . '/webservice/classes/token_table.php');
$baseurl = new moodle_url('/' . $CFG->admin . '/settings.php?section=webservicetokens');
$return = $OUTPUT->box_start('generalbox webservicestokenui');
if (has_capability('moodle/webservice:managealltokens', context_system::instance())) {
$return .= \html_writer::div(get_string('onlyseecreatedtokens', 'webservice'));
}
$table = new \webservice\token_table('webservicetokens');
$table->define_baseurl($baseurl);
$table->attributes['class'] = 'admintable generaltable'; // Any need changing?
$table->data = array();
ob_start();
$table->out(10, false);
$tablehtml = ob_get_contents();
ob_end_clean();
$return .= $tablehtml;
$tokenpageurl = "$CFG->wwwroot/$CFG->admin/webservice/tokens.php?sesskey=" . sesskey();
$return .= $OUTPUT->box_end();
// add a token to the table
$return .= "<a href=\"".$tokenpageurl."&amp;action=create\">";
$return .= get_string('add')."</a>";
return highlight($query, $return);
}
}
/**
* Colour picker
*

View File

@ -1740,12 +1740,21 @@ $functions = array(
'methodname' => 'get_users_by_field',
'classpath' => 'user/externallib.php',
'description' => 'Retrieve users\' information for a specified unique field - If you want to do a user search, use '
. 'core_user_get_users()',
. 'core_user_get_users() or core_user_search_identity().',
'type' => 'read',
'capabilities' => 'moodle/user:viewdetails, moodle/user:viewhiddendetails, moodle/course:useremail, moodle/user:update',
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_user_search_identity' => array(
'classname' => '\core_user\external\search_identity',
'methodname' => 'execute',
'description' => 'Return list of users identities matching the given criteria in their name or other identity fields.',
'type' => 'read',
'capabilities' => 'moodle/user:viewalldetails',
'ajax' => true,
'loginrequired' => true,
),
'core_user_remove_user_device' => array(
'classname' => 'core_user_external',
'methodname' => 'remove_user_device',

View File

@ -0,0 +1,2 @@
define ("core_user/form_user_selector",["exports","core/ajax","core/templates","core/str"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.transport=g;a.processResults=function(a,b){if(!Array.isArray(b)){return b}else{return b.map(function(a){return{value:a.id,label:a.label}})}};b=function(a){return a&&a.__esModule?a:{default:a}}(b);function e(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}function g(){return h.apply(this,arguments)}function h(){h=f(regeneratorRuntime.mark(function a(e,f,g,h){var i,j,k,l;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:i={methodname:"core_user_search_identity",args:{query:f}};a.prev=1;a.next=4;return b.default.call([i])[0];case 4:j=a.sent;if(!j.overflow){a.next=12;break}a.next=8;return(0,d.get_string)("toomanyuserstoshow","core",">"+j.maxusersperpage);case 8:k=a.sent;g(k);a.next=19;break;case 12:l=[];j.list.forEach(function(a){l.push((0,c.render)("core_user/form_user_selector_suggestion",a))});a.next=16;return Promise.all(l);case 16:l=a.sent;j.list.forEach(function(a,b){a.label=l[b]});g(j.list);case 19:a.next=24;break;case 21:a.prev=21;a.t0=a["catch"](1);h(a.t0);case 24:case"end":return a.stop();}}},a,null,[[1,21]])}));return h.apply(this,arguments)}});
//# sourceMappingURL=form_user_selector.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["../src/form_user_selector.js"],"names":["selector","results","Array","isArray","map","result","value","id","label","transport","query","callback","failure","request","methodname","args","Ajax","call","response","overflow","maxusersperpage","msg","labels","list","forEach","user","push","Promise","all","index"],"mappings":"0MA8EO,SAAwBA,CAAxB,CAAkCC,CAAlC,CAA2C,CAE9C,GAAI,CAACC,KAAK,CAACC,OAAN,CAAcF,CAAd,CAAL,CAA6B,CACzB,MAAOA,CAAAA,CAEV,CAHD,IAGO,CACH,MAAOA,CAAAA,CAAO,CAACG,GAAR,CAAY,SAAAC,CAAM,QAAK,CAACC,KAAK,CAAED,CAAM,CAACE,EAAf,CAAmBC,KAAK,CAAEH,CAAM,CAACG,KAAjC,CAAL,CAAlB,CACV,CACJ,C,CA9DD,uD,uUAYsBC,CAAAA,C,2EAAf,WAAyBT,CAAzB,CAAmCU,CAAnC,CAA0CC,CAA1C,CAAoDC,CAApD,+FAEGC,CAFH,CAEa,CACZC,UAAU,CAAE,2BADA,CAEZC,IAAI,CAAE,CACFL,KAAK,CAAEA,CADL,CAFM,CAFb,yBAUwBM,WAAKC,IAAL,CAAU,CAACJ,CAAD,CAAV,EAAqB,CAArB,CAVxB,QAUOK,CAVP,YAYKA,CAAQ,CAACC,QAZd,iCAauB,iBAAU,oBAAV,CAAgC,MAAhC,CAAwC,IAAMD,CAAQ,CAACE,eAAvD,CAbvB,QAaWC,CAbX,QAcKV,CAAQ,CAACU,CAAD,CAAR,CAdL,wBAiBSC,CAjBT,CAiBkB,EAjBlB,CAkBKJ,CAAQ,CAACK,IAAT,CAAcC,OAAd,CAAsB,SAAAC,CAAI,CAAI,CAC1BH,CAAM,CAACI,IAAP,CAAY,aAAe,yCAAf,CAA0DD,CAA1D,CAAZ,CACH,CAFD,EAlBL,gBAqBoBE,CAAAA,OAAO,CAACC,GAAR,CAAYN,CAAZ,CArBpB,SAqBKA,CArBL,QAuBKJ,CAAQ,CAACK,IAAT,CAAcC,OAAd,CAAsB,SAACC,CAAD,CAAOI,CAAP,CAAiB,CACnCJ,CAAI,CAACjB,KAAL,CAAac,CAAM,CAACO,CAAD,CACtB,CAFD,EAIAlB,CAAQ,CAACO,CAAQ,CAACK,IAAV,CAAR,CA3BL,6DA+BCX,CAAO,MAAP,CA/BD,uD","sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Provides the required functionality for an autocomplete element to select a user.\n *\n * @module core_user/form_user_selector\n * @package core_webservice\n * @copyright 2020 David Mudrák <david@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport {render as renderTemplate} from 'core/templates';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Load the list of users matching the query and render the selector labels for them.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n * @param {Function} failure A function to call in case of failure, receiving the error message.\n */\nexport async function transport(selector, query, callback, failure) {\n\n const request = {\n methodname: 'core_user_search_identity',\n args: {\n query: query\n }\n };\n\n try {\n const response = await Ajax.call([request])[0];\n\n if (response.overflow) {\n const msg = await getString('toomanyuserstoshow', 'core', '>' + response.maxusersperpage);\n callback(msg);\n\n } else {\n let labels = [];\n response.list.forEach(user => {\n labels.push(renderTemplate('core_user/form_user_selector_suggestion', user));\n });\n labels = await Promise.all(labels);\n\n response.list.forEach((user, index) => {\n user.label = labels[index];\n });\n\n callback(response.list);\n }\n\n } catch (e) {\n failure(e);\n }\n}\n\n/**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results returned by {@see transport()}.\n * @return {Array} New array of the selector options.\n */\nexport function processResults(selector, results) {\n\n if (!Array.isArray(results)) {\n return results;\n\n } else {\n return results.map(result => ({value: result.id, label: result.label}));\n }\n}\n"],"file":"form_user_selector.min.js"}

View File

@ -0,0 +1,87 @@
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the required functionality for an autocomplete element to select a user.
*
* @module core_user/form_user_selector
* @package core_webservice
* @copyright 2020 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import {render as renderTemplate} from 'core/templates';
import {get_string as getString} from 'core/str';
/**
* Load the list of users matching the query and render the selector labels for them.
*
* @param {String} selector The selector of the auto complete element.
* @param {String} query The query string.
* @param {Function} callback A callback function receiving an array of results.
* @param {Function} failure A function to call in case of failure, receiving the error message.
*/
export async function transport(selector, query, callback, failure) {
const request = {
methodname: 'core_user_search_identity',
args: {
query: query
}
};
try {
const response = await Ajax.call([request])[0];
if (response.overflow) {
const msg = await getString('toomanyuserstoshow', 'core', '>' + response.maxusersperpage);
callback(msg);
} else {
let labels = [];
response.list.forEach(user => {
labels.push(renderTemplate('core_user/form_user_selector_suggestion', user));
});
labels = await Promise.all(labels);
response.list.forEach((user, index) => {
user.label = labels[index];
});
callback(response.list);
}
} catch (e) {
failure(e);
}
}
/**
* Process the results for auto complete elements.
*
* @param {String} selector The selector of the auto complete element.
* @param {Array} results An array or results returned by {@see transport()}.
* @return {Array} New array of the selector options.
*/
export function processResults(selector, results) {
if (!Array.isArray(results)) {
return results;
} else {
return results.map(result => ({value: result.id, label: result.label}));
}
}

View File

@ -0,0 +1,131 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
namespace core_user\external;
/**
* Provides the core_user_search_identity external function.
*
* @package core_user
* @category external
* @copyright 2021 David Mudrák <david@moodle.com>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class search_identity extends \external_api {
/**
* Describes the external function parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): \external_function_parameters {
return new \external_function_parameters([
'query' => new \external_value(PARAM_TEXT, 'The search query', VALUE_REQUIRED),
]);
}
/**
* Finds users with the identity matching the given query.
*
* @param string $query The search request.
* @return array
*/
public static function execute(string $query): array {
global $DB, $CFG;
$params = \external_api::validate_parameters(self::execute_parameters(), [
'query' => $query,
]);
$query = $params['query'];
// Validate context.
$context = \context_system::instance();
self::validate_context($context);
require_capability('moodle/user:viewalldetails', $context);
$hasviewfullnames = has_capability('moodle/site:viewfullnames', $context);
$fields = \core\user_fields::for_name()->with_identity($context, false);
$extrafields = $fields->get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);
list($searchsql, $searchparams) = users_search_sql($query, '', true, $extrafields);
list($sortsql, $sortparams) = users_order_by_sql('', $query, $context);
$params = array_merge($searchparams, $sortparams);
$rs = $DB->get_recordset_select('user', $searchsql, $params, $sortsql,
'id' . $fields->get_sql()->selects, 0, $CFG->maxusersperpage + 1);
$count = 0;
$list = [];
foreach ($rs as $record) {
$user = (object)[
'id' => $record->id,
'fullname' => fullname($record, $hasviewfullnames),
'extrafields' => [],
];
foreach ($extrafields as $extrafield) {
// Sanitize the extra fields to prevent potential XSS exploit.
$user->extrafields[] = (object)[
'name' => $extrafield,
'value' => s($record->$extrafield)
];
}
$count++;
if ($count <= $CFG->maxusersperpage) {
$list[$record->id] = $user;
}
}
$rs->close();
return [
'list' => $list,
'maxusersperpage' => $CFG->maxusersperpage,
'overflow' => ($count > $CFG->maxusersperpage),
];
}
/**
* Describes the external function result value.
*
* @return external_description
*/
public static function execute_returns(): \external_description {
return new \external_single_structure([
'list' => new \external_multiple_structure(
new \external_single_structure([
'id' => new \external_value(\core_user::get_property_type('id'), 'ID of the user'),
// The output of the {@see fullname()} can contain formatting HTML such as <ruby> tags.
// So we need PARAM_RAW here and the caller is supposed to render it appropriately.
'fullname' => new \external_value(PARAM_RAW, 'The fullname of the user'),
'extrafields' => new \external_multiple_structure(
new \external_single_structure([
'name' => new \external_value(PARAM_TEXT, 'Name of the extrafield.'),
'value' => new \external_value(PARAM_TEXT, 'Value of the extrafield.'),
]), 'List of extra fields', VALUE_OPTIONAL)
])
),
'maxusersperpage' => new \external_value(PARAM_INT, 'Configured maximum users per page.'),
'overflow' => new \external_value(PARAM_BOOL, 'Were there more records than maxusersperpage found?'),
]);
}
}

View File

@ -0,0 +1,54 @@
{{!
This file is part of Moodle - https://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/form_user_selector_suggestion
Moodle template for the list of valid options in an user selector autocomplate form element.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* fullname - string Users full name
* extrafields - list
Example context (json):
{
"fullname": "Admin User",
"extrafields": [
{
"name": "email",
"value": "admin@example.com"
},
{
"name": "phone1",
"value": "0123456789"
}
]
}
}}
<span>
<span data-field="fullname">{{fullname}}</span>
<small>
{{#extrafields}}
<span data-field="{{name}}">{{{value}}}</span>
{{/extrafields}}
</small>
</span>

View File

@ -679,7 +679,7 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
}
/**
* Data provider for {@link self::test_create_users_invalid_parameter()}.
* Data provider for {@see self::test_create_users_invalid_parameter()}.
*
* @return array
*/
@ -1538,4 +1538,149 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
// Try to retrieve other user private files info.
core_user_external::get_private_files_info($user2->id);
}
/**
* Test the functionality of the {@see \core_user\external\search_identity} class.
*/
public function test_external_search_identity() {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
$user1 = self::getDataGenerator()->create_user([
'firstname' => 'Firstone',
'lastname' => 'Lastone',
'username' => 'usernameone',
'idnumber' => 'idnumberone',
'email' => 'userone@example.com',
'phone1' => 'tel1',
'phone2' => 'tel2',
'department' => 'Department Foo',
'institution' => 'Institution Foo',
'city' => 'City One',
'country' => 'AU',
]);
$user2 = self::getDataGenerator()->create_user([
'firstname' => 'Firsttwo',
'lastname' => 'Lasttwo',
'username' => 'usernametwo',
'idnumber' => 'idnumbertwo',
'email' => 'usertwo@example.com',
'phone1' => 'tel1',
'phone2' => 'tel2',
'department' => 'Department Foo',
'institution' => 'Institution Foo',
'city' => 'City One',
'country' => 'AU',
]);
$user3 = self::getDataGenerator()->create_user([
'firstname' => 'Firstthree',
'lastname' => 'Lastthree',
'username' => 'usernamethree',
'idnumber' => 'idnumberthree',
'email' => 'userthree@example.com',
'phone1' => 'tel1',
'phone2' => 'tel2',
'department' => 'Department Foo',
'institution' => 'Institution Foo',
'city' => 'City One',
'country' => 'AU',
]);
$CFG->showuseridentity = 'email,idnumber,city';
$CFG->maxusersperpage = 3;
$result = \core_user\external\search_identity::execute('Lastt');
$result = external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result);
$this->assertEquals(2, count($result['list']));
$this->assertEquals(3, $result['maxusersperpage']);
$this->assertEquals(false, $result['overflow']);
foreach ($result['list'] as $user) {
$this->assertEquals(3, count($user['extrafields']));
$this->assertEquals('email', $user['extrafields'][0]['name']);
$this->assertEquals('idnumber', $user['extrafields'][1]['name']);
$this->assertEquals('city', $user['extrafields'][2]['name']);
}
$CFG->showuseridentity = 'username';
$CFG->maxusersperpage = 2;
$result = \core_user\external\search_identity::execute('Firstt');
$result = external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result);
$this->assertEquals(2, count($result['list']));
$this->assertEquals(2, $result['maxusersperpage']);
$this->assertEquals(false, $result['overflow']);
foreach ($result['list'] as $user) {
$this->assertEquals(1, count($user['extrafields']));
$this->assertEquals('username', $user['extrafields'][0]['name']);
}
$CFG->showuseridentity = 'email';
$CFG->maxusersperpage = 2;
$result = \core_user\external\search_identity::execute('City One');
$result = external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result);
$this->assertEquals(0, count($result['list']));
$this->assertEquals(2, $result['maxusersperpage']);
$this->assertEquals(false, $result['overflow']);
$CFG->showuseridentity = 'city';
$CFG->maxusersperpage = 2;
foreach ($result['list'] as $user) {
$this->assertEquals(1, count($user['extrafields']));
$this->assertEquals('username', $user['extrafields'][0]['name']);
}
$result = \core_user\external\search_identity::execute('City One');
$result = external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result);
$this->assertEquals(2, count($result['list']));
$this->assertEquals(2, $result['maxusersperpage']);
$this->assertEquals(true, $result['overflow']);
}
/**
* Test functionality of the {@see \core_user\external\search_identity} class with alternativefullnameformat defined.
*/
public function test_external_search_identity_with_alternativefullnameformat() {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
$user1 = self::getDataGenerator()->create_user([
'lastname' => '小柳',
'lastnamephonetic' => 'Koyanagi',
'firstname' => '秋',
'firstnamephonetic' => 'Aki',
'email' => 'koyanagiaki@example.com',
'country' => 'JP',
]);
$CFG->showuseridentity = 'email';
$CFG->maxusersperpage = 3;
$CFG->alternativefullnameformat =
'<ruby>lastname firstname <rp>(</rp><rt>lastnamephonetic firstnamephonetic</rt><rp>)</rp></ruby>';
$result = \core_user\external\search_identity::execute('Ak');
$result = external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result);
$this->assertEquals(1, count($result['list']));
$this->assertEquals(3, $result['maxusersperpage']);
$this->assertEquals(false, $result['overflow']);
foreach ($result['list'] as $user) {
$this->assertEquals(1, count($user['extrafields']));
$this->assertEquals('email', $user['extrafields'][0]['name']);
}
}
}

View File

@ -1,5 +1,13 @@
This files describes API changes for code that uses the user API.
=== 3.11 ===
* Added new core_user/form_user_selector JS module that can be used as the 'ajax' handler for the autocomplete form
element implementing the user selector.
* Added new external function core_user_external::search_identity(). The main purpose of this external function is to
provide data for asynchronous user selectors and similar widgets. It allows to search users matching the given query
in their name or other available identity fields.
=== 3.9 ===
* The unified filter has been replaced by the participants filter. The following have therefore been deprecated:

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2021052500.68; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2021052500.69; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev (Build: 20210312)'; // Human-friendly version name

View File

@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the {@see core_webservice\token_filter} class.
*
* @package core_webservice
* @copyright 2020 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_webservice;
use moodleform;
/**
* Form allowing to filter displayed tokens.
*
* @copyright 2020 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class token_filter extends moodleform {
/**
* Defines the form fields.
*/
public function definition() {
global $DB;
$mform = $this->_form;
$presetdata = $this->_customdata;
$mform->addElement('header', 'tokenfilter', get_string('tokenfilter', 'webservice'));
if (empty($presetdata->token) && empty($presetdata->users) && empty($presetdata->services)) {
$mform->setExpanded('tokenfilter', false);
} else {
$mform->setExpanded('tokenfilter', true);
}
// Token.
$mform->addElement('text', 'token', get_string('token', 'core_webservice'), ['size' => 32]);
$mform->setType('token', PARAM_ALPHANUM);
// User selector.
$attributes = [
'multiple' => true,
'ajax' => 'core_user/form_user_selector',
'valuehtmlcallback' => function($userid) {
global $DB, $OUTPUT;
$context = \context_system::instance();
$fields = \core\user_fields::for_name()->with_identity($context, false);
$record = \core_user::get_user($userid, 'id' . $fields->get_sql()->selects, MUST_EXIST);
$user = (object)[
'id' => $record->id,
'fullname' => fullname($record, has_capability('moodle/site:viewfullnames', $context)),
'extrafields' => [],
];
foreach ($fields->get_required_fields([\core\user_fields::PURPOSE_IDENTITY]) as $extrafield) {
$user->extrafields[] = (object)[
'name' => $extrafield,
'value' => s($record->$extrafield)
];
}
return $OUTPUT->render_from_template('core_user/form_user_selector_suggestion', $user);
},
];
$mform->addElement('autocomplete', 'users', get_string('user'), [], $attributes);
// Service selector.
$options = $DB->get_records_menu('external_services', null, '', 'id, name');
$attributes = [
'multiple' => true,
];
$mform->addElement('autocomplete', 'services', get_string('service', 'webservice'), $options, $attributes);
// Action buttons.
$mform->addGroup([
$mform->createElement('submit', 'submitbutton', get_string('tokenfiltersubmit', 'core_webservice')),
$mform->createElement('submit', 'resetbutton', get_string('tokenfilterreset', 'core_webservice'), [], false),
], 'actionbuttons', '', ' ', false);
}
}

View File

@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the {@see \core_webservice\token_form} class.
*
* @package core_webservice
* @category admin
* @copyright 2020 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_webservice;
/**
* Form to create and edit a web service token.
*
* Tokens allow users call external functions provided by selected web services. They can optionally have IP restriction
* and date validity defined.
*
* @copyright 2010 Jerome Mouneyrac <jerome@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class token_form extends \moodleform {
/**
* Defines the form fields.
*/
public function definition() {
global $DB;
$mform = $this->_form;
$data = $this->_customdata;
$mform->addElement('header', 'token', get_string('token', 'webservice'));
// User selector.
$attributes = [
'multiple' => false,
'ajax' => 'core_user/form_user_selector',
'valuehtmlcallback' => function($userid) {
global $DB, $OUTPUT;
$context = \context_system::instance();
$fields = \core\user_fields::for_name()->with_identity($context, false);
$record = $DB->get_record('user', ['id' => $userid], $fields, MUST_EXIST);
$user = (object)[
'id' => $record->id,
'fullname' => fullname($record, has_capability('moodle/site:viewfullnames', $context)),
'extrafields' => [],
];
foreach ($fields->get_required_fields([\core\user_fields::PURPOSE_IDENTITY]) as $extrafield) {
$user->extrafields[] = (object)[
'name' => $extrafield,
'value' => s($record->$extrafield)
];
}
return $OUTPUT->render_from_template('core_user/form_user_selector_suggestion', $user);
},
];
$mform->addElement('autocomplete', 'user', get_string('user'), [], $attributes);
$mform->addRule('user', get_string('required'), 'required', null, 'client');
// Service selector.
$options = $DB->get_records_menu('external_services', null, '', 'id, name');
$mform->addElement('select', 'service', get_string('service', 'webservice'), $options);
$mform->addRule('service', get_string('required'), 'required', null, 'client');
$mform->setType('service', PARAM_INT);
$mform->addElement('text', 'iprestriction', get_string('iprestriction', 'webservice'));
$mform->setType('iprestriction', PARAM_RAW_TRIMMED);
$mform->addElement('date_selector', 'validuntil',
get_string('validuntil', 'webservice'), array('optional' => true));
$mform->setType('validuntil', PARAM_INT);
$mform->addElement('hidden', 'action');
$mform->setType('action', PARAM_ALPHANUMEXT);
$this->add_action_buttons(true);
$this->set_data($data);
}
/**
* Validate the submitted data.
*
* @param array $data Submitted data.
* @param array $files Submitted files.
* @return array Validation errors.
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if ($DB->get_field('user', 'suspended', ['id' => $data['user']], MUST_EXIST)) {
$errors['user'] = get_string('suspended', 'core') . ' - ' . get_string('forbiddenwsuser', 'core_webservice');
}
return $errors;
}
}

View File

@ -22,7 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace webservice;
namespace core_webservice;
defined('MOODLE_INTERNAL') || die;
@ -44,11 +44,22 @@ class token_table extends \table_sql {
*/
protected $showalltokens;
/** @var bool $hasviewfullnames Does the user have the viewfullnames capability. */
protected $hasviewfullnames;
/** @var array */
protected $userextrafields;
/** @var object */
protected $filterdata;
/**
* Sets up the table.
*
* @param int $id The id of the table
* @param object $filterdata The data submitted by the {@see token_filter}.
*/
public function __construct($id) {
public function __construct($id, $filterdata = null) {
parent::__construct($id);
// Get the context.
@ -56,6 +67,13 @@ class token_table extends \table_sql {
// Can we see tokens created by all users?
$this->showalltokens = has_capability('moodle/webservice:managealltokens', $context);
$this->hasviewfullnames = has_capability('moodle/site:viewfullnames', $context);
// List of user identity fields.
$this->userextrafields = \core\user_fields::get_identity_fields(\context_system::instance(), false);
// Filter form values.
$this->filterdata = $filterdata;
// Define the headers and columns.
$headers = [];
@ -66,7 +84,7 @@ class token_table extends \table_sql {
$headers[] = get_string('user');
$columns[] = 'fullname';
$headers[] = get_string('service', 'webservice');
$columns[] = 'name';
$columns[] = 'servicename';
$headers[] = get_string('iprestriction', 'webservice');
$columns[] = 'iprestriction';
$headers[] = get_string('validuntil', 'webservice');
@ -130,20 +148,33 @@ class token_table extends \table_sql {
public function col_fullname($data) {
global $OUTPUT;
$identity = [];
foreach ($this->userextrafields as $userextrafield) {
$identity[] = $data->$userextrafield;
}
$userprofilurl = new \moodle_url('/user/profile.php', ['id' => $data->userid]);
$content = \html_writer::link($userprofilurl, fullname($data));
$content = \html_writer::link($userprofilurl, fullname($data, $this->hasviewfullnames));
if ($identity) {
$content .= \html_writer::div('<small>' . implode(', ', $identity) . '</small>', 'useridentity text-muted');
}
// Make up list of capabilities that the user is missing for the given webservice.
$webservicemanager = new \webservice();
$usermissingcaps = $webservicemanager->get_missing_capabilities_by_users([['id' => $data->userid]], $data->serviceid);
if (!is_siteadmin($data->userid) && array_key_exists($data->userid, $usermissingcaps)) {
$missingcapabilities = implode(', ', $usermissingcaps[$data->userid]);
if (!empty($missingcapabilities)) {
$capabilitiesstring = get_string('usermissingcaps', 'webservice', $missingcapabilities) . '&nbsp;' .
$OUTPUT->help_icon('missingcaps', 'webservice');
$content .= \html_writer::div($capabilitiesstring, 'missingcaps');
}
if ($data->serviceshortname <> MOODLE_OFFICIAL_MOBILE_SERVICE && !is_siteadmin($data->userid)
&& array_key_exists($data->userid, $usermissingcaps)) {
$count = \html_writer::span(count($usermissingcaps[$data->userid]), 'badge badge-danger');
$links = array_map(function($capname) {
return get_capability_docs_link((object)['name' => $capname]) . \html_writer::div($capname, 'text-muted');
}, $usermissingcaps[$data->userid]);
$list = \html_writer::alist($links);
$help = $OUTPUT->help_icon('missingcaps', 'webservice');
$content .= print_collapsible_region(\html_writer::div($list . $help, 'missingcaps'), 'small mt-2',
\html_writer::random_id('usermissingcaps'), get_string('usermissingcaps', 'webservice', $count), '', true, true);
}
return $content;
@ -159,7 +190,7 @@ class token_table extends \table_sql {
global $USER;
// Hide the token if it wasn't created by the current user.
if ($data->creatorid != $USER->id) {
return '-';
return \html_writer::tag('small', get_string('onlyseecreatedtokens', 'core_webservice'), ['class' => 'text-muted']);
}
return $data->token;
@ -183,7 +214,17 @@ class token_table extends \table_sql {
}
$creatorprofileurl = new \moodle_url('/user/profile.php', ['id' => $data->creatorid]);
return \html_writer::link($creatorprofileurl, fullname((object)$user));
return \html_writer::link($creatorprofileurl, fullname((object)$user, $this->hasviewfullnames));
}
/**
* Format the service name column.
*
* @param \stdClass $data
* @return string
*/
public function col_servicename($data) {
return \html_writer::div(s($data->servicename)) . \html_writer::div(s($data->serviceshortname), 'small text-muted');
}
/**
@ -221,41 +262,59 @@ class token_table extends \table_sql {
$usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$creatorfields = $userfieldsapi->get_sql('c', false, 'creator', '', false)->selects;
$params = ["tokenmode" => EXTERNAL_TOKEN_PERMANENT];
if (!empty($this->userextrafields)) {
$usernamefields .= ',u.' . implode(',u.', $this->userextrafields);
}
// TODO: in order to let the administrator delete obsolete token, split the request in multiple request or use LEFT JOIN.
$params = ['tokenmode' => EXTERNAL_TOKEN_PERMANENT];
if ($this->showalltokens) {
// Show all tokens.
$sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid,
t.creatorid, $creatorfields
FROM {external_tokens} t, {user} u, {external_services} s, {user} c
WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid";
$countsql = "SELECT COUNT(t.id)
FROM {external_tokens} t, {user} u, {external_services} s, {user} c
WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid";
} else {
$selectfields = "SELECT t.id, t.token, t.iprestriction, t.validuntil, t.creatorid,
u.id AS userid, $usernamefields,
s.id AS serviceid, s.name AS servicename, s.shortname AS serviceshortname,
$creatorfields ";
$selectcount = "SELECT COUNT(t.id) ";
$sql = " FROM {external_tokens} t
JOIN {user} u ON u.id = t.userid
JOIN {external_services} s ON s.id = t.externalserviceid
JOIN {user} c ON c.id = t.creatorid
WHERE t.tokentype = :tokenmode";
if (!$this->showalltokens) {
// Only show tokens created by the current user.
$sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid,
t.creatorid, $creatorfields
FROM {external_tokens} t, {user} u, {external_services} s, {user} c
WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND
c.id = t.creatorid";
$countsql = "SELECT COUNT(t.id)
FROM {external_tokens} t, {user} u, {external_services} s, {user} c
WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND
t.userid = u.id AND c.id = t.creatorid";
$params["userid"] = $USER->id;
$sql .= " AND t.creatorid = :userid";
$params['userid'] = $USER->id;
}
if ($this->filterdata->token !== '') {
$sql .= " AND " . $DB->sql_like("t.token", ":token");
$params['token'] = "%" . $DB->sql_like_escape($this->filterdata->token) . "%";
}
if (!empty($this->filterdata->users)) {
list($sqlusers, $paramsusers) = $DB->get_in_or_equal($this->filterdata->users, SQL_PARAMS_NAMED, 'user');
$sql .= " AND t.userid {$sqlusers}";
$params += $paramsusers;
}
if (!empty($this->filterdata->services)) {
list($sqlservices, $paramsservices) = $DB->get_in_or_equal($this->filterdata->services, SQL_PARAMS_NAMED, 'service');
$sql .= " AND s.id {$sqlservices}";
$params += $paramsservices;
}
$sort = $this->get_sql_sort();
$sortsql = '';
if ($sort) {
$sql = $sql . ' ORDER BY ' . $sort;
$sortsql = " ORDER BY {$sort}";
}
$total = $DB->count_records_sql($countsql, $params);
$total = $DB->count_records_sql($selectcount . $sql, $params);
$this->pagesize($pagesize, $total);
$this->rawdata = $DB->get_recordset_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
$this->rawdata = $DB->get_recordset_sql($selectfields . $sql . $sortsql, $params, $this->get_page_start(),
$this->get_page_size());
}
}

View File

@ -285,9 +285,16 @@ class webservice {
*/
public function get_ws_authorised_users($serviceid) {
global $DB, $CFG;
$params = array($CFG->siteguest, $serviceid);
$sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
u.lastname as lastname,
$namefields = get_all_user_name_fields(true, 'u');
foreach (get_extra_user_fields(context_system::instance()) as $extrafield) {
$namefields .= ',u.' . $extrafield;
}
$sql = " SELECT u.id as id, esu.id as serviceuserid, {$namefields},
esu.iprestriction as iprestriction, esu.validuntil as validuntil,
esu.timecreated as timecreated
FROM {user} u, {external_services_users} esu
@ -296,6 +303,7 @@ class webservice {
AND esu.externalserviceid = ?";
$users = $DB->get_records_sql($sql, $params);
return $users;
}
@ -623,11 +631,16 @@ class webservice {
* as the front end does not display it itself. In pratice,
* admins would like the info, for more info you can follow: MDL-29962
*
* @deprecated since Moodle 3.11 in MDL-67748 without a replacement.
* @todo MDL-70187 Please delete this method completely in Moodle 4.3, thank you.
* @param int $userid user id
* @return array
*/
public function get_user_capabilities($userid) {
global $DB;
debugging('webservice::get_user_capabilities() has been deprecated.', DEBUG_DEVELOPER);
//retrieve the user capabilities
$sql = "SELECT DISTINCT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra
WHERE rc.roleid=ra.roleid AND ra.userid= ? AND rc.permission = ?";
@ -640,45 +653,97 @@ class webservice {
}
/**
* Get missing user capabilities for a given service
* WARNING: do not use this "broken" function. It was created in the goal to display some capabilities
* required by users. In theory we should not need to display this kind of information
* as the front end does not display it itself. In pratice,
* admins would like the info, for more info you can follow: MDL-29962
* Get missing user capabilities for the given service's functions.
*
* @param array $users users
* @param int $serviceid service id
* @return array of missing capabilities, keys being the user ids
* Every external function can declare some required capabilities to allow for easier setup of the web services.
* However, that is supposed to be used for informational admin report only. There is no automatic evaluation of
* the declared capabilities and the context of the capability evaluation is ignored. Also, actual capability
* evaluation is much more complex as it allows for overrides etc.
*
* Returned are capabilities that the given users do not seem to have assigned anywhere at the site and that should
* be checked by the admin.
*
* Do not use this method for anything else, particularly not for any security related checks. See MDL-29962 for the
* background of why we have this - there are arguments for dropping this feature completely.
*
* @param array $users List of users to check, consisting of objects, arrays or integer ids.
* @param int $serviceid The id of the external service to check.
* @return array List of missing capabilities: (int)userid => array of (string)capabilitynames
*/
public function get_missing_capabilities_by_users($users, $serviceid) {
public function get_missing_capabilities_by_users(array $users, int $serviceid): array {
global $DB;
$usersmissingcaps = array();
//retrieve capabilities required by the service
$servicecaps = $this->get_service_required_capabilities($serviceid);
// The following are default capabilities for all authenticated users and we will assume them granted.
$commoncaps = get_default_capabilities('user');
//retrieve users missing capabilities
foreach ($users as $user) {
//cast user array into object to be a bit more flexible
if (is_array($user)) {
$user = (object) $user;
}
$usercaps = $this->get_user_capabilities($user->id);
//detect the missing capabilities
foreach ($servicecaps as $functioname => $functioncaps) {
foreach ($functioncaps as $functioncap) {
if (!array_key_exists($functioncap, $usercaps)) {
if (!isset($usersmissingcaps[$user->id])
or array_search($functioncap, $usersmissingcaps[$user->id]) === false) {
$usersmissingcaps[$user->id][] = $functioncap;
}
}
// Get the list of additional capabilities required by the service.
$servicecaps = [];
foreach ($this->get_service_required_capabilities($serviceid) as $service => $caps) {
foreach ($caps as $cap) {
if (empty($commoncaps[$cap])) {
$servicecaps[$cap] = true;
}
}
}
return $usersmissingcaps;
if (empty($servicecaps)) {
return [];
}
// Prepare a list of user ids we want to check.
$userids = [];
foreach ($users as $user) {
if (is_object($user) && isset($user->id)) {
$userids[$user->id] = true;
} else if (is_array($user) && isset($user['id'])) {
$userids[$user['id']] = true;
} else {
throw new coding_exception('Unexpected format of users list in webservice::get_missing_capabilities_by_users().');
}
}
// Prepare a matrix of missing capabilities x users - consider them all missing by default.
foreach (array_keys($userids) as $userid) {
foreach (array_keys($servicecaps) as $capname) {
$matrix[$userid][$capname] = true;
}
}
list($capsql, $capparams) = $DB->get_in_or_equal(array_keys($servicecaps), SQL_PARAMS_NAMED, 'paramcap');
list($usersql, $userparams) = $DB->get_in_or_equal(array_keys($userids), SQL_PARAMS_NAMED, 'paramuser');
$sql = "SELECT c.name AS capability, u.id AS userid
FROM {capabilities} c
JOIN {role_capabilities} rc ON c.name = rc.capability
JOIN {role_assignments} ra ON ra.roleid = rc.roleid
JOIN {user} u ON ra.userid = u.id
WHERE rc.permission = :capallow
AND c.name {$capsql}
AND u.id {$usersql}";
$params = $capparams + $userparams + [
'capallow' => CAP_ALLOW,
];
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $record) {
// If there was a potential role assignment found that might grant the user the given capability,
// remove it from the matrix. Again, we ignore all the contexts, prohibits, prevents and other details
// of the permissions evaluations. See the function docblock for details.
unset($matrix[$record->userid][$record->capability]);
}
$rs->close();
foreach ($matrix as $userid => $caps) {
$matrix[$userid] = array_keys($caps);
if (empty($matrix[$userid])) {
unset($matrix[$userid]);
}
}
return $matrix;
}
/**
@ -1416,7 +1481,7 @@ abstract class webservice_base_server extends webservice_server {
7. The function is called with username/password (no user token is sent)
and none of the services has the function to allow the user.
These settings can be found in Administration > Site administration
> Plugins > Web services > External services and Manage tokens.');
> Server > Web services > External services and Manage tokens.');
}
// we have all we need now

View File

@ -96,7 +96,7 @@ class core_webservice_renderer extends plugin_renderer_base {
}
/**
* Display list of authorised users
* Display list of authorised users for the given external service.
*
* @param array $users authorised users
* @param int $serviceid service id
@ -104,25 +104,43 @@ class core_webservice_renderer extends plugin_renderer_base {
*/
public function admin_authorised_user_list($users, $serviceid) {
global $CFG;
$html = $this->output->box_start('generalbox', 'alloweduserlist');
$listitems = [];
$extrafields = get_extra_user_fields(context_system::instance());
foreach ($users as $user) {
$modifiedauthoriseduserurl = new moodle_url('/' . $CFG->admin . '/webservice/service_user_settings.php',
array('userid' => $user->id, 'serviceid' => $serviceid));
$html .= html_writer::tag('a', $user->firstname . " "
. $user->lastname . ", " . s($user->email),
array('href' => $modifiedauthoriseduserurl));
//add missing capabilities
if (!empty($user->missingcapabilities)) {
$html .= html_writer::tag('div',
get_string('usermissingcaps', 'webservice', $user->missingcapabilities)
. '&nbsp;' . $this->output->help_icon('missingcaps', 'webservice'),
array('class' => 'missingcaps', 'id' => 'usermissingcaps'));
$html .= html_writer::empty_tag('br');
} else {
$html .= html_writer::empty_tag('br') . html_writer::empty_tag('br');
$settingsurl = new moodle_url('/admin/webservice/service_user_settings.php',
['userid' => $user->id, 'serviceid' => $serviceid]);
$identity = [];
foreach ($extrafields as $extrafield) {
if (isset($user->{$extrafield})) {
$identity[] = s($user->{$extrafield});
}
}
$identity = $identity ? html_writer::div(implode(', ', $identity), 'small') : '';
$link = html_writer::link($settingsurl, fullname($user));
if (!empty($user->missingcapabilities)) {
$count = html_writer::span(count($user->missingcapabilities), 'badge badge-danger');
$links = array_map(function($capname) {
return get_capability_docs_link((object)['name' => $capname]) . html_writer::div($capname, 'text-muted');
}, $user->missingcapabilities);
$list = html_writer::alist($links);
$help = $this->output->help_icon('missingcaps', 'webservice');
$missingcaps = print_collapsible_region(html_writer::div($list . $help, 'missingcaps'), 'small',
html_writer::random_id('usermissingcaps'), get_string('usermissingcaps', 'webservice', $count), '', true, true);
} else {
$missingcaps = '';
}
$listitems[] = $link . $identity . $missingcaps;
}
$html .= $this->output->box_end();
$html = html_writer::div(html_writer::alist($listitems), 'alloweduserlist');
return $html;
}
@ -166,29 +184,6 @@ class core_webservice_renderer extends plugin_renderer_base {
$formcontinue, $formcancel);
}
/**
* Display a confirmation page to delete a token
*
* @param stdClass $token Required properties: id (token id), firstname (user firstname), lastname (user lastname), name (service name)
* @return string html
*/
public function admin_delete_token_confirmation($token) {
global $CFG;
$optionsyes = array('tokenid' => $token->id, 'action' => 'delete',
'confirm' => 1, 'sesskey' => sesskey());
$optionsno = array('section' => 'webservicetokens', 'sesskey' => sesskey());
$formcontinue = new single_button(
new moodle_url('/' . $CFG->admin . '/webservice/tokens.php', $optionsyes),
get_string('delete'));
$formcancel = new single_button(
new moodle_url('/' . $CFG->admin . '/settings.php', $optionsno),
get_string('cancel'), 'get');
return $this->output->confirm(get_string('deletetokenconfirm', 'webservice',
(object) array('user' => $token->firstname . " "
. $token->lastname, 'service' => $token->name)),
$formcontinue, $formcancel);
}
/**
* Display a list of functions for a given service
* If the service is built-in, do not display remove/add operation (read-only)

View File

@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Behat data generator for core_webservice.
*
* @package core_webservice
* @category test
* @copyright 2021 Andrew Nicols <andrew@nicols.co.uk>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_webservice_generator extends behat_generator_base {
/**
* Get the list of creatable entities for a web service.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'Services' => [
'singular' => 'Service',
'datagenerator' => 'service',
'required' => ['name'],
],
'Service functions' => [
'singular' => 'Service function',
'datagenerator' => 'service_functions',
'required' => ['service', 'functions'],
],
'Tokens' => [
'singular' => 'Token',
'datagenerator' => 'token',
'required' => ['user'],
'switchids' => [
'user' => 'userid',
],
],
];
}
}

View File

@ -0,0 +1,139 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../lib.php');
/**
* Data generator for core_webservice plugin.
*
* @package core_webservice
* @category test
* @copyright 2021 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_webservice_generator extends component_generator_base {
/**
* Create a new webservice service.
*
* @param array $data
* @return stdClass
*/
public function create_service(array $data): \stdClass {
$webservicemanager = new webservice();
$requiredfields = [
'name',
'shortname',
];
foreach ($requiredfields as $fieldname) {
if (!array_key_exists($fieldname, $data)) {
throw new \coding_exception("Field '{$fieldname}' missing when creating new service");
}
}
$optionalfields = [
'requiredcapability' => '',
'restrictedusers' => 0,
'component' => '',
'timemodified' => time(),
];
foreach ($optionalfields as $fieldname => $value) {
if (!array_key_exists($fieldname, $data)) {
$data[$fieldname] = $value;
}
}
$serviceid = $webservicemanager->add_external_service((object) $data);
return $webservicemanager->get_external_service_by_id($serviceid);
}
/**
* Associate a webservice function with service.
*
* @param array $data
*/
public function create_service_functions(array $data): void {
$webservicemanager = new webservice();
$requiredfields = [
'service',
'functions',
];
foreach ($requiredfields as $fieldname) {
if (!array_key_exists($fieldname, $data)) {
throw new \coding_exception("Field '{$fieldname}' missing when creating new service");
}
}
$service = $webservicemanager->get_external_service_by_shortname($data['service']);
$functions = explode(',', $data['functions']);
foreach ($functions as $functionname) {
$functionname = trim($functionname);
$webservicemanager->add_external_function_to_service($functionname, $service->id);
}
}
/**
* Create a new webservice token.
*
* @param array $data
*/
public function create_token(array $data): void {
$webservicemanager = new webservice();
$requiredfields = [
'userid',
'service',
];
foreach ($requiredfields as $fieldname) {
if (!array_key_exists($fieldname, $data)) {
throw new \coding_exception("Field '{$fieldname}' missing when creating new service");
}
}
$optionalfields = [
'context' => context_system::instance(),
'validuntil' => 0,
'iprestriction' => '',
];
foreach ($optionalfields as $fieldname => $value) {
if (!array_key_exists($fieldname, $data)) {
$data[$fieldname] = $value;
}
}
$service = $webservicemanager->get_external_service_by_shortname($data['service']);
external_generate_token(
EXTERNAL_TOKEN_PERMANENT,
$service->id,
$data['userid'],
$data['context'],
$data['validuntil'],
$data['iprestriction']
);
}
}

View File

@ -193,6 +193,65 @@ class webservice_test extends advanced_testcase {
$this->assertEquals($before + 60, $token->lastaccess);
}
/**
* Tests for the {@see webservice::get_missing_capabilities_by_users()} implementation.
*/
public function test_get_missing_capabilities_by_users() {
global $DB;
$this->resetAfterTest(true);
$wsman = new webservice();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Add a test web service.
$serviceid = $wsman->add_external_service((object)[
'name' => 'Test web service',
'enabled' => 1,
'requiredcapability' => '',
'restrictedusers' => false,
'component' => 'moodle',
'downloadfiles' => false,
'uploadfiles' => false,
]);
// Add a function to the service that does not declare any capability as required.
$wsman->add_external_function_to_service('core_webservice_get_site_info', $serviceid);
// Users can be provided as an array of objects, arrays or integers (ids).
$this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1, array($user2), $user3->id], $serviceid));
// Add a function to the service that declares some capability as required, but that capability is common for
// any user. Here we use 'core_message_delete_conversation' which declares 'moodle/site:deleteownmessage' which
// in turn is granted to the authenticated user archetype by default.
$wsman->add_external_function_to_service('core_message_delete_conversation', $serviceid);
// So all three users should have this capability implicitly.
$this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1, $user2, $user3], $serviceid));
// Add a function to the service that declares some non-common capability. Here we use
// 'core_group_add_group_members' that wants 'moodle/course:managegroups'.
$wsman->add_external_function_to_service('core_group_add_group_members', $serviceid);
// Make it so that the $user1 has the capability in some course.
$course1 = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'editingteacher');
// Check that no missing capability is reported for the $user1. We don't care at what actual context the
// external function call will evaluate the permission. We just check that there is a chance that the user has
// the capability somewhere.
$this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1], $serviceid));
// But there is no place at the site where the capability would be granted to the other two users, so it is
// reported as missing.
$missing = $wsman->get_missing_capabilities_by_users([$user1, $user2, $user3], $serviceid);
$this->assertArrayNotHasKey($user1->id, $missing);
$this->assertContains('moodle/course:managegroups', $missing[$user2->id]);
$this->assertContains('moodle/course:managegroups', $missing[$user3->id]);
}
/**
* Utility method that tests the parameter type of a method info's input/output parameter.
*

View File

@ -3,6 +3,12 @@ information provided here is intended especially for developers.
This information is intended for authors of webservices, not people writing webservice clients.
=== 3.11 ===
* The method webservice::get_user_capabilities() is deprecated now without a replacement. It has been used
internally only to populate the list of missing capabilities. That functionality has been improved so that
it no longer needs this standalone method.
=== 3.10 ===
* The class externallib_advanced_testcase, used in unit tests, has a new function called "configure_filters" to easily configure filters for external functions testing.