Merge branch 'MDL-76656-master' of https://github.com/meirzamoodle/moodle

This commit is contained in:
Ilya Tregubov 2023-06-28 18:08:21 +08:00
commit ab6dc892e4
No known key found for this signature in database
GPG Key ID: 0F58186F748E55C1
19 changed files with 221 additions and 39 deletions

View File

@ -0,0 +1,47 @@
{{!
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/>.
}}
{{!
@template core_admin/webservice_token_new
This is the template for displaying the newly created webservice token.
Context variable required for this template:
* token - Token value
Example context (json):
{
"token": "ef9ee4d0c6eed5eab8453a63b93b5b8b"
}
}}
<div class="d-inline-block">
<div class="alert alert-warning">
<div class="lead">{{#str}}tokennewmessage, webservice{{/str}}</div>
</div>
<div class="alert alert-primary">
<div class="lead">{{tokenname}}</div>
<div class="d-flex justify-content-start align-middle">
<div class="lead text-break pt-1" id="copytoclipboardtoken">{{token}}</div>
<button class="btn btn-primary ml-2" data-action="copytoclipboard" data-clipboard-target="#copytoclipboardtoken" data-clipboard-success-message="{{#str}}tokencopied, webservice{{/str}}">
{{#pix}}t/copy, core {{/pix}}{{#str}}copytoclipboard{{/str}}</button>
</div>
</div>
</div>
{{#js}}
require(['core/copy_to_clipboard']);
{{/js}}

View File

@ -19,19 +19,31 @@ Feature: Manage external services tokens
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 "Name" to "Webservice1"
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 "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"
Then I should see "Firstname1 Lastname1" in the "Webservice1" "table_row"
And I should see "127.0.0.1" in the "Webservice1" "table_row"
# Verify the message and the "Copy to clipboard" button.
And I should see "Copy the token now. It won't be shown again once you leave this page."
And "Copy to clipboard" "button" should exist
# New token can only read once.
And I reload the page
And I should not see "Copy the token now. It won't be shown again once you leave this page."
And "Copy to clipboard" "button" should not exist
# Delete token.
And I click on "Delete" "link" in the "Webservice1" "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 "Firstname1 Lastname1" "table_row" should not exist
And "Webservice1" "table_row" should not exist
@javascript @skip_chrome_zerosize
Scenario: Tokens can be filtered by user and by service
Scenario: Tokens can be filtered by name (case-insensitive), by user and by service
Given the following "core_webservice > Service" exists:
| name | Site information |
| shortname | siteinfo |
@ -40,10 +52,10 @@ Feature: Manage external services tokens
| 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 |
| user | service | name |
| user2 | siteinfo | WEBservice1 |
| user3 | moodle_mobile_app | webservicE3 |
| user4 | siteinfo | New service2 |
When I log in as "admin"
And I navigate to "Server > Web services > Manage tokens" in site administration
@ -53,6 +65,20 @@ Feature: Manage external services tokens
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 by name (case-insensitive).
And I click on "Tokens filter" "link"
And I set the field "Name" to "webservice"
And I press "Show only matching 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 "Firstname4 Lastname4" "table_row" should not exist
# 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 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"

View File

@ -30,7 +30,7 @@ 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);
$fname = optional_param('fname', '', PARAM_ALPHANUM);
$fusers = optional_param_array('fusers', [], PARAM_INT);
$fservices = optional_param_array('fservices', [], PARAM_INT);
@ -74,7 +74,8 @@ if ($action === 'create') {
$data->user,
context_system::instance(),
$data->validuntil,
$data->iprestriction
$data->iprestriction,
$data->name
);
redirect($PAGE->url);
}
@ -127,7 +128,7 @@ if ($action === 'delete') {
// 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,
'name' => $fname,
'users' => $fusers,
'services' => $fservices,
];
@ -150,12 +151,28 @@ 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', single_button::BUTTON_PRIMARY)), 'my-3');
if (!empty($SESSION->webservicenewlycreatedtoken)) {
$webservicemanager = new webservice();
$newtoken = $webservicemanager->get_created_by_user_ws_token(
$USER->id,
$SESSION->webservicenewlycreatedtoken
);
if ($newtoken) {
// Unset the session variable.
unset($SESSION->webservicenewlycreatedtoken);
// Display the newly created token.
echo $OUTPUT->render_from_template(
'core_admin/webservice_token_new', ['token' => $newtoken->token, 'tokenname' => $newtoken->tokenname]
);
}
}
$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]);
$baseurl = new moodle_url($PAGE->url, ['fname' => $filterdata->name]);
foreach ($filterdata->users as $i => $userid) {
$baseurl->param("fusers[{$i}]", $userid);

View File

@ -31,6 +31,7 @@ $string['privacy:metadata:tokens'] = 'A record of tokens for interacting with Mo
$string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
$string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
$string['privacy:metadata:tokens:lastaccess'] = 'The date when the token was last used';
$string['privacy:metadata:tokens:name'] = 'The token name';
$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO';
$string['privacy:metadata:tokens:timecreated'] = 'The date when the token was created';
$string['privacy:metadata:tokens:token'] = 'The user\'s token';

View File

@ -191,11 +191,16 @@ $string['testwithtestclient'] = 'Test the service';
$string['testwithtestclientdescription'] = 'Simulate external access to the service using the web service test client. Use an enabled protocol with token authentication. <strong>WARNING: The functions that you test WILL BE EXECUTED, so be careful what you choose to test!</strong>';
$string['token'] = 'Token';
$string['tokenauthlog'] = 'Token authentication';
$string['tokencopied'] = 'Text copied to clipboard.';
$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['tokenname'] = 'Name';
$string['tokennamehint'] = 'If you don\'t enter a name then a random name will be used.';
$string['tokennameprefix'] = 'Webservice-{$a}';
$string['tokennewmessage'] = 'Copy the token now. It won\'t be shown again once you leave this page.';
$string['unknownoptionkey'] = 'Unknown option key ({$a})';
$string['unnamedstringparam'] = 'A string parameter is unnamed.';
$string['updateusersettings'] = 'Update';
@ -210,6 +215,7 @@ $string['usernotallowed'] = 'The user is not allowed for this service. First you
$string['userservices'] = 'User services: {$a}';
$string['usersettingssaved'] = 'User settings saved';
$string['validuntil'] = 'Valid until';
$string['validuntil_empty'] = 'This token has no expiration date.';
$string['validuntil_help'] = 'If set, the service will be inactivated after this date for this user.';
$string['webservice'] = 'Web service';
$string['webservices'] = 'Web services';

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20230307" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20230524" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -2902,6 +2902,7 @@
<FIELD NAME="validuntil" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="timestampt - valid until data"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="created timestamp"/>
<FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="last access timestamp"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="token name, used to identify the token at the table view"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -3314,5 +3314,24 @@ privatefiles,moodle|/user/files.php';
upgrade_main_savepoint(true, 2023062200.00);
}
if ($oldversion < 2023062700.01) {
// Define field name to be added to external_tokens.
$table = new xmldb_table('external_tokens');
$field = new xmldb_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'lastaccess');
// Conditionally launch add field name.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Update the old external tokens.
$sql = 'UPDATE {external_tokens}
SET name = ' . $DB->sql_concat(
// We only need the prefix, so leave the third param with an empty string.
"'" . get_string('tokennameprefix', 'webservice', '') . "'",
"id");
$DB->execute($sql);
// Main savepoint reached.
upgrade_main_savepoint(true, 2023062700.01);
}
return true;
}

