Merge branch 'MDL-68861-master' of git://github.com/vmdef/moodle
[moodle.git] / badges / classes / backpack_api.php
blob0916dea47524545300248de54d0849b94bf86331
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->backpackapiurl = $sitebackpack->backpackapiurl;
92 $this->backpackapiversion = $sitebackpack->apiversion;
93 $this->password = $sitebackpack->password;
94 $this->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : '';
95 $this->isuserbackpack = false;
96 $this->backpackid = $sitebackpack->id;
97 if (!empty($userbackpack)) {
98 if ($userbackpack->externalbackpackid != $sitebackpack->id) {
99 throw new coding_exception('Incorrect backpack');
101 $this->isuserbackpack = true;
102 $this->password = $userbackpack->password;
103 $this->email = $userbackpack->email;
106 $this->define_mappings();
107 // Clear the last authentication error.
108 backpack_api_mapping::set_authentication_error('');
112 * Define the mappings supported by this usage and api version.
114 private function define_mappings() {
115 if ($this->backpackapiversion == OPEN_BADGES_V2) {
116 if ($this->isuserbackpack) {
117 $mapping = [];
118 $mapping[] = [
119 'collections', // Action.
120 '[URL]/backpack/collections', // URL
121 [], // Post params.
122 '', // Request exporter.
123 'core_badges\external\collection_exporter', // Response exporter.
124 true, // Multiple.
125 'get', // Method.
126 true, // JSON Encoded.
127 true // Auth required.
129 $mapping[] = [
130 'user', // Action.
131 '[SCHEME]://[HOST]/o/token', // URL
132 ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.
133 '', // Request exporter.
134 'oauth_token_response', // Response exporter.
135 false, // Multiple.
136 'post', // Method.
137 false, // JSON Encoded.
138 false, // Auth required.
140 $mapping[] = [
141 'assertion', // Action.
142 // Badgr.io does not return the public information about a badge
143 // if the issuer is associated with another user. We need to pass
144 // the expand parameters which are not in any specification to get
145 // additional information about the assertion in a single request.
146 '[URL]/backpack/assertions/[PARAM2]?expand=badgeclass&expand=issuer',
147 [], // Post params.
148 '', // Request exporter.
149 'core_badges\external\assertion_exporter', // Response exporter.
150 false, // Multiple.
151 'get', // Method.
152 true, // JSON Encoded.
153 true // Auth required.
155 $mapping[] = [
156 'badges', // Action.
157 '[URL]/backpack/collections/[PARAM1]', // URL
158 [], // Post params.
159 '', // Request exporter.
160 'core_badges\external\collection_exporter', // Response exporter.
161 true, // Multiple.
162 'get', // Method.
163 true, // JSON Encoded.
164 true // Auth required.
166 foreach ($mapping as $map) {
167 $map[] = true; // User api function.
168 $map[] = OPEN_BADGES_V2; // V2 function.
169 $this->mappings[] = new backpack_api_mapping(...$map);
171 } else {
172 $mapping = [];
173 $mapping[] = [
174 'user', // Action.
175 '[SCHEME]://[HOST]/o/token', // URL
176 ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.
177 '', // Request exporter.
178 'oauth_token_response', // Response exporter.
179 false, // Multiple.
180 'post', // Method.
181 false, // JSON Encoded.
182 false // Auth required.
184 $mapping[] = [
185 'issuers', // Action.
186 '[URL]/issuers', // URL
187 '[PARAM]', // Post params.
188 'core_badges\external\issuer_exporter', // Request exporter.
189 'core_badges\external\issuer_exporter', // Response exporter.
190 false, // Multiple.
191 'post', // Method.
192 true, // JSON Encoded.
193 true // Auth required.
195 $mapping[] = [
196 'badgeclasses', // Action.
197 '[URL]/issuers/[PARAM2]/badgeclasses', // URL
198 '[PARAM]', // Post params.
199 'core_badges\external\badgeclass_exporter', // Request exporter.
200 'core_badges\external\badgeclass_exporter', // Response exporter.
201 false, // Multiple.
202 'post', // Method.
203 true, // JSON Encoded.
204 true // Auth required.
206 $mapping[] = [
207 'assertions', // Action.
208 '[URL]/badgeclasses/[PARAM2]/assertions', // URL
209 '[PARAM]', // Post params.
210 'core_badges\external\assertion_exporter', // Request exporter.
211 'core_badges\external\assertion_exporter', // Response exporter.
212 false, // Multiple.
213 'post', // Method.
214 true, // JSON Encoded.
215 true // Auth required.
217 foreach ($mapping as $map) {
218 $map[] = false; // Site api function.
219 $map[] = OPEN_BADGES_V2; // V2 function.
220 $this->mappings[] = new backpack_api_mapping(...$map);
223 } else {
224 if ($this->isuserbackpack) {
225 $mapping = [];
226 $mapping[] = [
227 'user', // Action.
228 '[URL]/displayer/convert/email', // URL
229 ['email' => '[EMAIL]'], // Post params.
230 '', // Request exporter.
231 'convert_email_response', // Response exporter.
232 false, // Multiple.
233 'post', // Method.
234 false, // JSON Encoded.
235 false // Auth required.
237 $mapping[] = [
238 'groups', // Action.
239 '[URL]/displayer/[PARAM1]/groups.json', // URL
240 [], // Post params.
241 '', // Request exporter.
242 '', // Response exporter.
243 false, // Multiple.
244 'get', // Method.
245 true, // JSON Encoded.
246 true // Auth required.
248 $mapping[] = [
249 'badges', // Action.
250 '[URL]/displayer/[PARAM2]/group/[PARAM1].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.
259 foreach ($mapping as $map) {
260 $map[] = true; // User api function.
261 $map[] = OPEN_BADGES_V1; // V1 function.
262 $this->mappings[] = new backpack_api_mapping(...$map);
264 } else {
265 $mapping = [];
266 $mapping[] = [
267 'user', // Action.
268 '[URL]/displayer/convert/email', // URL
269 ['email' => '[EMAIL]'], // Post params.
270 '', // Request exporter.
271 'convert_email_response', // Response exporter.
272 false, // Multiple.
273 'post', // Method.
274 false, // JSON Encoded.
275 false // Auth required.
277 foreach ($mapping as $map) {
278 $map[] = false; // Site api function.
279 $map[] = OPEN_BADGES_V1; // V1 function.
280 $this->mappings[] = new backpack_api_mapping(...$map);
287 * Make an api request
289 * @param string $action The api function.
290 * @param string $collection An api parameter
291 * @param string $entityid An api parameter
292 * @param string $postdata The body of the api request.
293 * @return mixed
295 private function curl_request($action, $collection = null, $entityid = null, $postdata = null) {
296 global $CFG, $SESSION;
298 $curl = new curl();
299 $authrequired = false;
300 if ($this->backpackapiversion == OPEN_BADGES_V1) {
301 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
302 if (isset($SESSION->$useridkey)) {
303 if ($collection == null) {
304 $collection = $SESSION->$useridkey;
305 } else {
306 $entityid = $SESSION->$useridkey;
310 foreach ($this->mappings as $mapping) {
311 if ($mapping->is_match($action)) {
312 return $mapping->request(
313 $this->backpackapiurl,
314 $collection,
315 $entityid,
316 $this->email,
317 $this->password,
318 $postdata,
319 $this->backpackid
324 throw new coding_exception('Unknown request');
328 * Get the id to use for requests with this api.
330 * @return integer
332 private function get_auth_user_id() {
333 global $USER;
335 if ($this->isuserbackpack) {
336 return $USER->id;
337 } else {
338 // The access tokens for the system backpack are shared.
339 return -1;
344 * Get the name of the key to store this access token type.
346 * @param string $type
347 * @return string
349 private function get_token_key($type) {
350 // This should be removed when everything has a mapping.
351 $prefix = 'badges_';
352 if ($this->isuserbackpack) {
353 $prefix .= 'user_backpack_';
354 } else {
355 $prefix .= 'site_backpack_';
357 $prefix .= $type . '_token';
358 return $prefix;
362 * Normalise the return from a missing user request.
364 * @param string $status
365 * @return mixed
367 private function check_status($status) {
368 // V1 ONLY.
369 switch($status) {
370 case "missing":
371 $response = array(
372 'status' => $status,
373 'message' => get_string('error:nosuchuser', 'badges')
375 return $response;
377 return false;
381 * Make an api request to get an assertion
383 * @param string $entityid The id of the assertion.
384 * @return mixed
386 public function get_assertion($entityid) {
387 // V2 Only.
388 if ($this->backpackapiversion == OPEN_BADGES_V1) {
389 throw new coding_exception('Not supported in this backpack API');
392 return $this->curl_request('assertion', null, $entityid);
396 * Create a badgeclass assertion.
398 * @param string $entityid The id of the badge class.
399 * @param string $data The structure of the badge class assertion.
400 * @return mixed
402 public function put_badgeclass_assertion($entityid, $data) {
403 // V2 Only.
404 if ($this->backpackapiversion == OPEN_BADGES_V1) {
405 throw new coding_exception('Not supported in this backpack API');
408 return $this->curl_request('assertions', null, $entityid, $data);
412 * Select collections from a backpack.
414 * @param string $backpackid The id of the backpack
415 * @param stdClass[] $collections List of collections with collectionid or entityid.
416 * @return boolean
418 public function set_backpack_collections($backpackid, $collections) {
419 global $DB, $USER;
421 // Delete any previously selected collections.
422 $sqlparams = array('backpack' => $backpackid);
423 $select = 'backpackid = :backpack ';
424 $DB->delete_records_select('badge_external', $select, $sqlparams);
425 $badgescache = cache::make('core', 'externalbadges');
427 // Insert selected collections if they are not in database yet.
428 foreach ($collections as $collection) {
429 $obj = new stdClass();
430 $obj->backpackid = $backpackid;
431 if ($this->backpackapiversion == OPEN_BADGES_V1) {
432 $obj->collectionid = (int) $collection;
433 } else {
434 $obj->entityid = $collection;
435 $obj->collectionid = -1;
437 if (!$DB->record_exists('badge_external', (array) $obj)) {
438 $DB->insert_record('badge_external', $obj);
441 $badgescache->delete($USER->id);
442 return true;
446 * Create a badgeclass
448 * @param string $entityid The id of the entity.
449 * @param string $data The structure of the badge class.
450 * @return mixed
452 public function put_badgeclass($entityid, $data) {
453 // V2 Only.
454 if ($this->backpackapiversion == OPEN_BADGES_V1) {
455 throw new coding_exception('Not supported in this backpack API');
458 return $this->curl_request('badgeclasses', null, $entityid, $data);
462 * Create an issuer
464 * @param string $data The structure of the issuer.
465 * @return mixed
467 public function put_issuer($data) {
468 // V2 Only.
469 if ($this->backpackapiversion == OPEN_BADGES_V1) {
470 throw new coding_exception('Not supported in this backpack API');
473 return $this->curl_request('issuers', null, null, $data);
477 * Delete any user access tokens in the session so we will attempt to get new ones.
479 * @return void
481 public function clear_system_user_session() {
482 global $SESSION;
484 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
485 unset($SESSION->$useridkey);
487 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
488 unset($SESSION->$expireskey);
492 * Authenticate using the stored email and password and save the valid access tokens.
494 * @return integer The id of the authenticated user.
496 public function authenticate() {
497 global $SESSION;
499 $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
500 $backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0;
501 // If the backpack is changed we need to expire sessions.
502 if ($backpackid == $this->backpackid) {
503 if ($this->backpackapiversion == OPEN_BADGES_V2) {
504 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
505 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
506 if ($authuserid == $this->get_auth_user_id()) {
507 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
508 if (isset($SESSION->$expireskey)) {
509 $expires = $SESSION->$expireskey;
510 if ($expires > time()) {
511 // We have a current access token for this user
512 // that has not expired.
513 return -1;
517 } else {
518 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
519 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;
520 if (!empty($authuserid)) {
521 return $authuserid;
525 return $this->curl_request('user', $this->email);
529 * Get all collections in this backpack.
531 * @return stdClass[] The collections.
533 public function get_collections() {
534 global $PAGE;
536 if ($this->authenticate()) {
537 if ($this->backpackapiversion == OPEN_BADGES_V1) {
538 $result = $this->curl_request('groups');
539 if (isset($result->groups)) {
540 $result = $result->groups;
542 } else {
543 $result = $this->curl_request('collections');
545 if ($result) {
546 return $result;
549 return [];
553 * Get one collection by id.
555 * @param integer $collectionid
556 * @return stdClass The collection.
558 public function get_collection_record($collectionid) {
559 global $DB;
561 if ($this->backpackapiversion == OPEN_BADGES_V1) {
562 return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid));
563 } else {
564 return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid));
569 * Disconnect the backpack from this user.
571 * @param integer $userid The user in Moodle
572 * @param integer $backpackid The backpack to disconnect
573 * @return boolean
575 public function disconnect_backpack($userid, $backpackid) {
576 global $DB, $USER;
578 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) {
579 // Can't change someone elses backpack settings.
580 return false;
583 $badgescache = cache::make('core', 'externalbadges');
585 $DB->delete_records('badge_external', array('backpackid' => $backpackid));
586 $DB->delete_records('badge_backpack', array('userid' => $userid));
587 $badgescache->delete($userid);
588 return true;
592 * Handle the response from getting a collection to map to an id.
594 * @param stdClass $data The response data.
595 * @return string The collection id.
597 public function get_collection_id_from_response($data) {
598 if ($this->backpackapiversion == OPEN_BADGES_V1) {
599 return $data->groupId;
600 } else {
601 return $data->entityId;
606 * Get the last error message returned during an authentication request.
608 * @return string
610 public function get_authentication_error() {
611 return backpack_api_mapping::get_authentication_error();
615 * Get the list of badges in a collection.
617 * @param stdClass $collection The collection to deal with.
618 * @param boolean $expanded Fetch all the sub entities.
619 * @return stdClass[]
621 public function get_badges($collection, $expanded = false) {
622 global $PAGE;
624 if ($this->authenticate()) {
625 if ($this->backpackapiversion == OPEN_BADGES_V1) {
626 if (empty($collection->collectionid)) {
627 return [];
629 $result = $this->curl_request('badges', $collection->collectionid);
630 return $result->badges;
631 } else {
632 if (empty($collection->entityid)) {
633 return [];
635 // Now we can make requests.
636 $badges = $this->curl_request('badges', $collection->entityid);
637 if (count($badges) == 0) {
638 return [];
640 $badges = $badges[0];
641 if ($expanded) {
642 $publicassertions = [];
643 $context = context_system::instance();
644 $output = $PAGE->get_renderer('core', 'badges');
645 foreach ($badges->assertions as $assertion) {
646 $remoteassertion = $this->get_assertion($assertion);
647 // Remote badge was fetched nested in the assertion.
648 $remotebadge = $remoteassertion->badgeclass;
649 if (!$remotebadge) {
650 continue;
652 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion);
653 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]);
654 $remotebadge = $exporterinstance->export($output);
656 $remoteissuer = $remotebadge->issuer;
657 $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion);
658 $exporterinstance = new issuer_exporter($apidata, ['context' => $context]);
659 $remoteissuer = $exporterinstance->export($output);
661 $badgeclone = clone $remotebadge;
662 $badgeclone->issuer = $remoteissuer;
663 $remoteassertion->badge = $badgeclone;
664 $remotebadge->assertion = $remoteassertion;
665 $publicassertions[] = $remotebadge;
667 $badges = $publicassertions;
669 return $badges;