See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 25 namespace core_badges; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 require_once($CFG->libdir . '/filelib.php'); 30 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; 40 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'); 46 47 /** 48 * Class for communicating with backpacks. 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 53 */ 54 class 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 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'); 100 } 101 $this->isuserbackpack = true; 102 $this->password = $userbackpack->password; 103 $this->email = $userbackpack->email; 104 } 105 106 $this->define_mappings(); 107 // Clear the last authentication error. 108 backpack_api_mapping::set_authentication_error(''); 109 } 110 111 /** 112 * Define the mappings supported by this usage and api version. 113 */ 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. 128 ]; 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. 139 ]; 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. 154 ]; 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. 165 ]; 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); 170 } 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. 183 ]; 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. 194 ]; 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. 205 ]; 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. 216 ]; 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); 221 } 222 } 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. 236 ]; 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. 247 ]; 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. 258 ]; 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); 263 } 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. 276 ]; 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); 281 } 282 } 283 } 284 } 285 286 /** 287 * Make an api request 288 * 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 294 */ 295 private function curl_request($action, $collection = null, $entityid = null, $postdata = null) { 296 global $CFG, $SESSION; 297 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; 307 } 308 } 309 } 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 320 ); 321 } 322 } 323 324 throw new coding_exception('Unknown request'); 325 } 326 327 /** 328 * Get the id to use for requests with this api. 329 * 330 * @return integer 331 */ 332 private function get_auth_user_id() { 333 global $USER; 334 335 if ($this->isuserbackpack) { 336 return $USER->id; 337 } else { 338 // The access tokens for the system backpack are shared. 339 return -1; 340 } 341 } 342 343 /** 344 * Get the name of the key to store this access token type. 345 * 346 * @param string $type 347 * @return string 348 */ 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_'; 356 } 357 $prefix .= $type . '_token'; 358 return $prefix; 359 } 360 361 /** 362 * Normalise the return from a missing user request. 363 * 364 * @param string $status 365 * @return mixed 366 */ 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') 374 ); 375 return $response; 376 } 377 return false; 378 } 379 380 /** 381 * Make an api request to get an assertion 382 * 383 * @param string $entityid The id of the assertion. 384 * @return mixed 385 */ 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'); 390 } 391 392 return $this->curl_request('assertion', null, $entityid); 393 } 394 395 /** 396 * Create a badgeclass assertion. 397 * 398 * @param string $entityid The id of the badge class. 399 * @param string $data The structure of the badge class assertion. 400 * @return mixed 401 */ 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'); 406 } 407 408 return $this->curl_request('assertions', null, $entityid, $data); 409 } 410 411 /** 412 * Select collections from a backpack. 413 * 414 * @param string $backpackid The id of the backpack 415 * @param stdClass[] $collections List of collections with collectionid or entityid. 416 * @return boolean 417 */ 418 public function set_backpack_collections($backpackid, $collections) { 419 global $DB, $USER; 420 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'); 426 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; 436 } 437 if (!$DB->record_exists('badge_external', (array) $obj)) { 438 $DB->insert_record('badge_external', $obj); 439 } 440 } 441 $badgescache->delete($USER->id); 442 return true; 443 } 444 445 /** 446 * Create a badgeclass 447 * 448 * @param string $entityid The id of the entity. 449 * @param string $data The structure of the badge class. 450 * @return mixed 451 */ 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'); 456 } 457 458 return $this->curl_request('badgeclasses', null, $entityid, $data); 459 } 460 461 /** 462 * Create an issuer 463 * 464 * @param string $data The structure of the issuer. 465 * @return mixed 466 */ 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'); 471 } 472 473 return $this->curl_request('issuers', null, null, $data); 474 } 475 476 /** 477 * Delete any user access tokens in the session so we will attempt to get new ones. 478 * 479 * @return void 480 */ 481 public function clear_system_user_session() { 482 global $SESSION; 483 484 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); 485 unset($SESSION->$useridkey); 486 487 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN); 488 unset($SESSION->$expireskey); 489 } 490 491 /** 492 * Authenticate using the stored email and password and save the valid access tokens. 493 * 494 * @return integer The id of the authenticated user. 495 */ 496 public function authenticate() { 497 global $SESSION; 498 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; 514 } 515 } 516 } 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; 522 } 523 } 524 } 525 return $this->curl_request('user', $this->email); 526 } 527 528 /** 529 * Get all collections in this backpack. 530 * 531 * @return stdClass[] The collections. 532 */ 533 public function get_collections() { 534 global $PAGE; 535 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; 541 } 542 } else { 543 $result = $this->curl_request('collections'); 544 } 545 if ($result) { 546 return $result; 547 } 548 } 549 return []; 550 } 551 552 /** 553 * Get one collection by id. 554 * 555 * @param integer $collectionid 556 * @return stdClass The collection. 557 */ 558 public function get_collection_record($collectionid) { 559 global $DB; 560 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)); 565 } 566 } 567 568 /** 569 * Disconnect the backpack from this user. 570 * 571 * @param integer $userid The user in Moodle 572 * @param integer $backpackid The backpack to disconnect 573 * @return boolean 574 */ 575 public function disconnect_backpack($userid, $backpackid) { 576 global $DB, $USER; 577 578 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) { 579 // Can't change someone elses backpack settings. 580 return false; 581 } 582 583 $badgescache = cache::make('core', 'externalbadges'); 584 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; 589 } 590 591 /** 592 * Handle the response from getting a collection to map to an id. 593 * 594 * @param stdClass $data The response data. 595 * @return string The collection id. 596 */ 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; 602 } 603 } 604 605 /** 606 * Get the last error message returned during an authentication request. 607 * 608 * @return string 609 */ 610 public function get_authentication_error() { 611 return backpack_api_mapping::get_authentication_error(); 612 } 613 614 /** 615 * Get the list of badges in a collection. 616 * 617 * @param stdClass $collection The collection to deal with. 618 * @param boolean $expanded Fetch all the sub entities. 619 * @return stdClass[] 620 */ 621 public function get_badges($collection, $expanded = false) { 622 global $PAGE; 623 624 if ($this->authenticate()) { 625 if ($this->backpackapiversion == OPEN_BADGES_V1) { 626 if (empty($collection->collectionid)) { 627 return []; 628 } 629 $result = $this->curl_request('badges', $collection->collectionid); 630 return $result->badges; 631 } else { 632 if (empty($collection->entityid)) { 633 return []; 634 } 635 // Now we can make requests. 636 $badges = $this->curl_request('badges', $collection->entityid); 637 if (count($badges) == 0) { 638 return []; 639 } 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; 651 } 652 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion); 653 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]); 654 $remotebadge = $exporterinstance->export($output); 655 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); 660 661 $badgeclone = clone $remotebadge; 662 $badgeclone->issuer = $remoteissuer; 663 $remoteassertion->badge = $badgeclone; 664 $remotebadge->assertion = $remoteassertion; 665 $publicassertions[] = $remotebadge; 666 } 667 $badges = $publicassertions; 668 } 669 return $badges; 670 } 671 } 672 } 673 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body