View File

@ -56,6 +56,7 @@ class provider implements
'validuntil' => 'privacy:metadata:tokens:validuntil',
'timecreated' => 'privacy:metadata:tokens:timecreated',
'lastaccess' => 'privacy:metadata:tokens:lastaccess',
'name' => 'privacy:metadata:tokens:name',
], 'privacy:metadata:tokens');
$collection->add_database_table('external_services_users', [
@ -293,6 +294,7 @@ class provider implements
'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
'created_on' => transform::datetime($record->timecreated),
'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null,
'name' => $record->name,
];
}

View File

@ -173,6 +173,7 @@ class util {
* @param context $context
* @param int $validuntil date when the token expired
* @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
* @param string $name token name as a note or token identity at the table view.
* @return string generated token
*/
public static function generate_token(
@ -181,9 +182,10 @@ class util {
int $userid,
context $context,
int $validuntil = 0,
string $iprestriction = ''
string $iprestriction = '',
string $name = ''
): string {
global $DB, $USER;
global $DB, $USER, $SESSION;
// Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
$numtries = 0;
@ -220,7 +222,17 @@ class util {
// Generate the private token, it must be transmitted only via https.
$newtoken->privatetoken = random_string(64);
$DB->insert_record('external_tokens', $newtoken);
if (!$name) {
// Generate a token name.
$name = self::generate_token_name();
}
$newtoken->name = $name;
$tokenid = $DB->insert_record('external_tokens', $newtoken);
// Create new session to hold newly created token ID.
$SESSION->webservicenewlycreatedtoken = $tokenid;
return $newtoken->token;
}
@ -399,6 +411,7 @@ class util {
$token->iprestriction = null;
$token->sid = null;
$token->lastaccess = null;
$token->name = self::generate_token_name();
// Generate the private token, it must be transmitted only via https.
$token->privatetoken = random_string(64);
$token->id = $DB->insert_record('external_tokens', $token);
@ -619,4 +632,17 @@ class util {
$DB->delete_records('external_services', ['component' => $component]);
$DB->delete_records('external_functions', ['component' => $component]);
}
/**
* Generate token name.
*
* @return string
*/
public static function generate_token_name(): string {
return get_string(
'tokennameprefix',
'webservice',
random_string(5)
);
}
}

View File

@ -30,6 +30,9 @@ information provided here is intended especially for developers.
- core_useragent::get_device_type_cfg_var_name()
- theme_is_device_locked()
- theme_get_locked_theme_for_device()
* Addition of new 'name' field in the external_tokens table.
* \core_external\util::generate_token() has a new optional argument "name" used as a token name.
* Introduce a new public function \core_external\util::generate_token_name()
=== 4.2 ===

View File

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

View File

@ -51,9 +51,9 @@ class token_filter extends moodleform {
$mform->setExpanded('tokenfilter', true);
}
// Token.
$mform->addElement('text', 'token', get_string('token', 'core_webservice'), ['size' => 32]);
$mform->setType('token', PARAM_ALPHANUM);
// Token name.
$mform->addElement('text', 'name', get_string('tokenname', 'core_webservice'), ['size' => 32]);
$mform->setType('name', PARAM_TEXT);
// User selector.
$attributes = [

View File

@ -26,6 +26,8 @@
namespace core_webservice;
use core_user;
use DateInterval;
use DateTime;
/**
* Form to create and edit a web service token.
@ -49,6 +51,10 @@ class token_form extends \moodleform {
$mform->addElement('header', 'token', get_string('token', 'webservice'));
$mform->addElement('text', 'name', get_string('tokenname', 'webservice'));
$mform->setType('name', PARAM_TEXT);
$mform->addElement('static', 'tokennamehint', '', get_string('tokennamehint', 'webservice'));
// User selector.
$attributes = [
'multiple' => false,
@ -90,6 +96,10 @@ class token_form extends \moodleform {
$mform->addElement('date_selector', 'validuntil',
get_string('validuntil', 'webservice'), array('optional' => true));
// Expires in 30 days.
$expires = new DateTime();
$expires->add(new DateInterval("P30D"));
$mform->setDefault('validuntil', $expires->getTimestamp());
$mform->setType('validuntil', PARAM_INT);
$mform->addElement('hidden', 'action');

View File

@ -79,8 +79,8 @@ class token_table extends \table_sql {
$headers = [];
$columns = [];
$headers[] = get_string('token', 'webservice');
$columns[] = 'token';
$headers[] = get_string('tokenname', 'webservice');
$columns[] = 'name';
$headers[] = get_string('user');
$columns[] = 'fullname';
$headers[] = get_string('service', 'webservice');
@ -132,7 +132,7 @@ class token_table extends \table_sql {
*/
public function col_validuntil($data) {
if (empty($data->validuntil)) {
return '';
return get_string('validuntil_empty', 'webservice');
} else {
return userdate($data->validuntil, get_string('strftimedatetime', 'langconfig'));
}
@ -184,8 +184,13 @@ class token_table extends \table_sql {
*
* @param \stdClass $data Data for the current row
* @return string Content for the column
*
* @deprecated since Moodle 4.3 MDL-76656. Please do not use this function anymore.
* @todo MDL-78605 Final deprecation in Moodle 4.7.
*/
public function col_token($data) {
debugging('The function ' . __FUNCTION__ . '() is deprecated - please do not use it any more. ', DEBUG_DEVELOPER);
global $USER;
// Hide the token if it wasn't created by the current user.
if ($data->creatorid != $USER->id) {
@ -195,6 +200,16 @@ class token_table extends \table_sql {
return $data->token;
}
/**
* Generate the name column.
*
* @param \stdClass $data Data for the current row
* @return string Content for the column
*/
public function col_name($data) {
return $data->name;
}
/**
* Generate the creator column.
*
@ -267,7 +282,7 @@ class token_table extends \table_sql {
$params = ['tokenmode' => EXTERNAL_TOKEN_PERMANENT];
$selectfields = "SELECT t.id, t.token, t.iprestriction, t.validuntil, t.creatorid,
$selectfields = "SELECT t.id, t.name, t.iprestriction, t.validuntil, t.creatorid,
u.id AS userid, $usernamefields,
s.id AS serviceid, s.name AS servicename, s.shortname AS serviceshortname,
$creatorfields ";
@ -286,9 +301,9 @@ class token_table extends \table_sql {
$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 ($this->filterdata->name !== '') {
$sql .= " AND " . $DB->sql_like("t.name", ":name", false, false);
$params['name'] = "%" . $DB->sql_like_escape($this->filterdata->name) . "%";
}
if (!empty($this->filterdata->users)) {

View File

@ -372,6 +372,7 @@ class webservice {
$newtoken->contextid = context_system::instance()->id;
$newtoken->creatorid = $userid;
$newtoken->timecreated = time();
$newtoken->name = \core_external\util::generate_token_name();
// Generate the private token, it must be transmitted only via https.
$newtoken->privatetoken = random_string(64);
@ -395,7 +396,8 @@ class webservice {
global $DB;
//here retrieve token list (including linked users firstname/lastname and linked services name)
$sql = "SELECT
t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil
t.id, t.creatorid, t.name as tokenname, u.firstname, u.lastname,
s.id as wsid, s.name as servicename, s.enabled, s.restrictedusers, t.validuntil
FROM
{external_tokens} t, {user} u, {external_services} s
WHERE
@ -422,7 +424,7 @@ class webservice {
public function get_created_by_user_ws_token($userid, $tokenid) {
global $DB;
$sql = "SELECT
t.id, t.token, u.firstname, u.lastname, s.name
t.id, t.token, t.name AS tokenname, u.firstname, u.lastname, s.name
FROM
{external_tokens} t, {user} u, {external_services} s
WHERE
@ -861,9 +863,10 @@ class webservice {
public static function get_active_tokens($userid) {
global $DB;
$sql = 'SELECT t.*, s.name as servicename FROM {external_tokens} t JOIN
{external_services} s ON t.externalserviceid = s.id WHERE
t.userid = :userid AND (COALESCE(t.validuntil, 0) = 0 OR t.validuntil > :now)';
$sql = 'SELECT t.id, t.creatorid, t.externalserviceid, t.name AS tokenname, t.validuntil, s.name AS servicename
FROM {external_tokens} t
JOIN {external_services} s ON t.externalserviceid = s.id
WHERE t.userid = :userid AND (COALESCE(t.validuntil, 0) = 0 OR t.validuntil > :now)';
$params = array('userid' => $userid, 'now' => time());
return $DB->get_records_sql($sql, $params);
}

View File

@ -290,7 +290,7 @@ class core_webservice_renderer extends plugin_renderer_base {
// display strings
$stroperation = get_string('operation', 'webservice');
$strtoken = get_string('key', 'webservice');
$strtoken = get_string('tokenname', 'webservice');
$strservice = get_string('service', 'webservice');
$strcreator = get_string('tokencreator', 'webservice');
$strcontext = get_string('context', 'webservice');
@ -338,11 +338,11 @@ class core_webservice_renderer extends plugin_renderer_base {
$validuntil = userdate($token->validuntil, get_string('strftimedatetime', 'langconfig'));
}
$tokenname = $token->name;
if (!$token->enabled) { //that is the (1 token-1ws) related ws is not enabled.
$tokenname = '<span class="dimmed_text">'.$token->name.'</span>';
$servicename = $token->servicename;
if (!$token->enabled) { // That is the (1 token-1ws) related ws is not enabled.
$servicename = '<span class="dimmed_text">'.$token->servicename.'</span>';
}
$row = array($token->token, $tokenname, $validuntil, $creatoratag, $reset);
$row = array($token->tokenname, $servicename, $validuntil, $creatoratag, $reset);
if ($documentation) {
$doclink = new moodle_url('/webservice/wsdoc.php',

View File

@ -86,6 +86,7 @@ class externallib_test extends externallib_advanced_testcase {
$externaltoken->contextid = 1;
$externaltoken->creatorid = $USER->id;
$externaltoken->timecreated = time();
$externaltoken->name = \core_external\util::generate_token_name();
$DB->insert_record('external_tokens', $externaltoken);
$siteinfo = \core_webservice_external::get_site_info();
@ -154,6 +155,7 @@ class externallib_test extends externallib_advanced_testcase {
$externaltoken->contextid = 1;
$externaltoken->creatorid = $USER->id;
$externaltoken->timecreated = time();
$externaltoken->name = \core_external\util::generate_token_name();
$DB->insert_record('external_tokens', $externaltoken);
// Set a home page by user preferences.
@ -260,6 +262,7 @@ class externallib_test extends externallib_advanced_testcase {
$externaltoken->contextid = 1;
$externaltoken->creatorid = $USER->id;
$externaltoken->timecreated = time();
$externaltoken->name = \core_external\util::generate_token_name();
$DB->insert_record('external_tokens', $externaltoken);
// Execution should complete.

View File

@ -118,6 +118,7 @@ class core_webservice_generator extends component_generator_base {
'context' => context_system::instance(),
'validuntil' => 0,
'iprestriction' => '',
'name' => '',
];
foreach ($optionalfields as $fieldname => $value) {
@ -134,7 +135,8 @@ class core_webservice_generator extends component_generator_base {
$data['userid'],
$data['context'],
$data['validuntil'],
$data['iprestriction']
$data['iprestriction'],
$data['name']
);
}
}

View File

@ -87,6 +87,7 @@ class lib_test extends \advanced_testcase {
$externaltoken->contextid = 1;
$externaltoken->creatorid = $USER->id;
$externaltoken->timecreated = time();
$externaltoken->name = \core_external\util::generate_token_name();
$DB->insert_record('external_tokens', $externaltoken);
// Add a function to the service.