MDL-65959 badges: Update the implementation to use admin set backpack
[moodle.git] / badges / classes / backpack_api.php
CommitLineData
aae219ac
DW
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.
13//
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/>.
16
17/**
18 * Communicate with backpacks.
19 *
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>
23 */
24
25namespace core_badges;
26
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->libdir . '/filelib.php');
30
31use cache;
32use coding_exception;
33use core_badges\external\assertion_exporter;
34use core_badges\external\collection_exporter;
35use core_badges\external\issuer_exporter;
36use core_badges\external\badgeclass_exporter;
37use curl;
38use stdClass;
58c7d2ce 39use context_system;
aae219ac
DW
40
41define('BADGE_ACCESS_TOKEN', 'access');
42define('BADGE_USER_ID_TOKEN', 'user_id');
43define('BADGE_BACKPACK_ID_TOKEN', 'backpack_id');
44define('BADGE_REFRESH_TOKEN', 'refresh');
45define('BADGE_EXPIRES_TOKEN', 'expires');
46
47/**
48 * Class for communicating with backpacks.
1837b1d5
DW
49 *
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
aae219ac
DW
53 */
54class backpack_api {
55
56 /** @var string The email address of the issuer or the backpack owner. */
57 private $email;
58
59 /** @var string The base url used for api requests to this backpack. */
60 private $backpackapiurl;
61
62 /** @var integer The backpack api version to use. */
63 private $backpackapiversion;
64
65 /** @var string The password to authenticate requests. */
66 private $password;
67
68 /** @var boolean User or site api requests. */
69 private $isuserbackpack;
70
71 /** @var integer The id of the backpack we are talking to. */
72 private $backpackid;
73
74 /** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */
75 private $mappings = [];
76
77 /**
78 * Create a wrapper to communicate with the backpack.
79 *
80 * The resulting class can only do either site backpack communication or
81 * user backpack communication.
82 *
83 * @param stdClass $sitebackpack The site backpack record
84 * @param mixed $userbackpack Optional - if passed it represents the users backpack.
85 */
86 public function __construct($sitebackpack, $userbackpack = false) {
87 global $CFG;
88 $admin = get_admin();
89
aae219ac
DW
90 $this->backpackapiurl = $sitebackpack->backpackapiurl;
91 $this->backpackapiversion = $sitebackpack->apiversion;
92 $this->password = $sitebackpack->password;
3cae9421 93 $this->email = $sitebackpack->backpackemail;
aae219ac
DW
94 $this->isuserbackpack = false;
95 $this->backpackid = $sitebackpack->id;
96 if (!empty($userbackpack)) {
97 $this->isuserbackpack = true;
aae219ac
DW
98 $this->password = $userbackpack->password;
99 $this->email = $userbackpack->email;
100 }
101
102 $this->define_mappings();
7444ba74
DW
103 // Clear the last authentication error.
104 backpack_api_mapping::set_authentication_error('');
aae219ac
DW
105 }
106
107 /**
108 * Define the mappings supported by this usage and api version.
109 */
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.
124 ];
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.
135 ];
aae219ac
DW
136 $mapping[] = [
137 'assertion', // Action.
58c7d2ce
DW
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',
aae219ac
DW
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.
150 ];
b6435e09
P
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.
165 ];
aae219ac
DW
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.
176 ];
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);
181 }
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.
194 ];
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.
205 ];
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.
216 ];
217 $mapping[] = [
218 'assertions', // Action.
58c7d2ce 219 '[URL]/badgeclasses/[PARAM2]/assertions', // URL
aae219ac
DW
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.
227 ];
228 foreach ($mapping as $map) {
229 $map[] = false; // Site api function.
230 $map[] = OPEN_BADGES_V2; // V2 function.
231 $this->mappings[] = new backpack_api_mapping(...$map);
232 }
233 }
234 } else {
235 if ($this->isuserbackpack) {
236 $mapping = [];
237 $mapping[] = [
238 'user', // Action.
239 '[URL]/displayer/convert/email', // URL
240 ['email' => '[EMAIL]'], // Post params.
241 '', // Request exporter.
242 'convert_email_response', // Response exporter.
243 false, // Multiple.
244 'post', // Method.
245 false, // JSON Encoded.
246 false // Auth required.
247 ];
248 $mapping[] = [
249 'groups', // Action.
250 '[URL]/displayer/[PARAM1]/groups.json', // URL
251 [], // Post params.
252 '', // Request exporter.
253 '', // Response exporter.
254 false, // Multiple.
255 'get', // Method.
256 true, // JSON Encoded.
257 true // Auth required.
258 ];
259 $mapping[] = [
260 'badges', // Action.
261 '[URL]/displayer/[PARAM2]/group/[PARAM1].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.
269 ];
270 foreach ($mapping as $map) {
271 $map[] = true; // User api function.
272 $map[] = OPEN_BADGES_V1; // V1 function.
273 $this->mappings[] = new backpack_api_mapping(...$map);
274 }
275 } else {
276 $mapping = [];
277 $mapping[] = [
278 'user', // Action.
279 '[URL]/displayer/convert/email', // URL
280 ['email' => '[EMAIL]'], // Post params.
281 '', // Request exporter.
282 'convert_email_response', // Response exporter.
283 false, // Multiple.
284 'post', // Method.
285 false, // JSON Encoded.
286 false // Auth required.
287 ];
288 foreach ($mapping as $map) {
289 $map[] = false; // Site api function.
290 $map[] = OPEN_BADGES_V1; // V1 function.
291 $this->mappings[] = new backpack_api_mapping(...$map);
292 }
293 }
294 }
295 }
296
297 /**
298 * Make an api request
299 *
300 * @param string $action The api function.
301 * @param string $collection An api parameter
302 * @param string $entityid An api parameter
303 * @param string $postdata The body of the api request.
304 * @return mixed
305 */
306 private function curl_request($action, $collection = null, $entityid = null, $postdata = null) {
307 global $CFG, $SESSION;
308
309 $curl = new curl();
310 $authrequired = false;
311 if ($this->backpackapiversion == OPEN_BADGES_V1) {
312 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
313 if (isset($SESSION->$useridkey)) {
314 if ($collection == null) {
315 $collection = $SESSION->$useridkey;
316 } else {
317 $entityid = $SESSION->$useridkey;
318 }
319 }
320 }
321 foreach ($this->mappings as $mapping) {
322 if ($mapping->is_match($action)) {
323 return $mapping->request(
324 $this->backpackapiurl,
325 $collection,
326 $entityid,
327 $this->email,
328 $this->password,
329 $postdata,
330 $this->backpackid
331 );
332 }
333 }
334
335 throw new coding_exception('Unknown request');
336 }
337
338 /**
339 * Get the id to use for requests with this api.
340 *
341 * @return integer
342 */
343 private function get_auth_user_id() {
344 global $USER;
345
346 if ($this->isuserbackpack) {
347 return $USER->id;
348 } else {
349 // The access tokens for the system backpack are shared.
350 return -1;
351 }
352 }
353
354 /**
355 * Get the name of the key to store this access token type.
356 *
357 * @param string $type
358 * @return string
359 */
360 private function get_token_key($type) {
361 // This should be removed when everything has a mapping.
362 $prefix = 'badges_';
363 if ($this->isuserbackpack) {
364 $prefix .= 'user_backpack_';
365 } else {
366 $prefix .= 'site_backpack_';
367 }
368 $prefix .= $type . '_token';
369 return $prefix;
370 }
371
372 /**
373 * Normalise the return from a missing user request.
374 *
375 * @param string $status
376 * @return mixed
377 */
378 private function check_status($status) {
379 // V1 ONLY.
380 switch($status) {
381 case "missing":
382 $response = array(
383 'status' => $status,
384 'message' => get_string('error:nosuchuser', 'badges')
385 );
386 return $response;
387 }
388 return false;
389 }
390
391 /**
392 * Make an api request to get an assertion
393 *
394 * @param string $entityid The id of the assertion.
395 * @return mixed
396 */
397 public function get_assertion($entityid) {
398 // V2 Only.
399 if ($this->backpackapiversion == OPEN_BADGES_V1) {
400 throw new coding_exception('Not supported in this backpack API');
401 }
402
403 return $this->curl_request('assertion', null, $entityid);
404 }
405
aae219ac
DW
406 /**
407 * Create a badgeclass assertion.
408 *
409 * @param string $entityid The id of the badge class.
410 * @param string $data The structure of the badge class assertion.
411 * @return mixed
412 */
413 public function put_badgeclass_assertion($entityid, $data) {
414 // V2 Only.
415 if ($this->backpackapiversion == OPEN_BADGES_V1) {
416 throw new coding_exception('Not supported in this backpack API');
417 }
418
419 return $this->curl_request('assertions', null, $entityid, $data);
420 }
421
b6435e09
P
422 /**
423 * Import a badge assertion into a backpack. This is used to handle cross domain backpacks.
424 *
425 * @param string $data The structure of the badge class assertion.
426 * @return mixed
427 * @throws coding_exception
428 */
429 public function import_badge_assertion(string $data) {
430 // V2 Only.
431 if ($this->backpackapiversion == OPEN_BADGES_V1) {
432 throw new coding_exception('Not supported in this backpack API');
433 }
434
435 return $this->curl_request('importbadge', null, null, $data);
436 }
437
aae219ac
DW
438 /**
439 * Select collections from a backpack.
440 *
441 * @param string $backpackid The id of the backpack
442 * @param stdClass[] $collections List of collections with collectionid or entityid.
443 * @return boolean
444 */
445 public function set_backpack_collections($backpackid, $collections) {
446 global $DB, $USER;
447
448 // Delete any previously selected collections.
449 $sqlparams = array('backpack' => $backpackid);
450 $select = 'backpackid = :backpack ';
451 $DB->delete_records_select('badge_external', $select, $sqlparams);
452 $badgescache = cache::make('core', 'externalbadges');
453
454 // Insert selected collections if they are not in database yet.
455 foreach ($collections as $collection) {
456 $obj = new stdClass();
457 $obj->backpackid = $backpackid;
458 if ($this->backpackapiversion == OPEN_BADGES_V1) {
459 $obj->collectionid = (int) $collection;
460 } else {
461 $obj->entityid = $collection;
462 $obj->collectionid = -1;
463 }
464 if (!$DB->record_exists('badge_external', (array) $obj)) {
465 $DB->insert_record('badge_external', $obj);
466 }
467 }
468 $badgescache->delete($USER->id);
469 return true;
470 }
471
472 /**
473 * Create a badgeclass
474 *
475 * @param string $entityid The id of the entity.
476 * @param string $data The structure of the badge class.
477 * @return mixed
478 */
479 public function put_badgeclass($entityid, $data) {
480 // V2 Only.
481 if ($this->backpackapiversion == OPEN_BADGES_V1) {
482 throw new coding_exception('Not supported in this backpack API');
483 }
484
485 return $this->curl_request('badgeclasses', null, $entityid, $data);
486 }
487
488 /**
489 * Create an issuer
490 *
491 * @param string $data The structure of the issuer.
492 * @return mixed
493 */
494 public function put_issuer($data) {
495 // V2 Only.
496 if ($this->backpackapiversion == OPEN_BADGES_V1) {
497 throw new coding_exception('Not supported in this backpack API');
498 }
499
500 return $this->curl_request('issuers', null, null, $data);
501 }
502
2fe34536
DW
503 /**
504 * Delete any user access tokens in the session so we will attempt to get new ones.
505 *
506 * @return void
507 */
508 public function clear_system_user_session() {
509 global $SESSION;
510
511 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
512 unset($SESSION->$useridkey);
513
514 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
515 unset($SESSION->$expireskey);
516 }
517
aae219ac
DW
518 /**
519 * Authenticate using the stored email and password and save the valid access tokens.
520 *
521 * @return integer The id of the authenticated user.
522 */
523 public function authenticate() {
524 global $SESSION;
525
526 $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
527 $backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0;
528 // If the backpack is changed we need to expire sessions.
529 if ($backpackid == $this->backpackid) {
530 if ($this->backpackapiversion == OPEN_BADGES_V2) {
531 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
532 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
533 if ($authuserid == $this->get_auth_user_id()) {
534 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
535 if (isset($SESSION->$expireskey)) {
536 $expires = $SESSION->$expireskey;
537 if ($expires > time()) {
538 // We have a current access token for this user
539 // that has not expired.
540 return -1;
541 }
542 }
543 }
544 } else {
545 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
546 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
547 if (!empty($authuserid)) {
548 return $authuserid;
549 }
550 }
551 }
552 return $this->curl_request('user', $this->email);
553 }
554
555 /**
556 * Get all collections in this backpack.
557 *
558 * @return stdClass[] The collections.
559 */
560 public function get_collections() {
561 global $PAGE;
562
563 if ($this->authenticate()) {
564 if ($this->backpackapiversion == OPEN_BADGES_V1) {
565 $result = $this->curl_request('groups');
566 if (isset($result->groups)) {
567 $result = $result->groups;
568 }
569 } else {
570 $result = $this->curl_request('collections');
571 }
572 if ($result) {
573 return $result;
574 }
575 }
576 return [];
577 }
578
579 /**
580 * Get one collection by id.
581 *
582 * @param integer $collectionid
583 * @return stdClass The collection.
584 */
585 public function get_collection_record($collectionid) {
586 global $DB;
587
588 if ($this->backpackapiversion == OPEN_BADGES_V1) {
589 return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid));
590 } else {
591 return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid));
592 }
593 }
594
595 /**
596 * Disconnect the backpack from this user.
597 *
598 * @param integer $userid The user in Moodle
599 * @param integer $backpackid The backpack to disconnect
600 * @return boolean
601 */
3cae9421 602 public function disconnect_backpack($userid, $backpackid) {
aae219ac
DW
603 global $DB, $USER;
604
605 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) {
606 // Can't change someone elses backpack settings.
607 return false;
608 }
609
610 $badgescache = cache::make('core', 'externalbadges');
611
612 $DB->delete_records('badge_external', array('backpackid' => $backpackid));
613 $DB->delete_records('badge_backpack', array('userid' => $userid));
614 $badgescache->delete($userid);
615 return true;
616 }
617
618 /**
619 * Handle the response from getting a collection to map to an id.
620 *
621 * @param stdClass $data The response data.
622 * @return string The collection id.
623 */
624 public function get_collection_id_from_response($data) {
625 if ($this->backpackapiversion == OPEN_BADGES_V1) {
626 return $data->groupId;
627 } else {
628 return $data->entityId;
629 }
630 }
631
7444ba74
DW
632 /**
633 * Get the last error message returned during an authentication request.
634 *
635 * @return string
636 */
637 public function get_authentication_error() {
638 return backpack_api_mapping::get_authentication_error();
639 }
640
aae219ac
DW
641 /**
642 * Get the list of badges in a collection.
643 *
644 * @param stdClass $collection The collection to deal with.
645 * @param boolean $expanded Fetch all the sub entities.
646 * @return stdClass[]
647 */
648 public function get_badges($collection, $expanded = false) {
58c7d2ce
DW
649 global $PAGE;
650
aae219ac
DW
651 if ($this->authenticate()) {
652 if ($this->backpackapiversion == OPEN_BADGES_V1) {
653 if (empty($collection->collectionid)) {
654 return [];
655 }
656 $result = $this->curl_request('badges', $collection->collectionid);
657 return $result->badges;
658 } else {
659 if (empty($collection->entityid)) {
660 return [];
661 }
662 // Now we can make requests.
663 $badges = $this->curl_request('badges', $collection->entityid);
664 if (count($badges) == 0) {
665 return [];
666 }
667 $badges = $badges[0];
668 if ($expanded) {
669 $publicassertions = [];
58c7d2ce
DW
670 $context = context_system::instance();
671 $output = $PAGE->get_renderer('core', 'badges');
aae219ac
DW
672 foreach ($badges->assertions as $assertion) {
673 $remoteassertion = $this->get_assertion($assertion);
58c7d2ce
DW
674 // Remote badge was fetched nested in the assertion.
675 $remotebadge = $remoteassertion->badgeclass;
aae219ac
DW
676 if (!$remotebadge) {
677 continue;
678 }
58c7d2ce
DW
679 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion);
680 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]);
681 $remotebadge = $exporterinstance->export($output);
682
683 $remoteissuer = $remotebadge->issuer;
684 $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion);
685 $exporterinstance = new issuer_exporter($apidata, ['context' => $context]);
686 $remoteissuer = $exporterinstance->export($output);
687
aae219ac
DW
688 $badgeclone = clone $remotebadge;
689 $badgeclone->issuer = $remoteissuer;
690 $remoteassertion->badge = $badgeclone;
691 $remotebadge->assertion = $remoteassertion;
aae219ac
DW
692 $publicassertions[] = $remotebadge;
693 }
694 $badges = $publicassertions;
695 }
696 return $badges;
697 }
698 }
699 }
700}