Merge branch 'MDL-70941-310' of git://github.com/dravek/moodle into MOODLE_310_STABLE
[moodle.git] / badges / classes / backpack_api.php
blobe0db0035400e11ce63c978f42aba9b6eb8d02cb2
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Communicate with backpacks.
20 * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com>
25 namespace core_badges;
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->libdir . '/filelib.php');
31 use cache;
32 use coding_exception;
33 use core_badges\external\assertion_exporter;
34 use core_badges\external\collection_exporter;
35 use core_badges\external\issuer_exporter;
36 use core_badges\external\badgeclass_exporter;
37 use curl;
38 use stdClass;
39 use context_system;
41 define('BADGE_ACCESS_TOKEN', 'access');
42 define('BADGE_USER_ID_TOKEN', 'user_id');
43 define('BADGE_BACKPACK_ID_TOKEN', 'backpack_id');
44 define('BADGE_REFRESH_TOKEN', 'refresh');
45 define('BADGE_EXPIRES_TOKEN', 'expires');
47 /**
48 * Class for communicating with backpacks.
50 * @package core_badges
51 * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 class backpack_api {
56 /** @var string The email address of the issuer or the backpack owner. */
57 private $email;
59 /** @var string The base url used for api requests to this backpack. */
60 private $backpackapiurl;
62 /** @var integer The backpack api version to use. */
63 private $backpackapiversion;
65 /** @var string The password to authenticate requests. */
66 private $password;
68 /** @var boolean User or site api requests. */
69 private $isuserbackpack;
71 /** @var integer The id of the backpack we are talking to. */
72 private $backpackid;
74 /** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */
75 private $mappings = [];
77 /**
78 * Create a wrapper to communicate with the backpack.
80 * The resulting class can only do either site backpack communication or
81 * user backpack communication.
83 * @param stdClass $sitebackpack The site backpack record
84 * @param mixed $userbackpack Optional - if passed it represents the users backpack.
86 public function __construct($sitebackpack, $userbackpack = false) {
87 global $CFG;
88 $admin = get_admin();
90 $this->backpackapiurl = $sitebackpack->backpackapiurl;
91 $this->backpackapiversion = $sitebackpack->apiversion;
92 $this->password = $sitebackpack->password;
93 $this->email = $sitebackpack->backpackemail;
94 $this->isuserbackpack = false;
95 $this->backpackid = $sitebackpack->id;
96 if (!empty($userbackpack)) {
97 $this->isuserbackpack = true;
98 $this->password = $userbackpack->password;
99 $this->email = $userbackpack->email;
102 $this->define_mappings();
103 // Clear the last authentication error.
104 backpack_api_mapping::set_authentication_error('');
108 * Define the mappings supported by this usage and api version.
110 private function define_mappings() {
111 if ($this->backpackapiversion == OPEN_BADGES_V2) {
112 if ($this->isuserbackpack) {
113 $mapping = [];
114 $mapping[] = [
115 'collections', // Action.
116 '[URL]/backpack/collections', // URL
117 [], // Post params.
118 '', // Request exporter.
119 'core_badges\external\collection_exporter', // Response exporter.
120 true, // Multiple.
121 'get', // Method.
122 true, // JSON Encoded.
123 true // Auth required.
125 $mapping[] = [
126 'user', // Action.
127 '[SCHEME]://[HOST]/o/token', // URL
128 ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.
129 '', // Request exporter.
130 'oauth_token_response', // Response exporter.
131 false, // Multiple.
132 'post', // Method.
133 false, // JSON Encoded.
134 false, // Auth required.
136 $mapping[] = [
137 'assertion', // Action.
138 // Badgr.io does not return the public information about a badge
139 // if the issuer is associated with another user. We need to pass
140 // the expand parameters which are not in any specification to get
141 // additional information about the assertion in a single request.
142 '[URL]/backpack/assertions/[PARAM2]?expand=badgeclass&expand=issuer',
143 [], // Post params.
144 '', // Request exporter.
145 'core_badges\external\assertion_exporter', // Response exporter.
146 false, // Multiple.
147 'get', // Method.
148 true, // JSON Encoded.
149 true // Auth required.
151 $mapping[] = [
152 'importbadge', // Action.
153 // Badgr.io does not return the public information about a badge
154 // if the issuer is associated with another user. We need to pass
155 // the expand parameters which are not in any specification to get
156 // additional information about the assertion in a single request.
157 '[URL]/backpack/import',
158 ['url' => '[PARAM]'], // Post params.
159 '', // Request exporter.
160 'core_badges\external\assertion_exporter', // Response exporter.
161 false, // Multiple.
162 'post', // Method.
163 true, // JSON Encoded.
164 true // Auth required.
166 $mapping[] = [
167 'badges', // Action.
168 '[URL]/backpack/collections/[PARAM1]', // URL
169 [], // Post params.
170 '', // Request exporter.
171 'core_badges\external\collection_exporter', // Response exporter.
172 true, // Multiple.
173 'get', // Method.
174 true, // JSON Encoded.
175 true // Auth required.
177 foreach ($mapping as $map) {
178 $map[] = true; // User api function.
179 $map[] = OPEN_BADGES_V2; // V2 function.
180 $this->mappings[] = new backpack_api_mapping(...$map);
182 } else {
183 $mapping = [];
184 $mapping[] = [
185 'user', // Action.
186 '[SCHEME]://[HOST]/o/token', // URL
187 ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.
188 '', // Request exporter.
189 'oauth_token_response', // Response exporter.
190 false, // Multiple.
191 'post', // Method.
192 false, // JSON Encoded.
193 false // Auth required.
195 $mapping[] = [
196 'issuers', // Action.
197 '[URL]/issuers', // URL
198 '[PARAM]', // Post params.
199 'core_badges\external\issuer_exporter', // Request exporter.
200 'core_badges\external\issuer_exporter', // Response exporter.
201 false, // Multiple.
202 'post', // Method.
203 true, // JSON Encoded.
204 true // Auth required.
206 $mapping[] = [
207 'badgeclasses', // Action.
208 '[URL]/issuers/[PARAM2]/badgeclasses', // URL
209 '[PARAM]', // Post params.
210 'core_badges\external\badgeclass_exporter', // Request exporter.
211 'core_badges\external\badgeclass_exporter', // Response exporter.
212 false, // Multiple.
213 'post', // Method.
214 true, // JSON Encoded.
215 true // Auth required.
217 $mapping[] = [
218 'assertions', // Action.
219 '[URL]/badgeclasses/[PARAM2]/assertions', // URL
220 '[PARAM]', // Post params.
221 'core_badges\external\assertion_exporter', // Request exporter.
222 'core_badges\external\assertion_exporter', // Response exporter.
223 false, // Multiple.
224 'post', // Method.
225 true, // JSON Encoded.
226 true // Auth required.
228 $mapping[] = [
229 'updateassertion', // Action.
230 '[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer',
231 '[PARAM]', // Post params.
232 'core_badges\external\assertion_exporter', // Request exporter.
233 'core_badges\external\assertion_exporter', // Response exporter.
234 false, // Multiple.
235 'put', // Method.
236 true, // JSON Encoded.
237 true // Auth required.
239 foreach ($mapping as $map) {
240 $map[] = false; // Site api function.
241 $map[] = OPEN_BADGES_V2; // V2 function.
242 $this->mappings[] = new backpack_api_mapping(...$map);
245 } else {
246 if ($this->isuserbackpack) {
247 $mapping = [];
248 $mapping[] = [
249 'user', // Action.
250 '[URL]/displayer/convert/email', // URL
251 ['email' => '[EMAIL]'], // Post params.
252 '', // Request exporter.
253 'convert_email_response', // Response exporter.
254 false, // Multiple.
255 'post', // Method.
256 false, // JSON Encoded.
257 false // Auth required.
259 $mapping[] = [
260 'groups', // Action.
261 '[URL]/displayer/[PARAM1]/groups.json', // URL
262 [], // Post params.
263 '', // Request exporter.
264 '', // Response exporter.
265 false, // Multiple.
266 'get', // Method.
267 true, // JSON Encoded.
268 true // Auth required.
270 $mapping[] = [
271 'badges', // Action.
272 '[URL]/displayer/[PARAM2]/group/[PARAM1].json', // URL
273 [], // Post params.
274 '', // Request exporter.
275 '', // Response exporter.
276 false, // Multiple.
277 'get', // Method.
278 true, // JSON Encoded.
279 true // Auth required.
281 foreach ($mapping as $map) {
282 $map[] = true; // User api function.
283 $map[] = OPEN_BADGES_V1; // V1 function.
284 $this->mappings[] = new backpack_api_mapping(...$map);
286 } else {
287 $mapping = [];
288 $mapping[] = [
289 'user', // Action.
290 '[URL]/displayer/convert/email', // URL
291 ['email' => '[EMAIL]'], // Post params.
292 '', // Request exporter.
293 'convert_email_response', // Response exporter.
294 false, // Multiple.
295 'post', // Method.
296 false, // JSON Encoded.
297 false // Auth required.
299 foreach ($mapping as $map) {
300 $map[] = false; // Site api function.
301 $map[] = OPEN_BADGES_V1; // V1 function.
302 $this->mappings[] = new backpack_api_mapping(...$map);
309 * Make an api request
311 * @param string $action The api function.
312 * @param string $collection An api parameter
313 * @param string $entityid An api parameter
314 * @param string $postdata The body of the api request.
315 * @return mixed
317 private function curl_request($action, $collection = null, $entityid = null, $postdata = null) {
318 global $CFG, $SESSION;
320 $curl = new curl();
321 $authrequired = false;
322 if ($this->backpackapiversion == OPEN_BADGES_V1) {
323 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
324 if (isset($SESSION->$useridkey)) {
325 if ($collection == null) {
326 $collection = $SESSION->$useridkey;
327 } else {
328 $entityid = $SESSION->$useridkey;
332 foreach ($this->mappings as $mapping) {
333 if ($mapping->is_match($action)) {
334 return $mapping->request(
335 $this->backpackapiurl,
336 $collection,
337 $entityid,
338 $this->email,
339 $this->password,
340 $postdata,
341 $this->backpackid
346 throw new coding_exception('Unknown request');
350 * Get the id to use for requests with this api.
352 * @return integer
354 private function get_auth_user_id() {
355 global $USER;
357 if ($this->isuserbackpack) {
358 return $USER->id;
359 } else {
360 // The access tokens for the system backpack are shared.
361 return -1;
366 * Get the name of the key to store this access token type.
368 * @param string $type
369 * @return string
371 private function get_token_key($type) {
372 // This should be removed when everything has a mapping.
373 $prefix = 'badges_';
374 if ($this->isuserbackpack) {
375 $prefix .= 'user_backpack_';
376 } else {
377 $prefix .= 'site_backpack_';
379 $prefix .= $type . '_token';
380 return $prefix;
384 * Normalise the return from a missing user request.
386 * @param string $status
387 * @return mixed
389 private function check_status($status) {
390 // V1 ONLY.
391 switch($status) {
392 case "missing":
393 $response = array(
394 'status' => $status,
395 'message' => get_string('error:nosuchuser', 'badges')
397 return $response;
399 return false;
403 * Make an api request to get an assertion
405 * @param string $entityid The id of the assertion.
406 * @return mixed
408 public function get_assertion($entityid) {
409 // V2 Only.
410 if ($this->backpackapiversion == OPEN_BADGES_V1) {
411 throw new coding_exception('Not supported in this backpack API');
414 return $this->curl_request('assertion', null, $entityid);
418 * Create a badgeclass assertion.
420 * @param string $entityid The id of the badge class.
421 * @param string $data The structure of the badge class assertion.
422 * @return mixed
424 public function put_badgeclass_assertion($entityid, $data) {
425 // V2 Only.
426 if ($this->backpackapiversion == OPEN_BADGES_V1) {
427 throw new coding_exception('Not supported in this backpack API');
430 return $this->curl_request('assertions', null, $entityid, $data);
434 * Update a badgeclass assertion.
436 * @param string $entityid The id of the badge class.
437 * @param array $data The structure of the badge class assertion.
438 * @return mixed
440 public function update_assertion(string $entityid, array $data) {
441 // V2 Only.
442 if ($this->backpackapiversion == OPEN_BADGES_V1) {
443 throw new coding_exception('Not supported in this backpack API');
446 return $this->curl_request('updateassertion', null, $entityid, $data);
450 * Import a badge assertion into a backpack. This is used to handle cross domain backpacks.
452 * @param string $data The structure of the badge class assertion.
453 * @return mixed
454 * @throws coding_exception
456 public function import_badge_assertion(string $data) {
457 // V2 Only.
458 if ($this->backpackapiversion == OPEN_BADGES_V1) {
459 throw new coding_exception('Not supported in this backpack API');
462 return $this->curl_request('importbadge', null, null, $data);
466 * Select collections from a backpack.
468 * @param string $backpackid The id of the backpack
469 * @param stdClass[] $collections List of collections with collectionid or entityid.
470 * @return boolean
472 public function set_backpack_collections($backpackid, $collections) {
473 global $DB, $USER;
475 // Delete any previously selected collections.
476 $sqlparams = array('backpack' => $backpackid);
477 $select = 'backpackid = :backpack ';
478 $DB->delete_records_select('badge_external', $select, $sqlparams);
479 $badgescache = cache::make('core', 'externalbadges');
481 // Insert selected collections if they are not in database yet.
482 foreach ($collections as $collection) {
483 $obj = new stdClass();
484 $obj->backpackid = $backpackid;
485 if ($this->backpackapiversion == OPEN_BADGES_V1) {
486 $obj->collectionid = (int) $collection;
487 } else {
488 $obj->entityid = $collection;
489 $obj->collectionid = -1;
491 if (!$DB->record_exists('badge_external', (array) $obj)) {
492 $DB->insert_record('badge_external', $obj);
495 $badgescache->delete($USER->id);
496 return true;
500 * Create a badgeclass
502 * @param string $entityid The id of the entity.
503 * @param string $data The structure of the badge class.
504 * @return mixed
506 public function put_badgeclass($entityid, $data) {
507 // V2 Only.
508 if ($this->backpackapiversion == OPEN_BADGES_V1) {
509 throw new coding_exception('Not supported in this backpack API');
512 return $this->curl_request('badgeclasses', null, $entityid, $data);
516 * Create an issuer
518 * @param string $data The structure of the issuer.
519 * @return mixed
521 public function put_issuer($data) {
522 // V2 Only.
523 if ($this->backpackapiversion == OPEN_BADGES_V1) {
524 throw new coding_exception('Not supported in this backpack API');
527 return $this->curl_request('issuers', null, null, $data);
531 * Delete any user access tokens in the session so we will attempt to get new ones.
533 * @return void
535 public function clear_system_user_session() {
536 global $SESSION;
538 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
539 unset($SESSION->$useridkey);
541 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
542 unset($SESSION->$expireskey);
546 * Authenticate using the stored email and password and save the valid access tokens.
548 * @return integer The id of the authenticated user.
550 public function authenticate() {
551 global $SESSION;
553 $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
554 $backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0;
555 // If the backpack is changed we need to expire sessions.
556 if ($backpackid == $this->backpackid) {
557 if ($this->backpackapiversion == OPEN_BADGES_V2) {
558 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
559 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
560 if ($authuserid == $this->get_auth_user_id()) {
561 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
562 if (isset($SESSION->$expireskey)) {
563 $expires = $SESSION->$expireskey;
564 if ($expires > time()) {
565 // We have a current access token for this user
566 // that has not expired.
567 return -1;
571 } else {
572 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
573 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
574 if (!empty($authuserid)) {
575 return $authuserid;
579 return $this->curl_request('user', $this->email);
583 * Get all collections in this backpack.
585 * @return stdClass[] The collections.
587 public function get_collections() {
588 global $PAGE;
590 if ($this->authenticate()) {
591 if ($this->backpackapiversion == OPEN_BADGES_V1) {
592 $result = $this->curl_request('groups');
593 if (isset($result->groups)) {
594 $result = $result->groups;
596 } else {
597 $result = $this->curl_request('collections');
599 if ($result) {
600 return $result;
603 return [];
607 * Get one collection by id.
609 * @param integer $collectionid
610 * @return stdClass The collection.
612 public function get_collection_record($collectionid) {
613 global $DB;
615 if ($this->backpackapiversion == OPEN_BADGES_V1) {
616 return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid));
617 } else {
618 return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid));
623 * Disconnect the backpack from this user.
625 * @param integer $userid The user in Moodle
626 * @param integer $backpackid The backpack to disconnect
627 * @return boolean
629 public function disconnect_backpack($userid, $backpackid) {
630 global $DB, $USER;
632 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) {
633 // Can't change someone elses backpack settings.
634 return false;
637 $badgescache = cache::make('core', 'externalbadges');
639 $DB->delete_records('badge_external', array('backpackid' => $backpackid));
640 $DB->delete_records('badge_backpack', array('userid' => $userid));
641 $badgescache->delete($userid);
642 return true;
646 * Handle the response from getting a collection to map to an id.
648 * @param stdClass $data The response data.
649 * @return string The collection id.
651 public function get_collection_id_from_response($data) {
652 if ($this->backpackapiversion == OPEN_BADGES_V1) {
653 return $data->groupId;
654 } else {
655 return $data->entityId;
660 * Get the last error message returned during an authentication request.
662 * @return string
664 public function get_authentication_error() {
665 return backpack_api_mapping::get_authentication_error();
669 * Get the list of badges in a collection.
671 * @param stdClass $collection The collection to deal with.
672 * @param boolean $expanded Fetch all the sub entities.
673 * @return stdClass[]
675 public function get_badges($collection, $expanded = false) {
676 global $PAGE;
678 if ($this->authenticate()) {
679 if ($this->backpackapiversion == OPEN_BADGES_V1) {
680 if (empty($collection->collectionid)) {
681 return [];
683 $result = $this->curl_request('badges', $collection->collectionid);
684 return $result->badges;
685 } else {
686 if (empty($collection->entityid)) {
687 return [];
689 // Now we can make requests.
690 $badges = $this->curl_request('badges', $collection->entityid);
691 if (count($badges) == 0) {
692 return [];
694 $badges = $badges[0];
695 if ($expanded) {
696 $publicassertions = [];
697 $context = context_system::instance();
698 $output = $PAGE->get_renderer('core', 'badges');
699 foreach ($badges->assertions as $assertion) {
700 $remoteassertion = $this->get_assertion($assertion);
701 // Remote badge was fetched nested in the assertion.
702 $remotebadge = $remoteassertion->badgeclass;
703 if (!$remotebadge) {
704 continue;
706 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion);
707 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]);
708 $remotebadge = $exporterinstance->export($output);
710 $remoteissuer = $remotebadge->issuer;
711 $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion);
712 $exporterinstance = new issuer_exporter($apidata, ['context' => $context]);
713 $remoteissuer = $exporterinstance->export($output);
715 $badgeclone = clone $remotebadge;
716 $badgeclone->issuer = $remoteissuer;
717 $remoteassertion->badge = $badgeclone;
718 $remotebadge->assertion = $remoteassertion;
719 $publicassertions[] = $remotebadge;
721 $badges = $publicassertions;
723 return $badges;