diff --git a/admin/tests/behat/manage_tokens.feature b/admin/tests/behat/manage_tokens.feature
new file mode 100644
index 00000000000..d30e2300bd1
--- /dev/null
+++ b/admin/tests/behat/manage_tokens.feature
@@ -0,0 +1,26 @@
+@core @core_admin
+Feature: Manage tokens
+ In order to manage webservice usage
+ As an admin
+ I need to be able to create and delete tokens
+
+ Background:
+ Given the following "users" exist:
+ | username | password | firstname | lastname |
+ | testuser | testuser | Joe | Bloggs |
+ | testuser2 | testuser2 | TestFirstname | TestLastname |
+ 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"
+ 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"
+ And I press "Delete"
+ And I should not see "Joe Bloggs"
diff --git a/admin/webservice/tokens.php b/admin/webservice/tokens.php
index 60ea271005f..f15f7c20578 100644
--- a/admin/webservice/tokens.php
+++ b/admin/webservice/tokens.php
@@ -101,7 +101,11 @@ switch ($action) {
break;
case 'delete':
- $token = $webservicemanager->get_created_by_user_ws_token($USER->id, $tokenid);
+ $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()) {
diff --git a/lang/en/role.php b/lang/en/role.php
index 45870a2f0ba..2bfc5df9fdd 100644
--- a/lang/en/role.php
+++ b/lang/en/role.php
@@ -455,9 +455,9 @@ $string['useshowadvancedtochange'] = 'Use \'Show advanced\' to change';
$string['viewingdefinitionofrolex'] = 'Viewing the definition of role \'{$a}\'';
$string['viewrole'] = 'View role details';
$string['webservice:createtoken'] = 'Create a web service token';
+$string['webservice:managealltokens'] = 'Manage all users\' web services';
$string['webservice:createmobiletoken'] = 'Create a web service token for mobile access';
$string['whydoesuserhavecap'] = 'Why does {$a->fullname} have capability {$a->capability} in context {$a->context}?';
$string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability {$a->capability} in context {$a->context}?';
$string['xroleassignments'] = '{$a}\'s role assignments';
$string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
-
diff --git a/lang/en/webservice.php b/lang/en/webservice.php
index ebfe9384082..057f9f609b1 100644
--- a/lang/en/webservice.php
+++ b/lang/en/webservice.php
@@ -131,6 +131,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'] = 'Only tokens you own or created can be seen. You can still delete other tokens.';
$string['operation'] = 'Operation';
$string['optional'] = 'Optional';
$string['passwordisexpired'] = 'Password is expired.';
diff --git a/lib/adminlib.php b/lib/adminlib.php
index 53866698d66..410130ec38a 100644
--- a/lib/adminlib.php
+++ b/lib/adminlib.php
@@ -9524,84 +9524,29 @@ class admin_setting_managewebservicetokens extends admin_setting {
* @return string
*/
public function output_html($data, $query='') {
- global $CFG, $OUTPUT, $DB, $USER;
+ global $CFG, $OUTPUT;
- // display strings
- $stroperation = get_string('operation', 'webservice');
- $strtoken = get_string('token', 'webservice');
- $strservice = get_string('service', 'webservice');
- $struser = get_string('user');
- $strcontext = get_string('context', 'webservice');
- $strvaliduntil = get_string('validuntil', 'webservice');
- $striprestriction = get_string('iprestriction', 'webservice');
+ 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');
- $table = new html_table();
- $table->head = array($strtoken, $struser, $strservice, $striprestriction, $strvaliduntil, $stroperation);
- $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'centeralign', 'centeralign', 'centeralign');
- $table->id = 'webservicetokens';
- $table->attributes['class'] = 'admintable generaltable';
+ 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();
- //TODO: in order to let the administrator delete obsolete token, split this request in multiple request or use LEFT JOIN
-
- //here retrieve token list (including linked users firstname/lastname and linked services name)
- $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.iprestriction, t.validuntil, s.id AS serviceid
- FROM {external_tokens} t, {user} u, {external_services} s
- WHERE t.creatorid=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id";
- $tokens = $DB->get_records_sql($sql, array($USER->id, EXTERNAL_TOKEN_PERMANENT));
- if (!empty($tokens)) {
- foreach ($tokens as $token) {
- //TODO: retrieve context
-
- $delete = "id."\">";
- $delete .= get_string('delete')."";
-
- $validuntil = '';
- if (!empty($token->validuntil)) {
- $validuntil = userdate($token->validuntil, get_string('strftimedatetime', 'langconfig'));
- }
-
- $iprestriction = '';
- if (!empty($token->iprestriction)) {
- $iprestriction = $token->iprestriction;
- }
-
- $userprofilurl = new moodle_url('/user/profile.php?id='.$token->userid);
- $useratag = html_writer::start_tag('a', array('href' => $userprofilurl));
- $useratag .= $token->firstname." ".$token->lastname;
- $useratag .= html_writer::end_tag('a');
-
- //check user missing capabilities
- require_once($CFG->dirroot . '/webservice/lib.php');
- $webservicemanager = new webservice();
- $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users(
- array(array('id' => $token->userid)), $token->serviceid);
-
- if (!is_siteadmin($token->userid) and
- array_key_exists($token->userid, $usermissingcaps)) {
- $missingcapabilities = implode(', ',
- $usermissingcaps[$token->userid]);
- if (!empty($missingcapabilities)) {
- $useratag .= html_writer::tag('div',
- get_string('usermissingcaps', 'webservice',
- $missingcapabilities)
- . ' ' . $OUTPUT->help_icon('missingcaps', 'webservice'),
- array('class' => 'missingcaps'));
- }
- }
-
- $table->data[] = array($token->token, $useratag, $token->name, $iprestriction, $validuntil, $delete);
- }
-
- $return .= html_writer::table($table);
- } else {
- $return .= get_string('notoken', 'webservice');
- }
-
$return .= $OUTPUT->box_end();
// add a token to the table
$return .= "";
diff --git a/lib/db/access.php b/lib/db/access.php
index 808d0b9c1ea..86c450ca279 100644
--- a/lib/db/access.php
+++ b/lib/db/access.php
@@ -1845,6 +1845,13 @@ $capabilities = array(
'manager' => CAP_ALLOW
)
),
+ 'moodle/webservice:managealltokens' => array(
+
+ 'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_PERSONAL,
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_SYSTEM,
+ 'archetypes' => array()
+ ),
'moodle/webservice:createmobiletoken' => array(
'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
diff --git a/webservice/classes/token_table.php b/webservice/classes/token_table.php
new file mode 100644
index 00000000000..059fc049d8f
--- /dev/null
+++ b/webservice/classes/token_table.php
@@ -0,0 +1,260 @@
+.
+
+/**
+ * Contains the class used for the displaying the tokens table.
+ *
+ * @package core_webservice
+ * @copyright 2017 John Okely
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace webservice;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+require_once($CFG->dirroot . '/webservice/lib.php');
+require_once($CFG->dirroot . '/user/lib.php');
+
+/**
+ * Class for the displaying the participants table.
+ *
+ * @package core_webservice
+ * @copyright 2017 John Okely
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class token_table extends \table_sql {
+
+ /**
+ * @var bool $showalltokens Whether or not the user is able to see all tokens.
+ */
+ protected $showalltokens;
+
+ /**
+ * Sets up the table.
+ * @param int $id The id of the table
+ */
+ public function __construct($id) {
+ parent::__construct($id);
+
+ // Get the context.
+ $context = \context_system::instance();
+
+ // Can we see tokens created by all users?
+ $this->showalltokens = has_capability('moodle/webservice:managealltokens', $context);
+
+ // Define the headers and columns.
+ $headers = [];
+ $columns = [];
+
+ $headers[] = get_string('token', 'webservice');
+ $columns[] = 'token';
+ $headers[] = get_string('user');
+ $columns[] = 'fullname';
+ $headers[] = get_string('service', 'webservice');
+ $columns[] = 'name';
+ $headers[] = get_string('iprestriction', 'webservice');
+ $columns[] = 'iprestriction';
+ $headers[] = get_string('validuntil', 'webservice');
+ $columns[] = 'validuntil';
+ if ($this->showalltokens) {
+ // Only need to show creator if you can see tokens created by other people.
+ $headers[] = get_string('tokencreator', 'webservice');
+ $columns[] = 'creatorlastname'; // So we can have semi-useful sorting. Table SQL doesn't two fullname collumns.
+ }
+ $headers[] = get_string('operation', 'webservice');
+ $columns[] = 'operation';
+
+ $this->define_columns($columns);
+ $this->define_headers($headers);
+
+ $this->no_sorting('operation');
+ $this->no_sorting('token');
+ $this->no_sorting('iprestriction');
+
+ $this->set_attribute('id', $id);
+ }
+
+ /**
+ * Generate the operation column.
+ *
+ * @param \stdClass $data Data for the current row
+ * @return string Content for the column
+ */
+ public function col_operation($data) {
+ $tokenpageurl = new \moodle_url(
+ "/admin/webservice/tokens.php",
+ [
+ "sesskey" => sesskey(),
+ "action" => "delete",
+ "tokenid" => $data->id
+ ]
+ );
+ return \html_writer::link($tokenpageurl, get_string("delete"));
+ }
+
+ /**
+ * Generate the validuntil column.
+ *
+ * @param \stdClass $data Data for the current row
+ * @return string Content for the column
+ */
+ public function col_validuntil($data) {
+ if (empty($data->validuntil)) {
+ return '';
+ } else {
+ return userdate($data->validuntil, get_string('strftimedatetime', 'langconfig'));
+ }
+ }
+
+ /**
+ * Generate the fullname column. Also includes capabilities the user is missing for the webservice (if any)
+ *
+ * @param \stdClass $data Data for the current row
+ * @return string Content for the column
+ */
+ public function col_fullname($data) {
+ global $OUTPUT;
+
+ $userprofilurl = new \moodle_url('/user/profile.php', ['id' => $data->userid]);
+ $content = \html_writer::link($userprofilurl, fullname($data));
+
+ // 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) . ' ' .
+ $OUTPUT->help_icon('missingcaps', 'webservice');
+ $content .= \html_writer::div($capabilitiesstring, 'missingcaps');
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Generate the token column.
+ *
+ * @param \stdClass $data Data for the current row
+ * @return string Content for the column
+ */
+ public function col_token($data) {
+ global $USER;
+ // Hide the token if it wasn't created by the current user.
+ if ($data->creatorid != $USER->id) {
+ return '-';
+ }
+
+ return $data->token;
+ }
+
+ /**
+ * Generate the creator column.
+ *
+ * @param \stdClass $data
+ * @return string
+ */
+ public function col_creatorlastname($data) {
+ // We have loaded all the name fields for the creator, with the 'creator' prefix.
+ // So just remove the prefix and make up a user object.
+ $user = [];
+ foreach ($data as $key => $value) {
+ if (strpos($key, 'creator') !== false) {
+ $newkey = str_replace('creator', '', $key);
+ $user[$newkey] = $value;
+ }
+ }
+
+ $creatorprofileurl = new \moodle_url('/user/profile.php', ['id' => $data->creatorid]);
+ return \html_writer::link($creatorprofileurl, fullname((object)$user));
+ }
+
+ /**
+ * This function is used for the extra user fields.
+ *
+ * These are being dynamically added to the table so there are no functions 'col_' as
+ * the list has the potential to increase in the future and we don't want to have to remember to add
+ * a new method to this class. We also don't want to pollute this class with unnecessary methods.
+ *
+ * @param string $colname The column name
+ * @param \stdClass $data
+ * @return string
+ */
+ public function other_cols($colname, $data) {
+ return s($data->{$colname});
+ }
+
+ /**
+ * Query the database for results to display in the table.
+ *
+ * Note: Initial bars are not implemented for this table because it includes user details twice and the initial bars do not work
+ * when the user table is included more than once.
+ *
+ * @param int $pagesize size of page for paginated displayed table.
+ * @param bool $useinitialsbar Not implemented. Please pass false.
+ */
+ public function query_db($pagesize, $useinitialsbar = false) {
+ global $DB, $USER;
+
+ if ($useinitialsbar) {
+ debugging('Initial bar not implemented yet. Call out($pagesize, false)');
+ }
+
+ $usernamefields = get_all_user_name_fields(true, 'u');
+ $creatorfields = get_all_user_name_fields(true, 'c', null, 'creator');
+
+ $params = ["tokenmode" => EXTERNAL_TOKEN_PERMANENT];
+
+ // TODO: in order to let the administrator delete obsolete token, split the request in multiple request or use LEFT JOIN.
+
+ 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 {
+ // 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;
+ }
+
+ $sort = $this->get_sql_sort();
+ if ($sort) {
+ $sql = $sql . ' ORDER BY ' . $sort;
+ }
+
+ $total = $DB->count_records_sql($countsql, $params);
+ $this->pagesize($pagesize, $total);
+
+ $this->rawdata = $DB->get_recordset_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
+ }
+}
diff --git a/webservice/lib.php b/webservice/lib.php
index 0101ccacded..2117e8bc28e 100644
--- a/webservice/lib.php
+++ b/webservice/lib.php
@@ -402,6 +402,29 @@ class webservice {
return $token;
}
+ /**
+ * Return a token of an arbitrary user by tokenid, including details of the associated user and the service name.
+ * If no tokens exist an exception is thrown
+ *
+ * The returned value is a stdClass:
+ * ->id token id
+ * ->token
+ * ->firstname user firstname
+ * ->lastname
+ * ->name service name
+ *
+ * @param int $tokenid token id
+ * @return stdClass
+ */
+ public function get_token_by_id_with_details($tokenid) {
+ global $DB;
+ $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.creatorid
+ FROM {external_tokens} t, {user} u, {external_services} s
+ WHERE t.id=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id";
+ $token = $DB->get_record_sql($sql, array($tokenid, EXTERNAL_TOKEN_PERMANENT), MUST_EXIST);
+ return $token;
+ }
+
/**
* Return a database token record for a token id
*