Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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->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; 100 } 101 102 $this->define_mappings(); 103 // Clear the last authentication error. 104 backpack_api_mapping::set_authentication_error(''); 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 ]; 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. 150 ]; 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 ]; 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. 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. 227 ]; 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. 238 ]; 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); 243 } 244 } 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. 258 ]; 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. 269 ]; 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. 280 ]; 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); 285 } 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. 298 ]; 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); 303 } 304 } 305 } 306 } 307 308 /** 309 * Make an api request 310 * 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 316 */ 317 private function curl_request($action, $collection = null, $entityid = null, $postdata = null) { 318 global $CFG, $SESSION; 319 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; 329 } 330 } 331 } 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 342 ); 343 } 344 } 345 346 throw new coding_exception('Unknown request'); 347 } 348 349 /** 350 * Get the id to use for requests with this api. 351 * 352 * @return integer 353 */ 354 private function get_auth_user_id() { 355 global $USER; 356 357 if ($this->isuserbackpack) { 358 return $USER->id; 359 } else { 360 // The access tokens for the system backpack are shared. 361 return -1; 362 } 363 } 364 365 /** 366 * Get the name of the key to store this access token type. 367 * 368 * @param string $type 369 * @return string 370 */ 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_'; 378 } 379 $prefix .= $type . '_token'; 380 return $prefix; 381 } 382 383 /** 384 * Normalise the return from a missing user request. 385 * 386 * @param string $status 387 * @return mixed 388 */ 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') 396 ); 397 return $response; 398 } 399 return false; 400 } 401 402 /** 403 * Make an api request to get an assertion 404 * 405 * @param string $entityid The id of the assertion. 406 * @return mixed 407 */ 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'); 412 } 413 414 return $this->curl_request('assertion', null, $entityid); 415 } 416 417 /** 418 * Create a badgeclass assertion. 419 * 420 * @param string $entityid The id of the badge class. 421 * @param string $data The structure of the badge class assertion. 422 * @return mixed 423 */ 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'); 428 } 429 430 return $this->curl_request('assertions', null, $entityid, $data); 431 } 432 433 /** 434 * Update a badgeclass assertion. 435 * 436 * @param string $entityid The id of the badge class. 437 * @param array $data The structure of the badge class assertion. 438 * @return mixed 439 */ 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'); 444 } 445 446 return $this->curl_request('updateassertion', null, $entityid, $data); 447 } 448 449 /** 450 * Import a badge assertion into a backpack. This is used to handle cross domain backpacks. 451 * 452 * @param string $data The structure of the badge class assertion. 453 * @return mixed 454 * @throws coding_exception 455 */ 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'); 460 } 461 462 return $this->curl_request('importbadge', null, null, $data); 463 } 464 465 /** 466 * Select collections from a backpack. 467 * 468 * @param string $backpackid The id of the backpack 469 * @param stdClass[] $collections List of collections with collectionid or entityid. 470 * @return boolean 471 */ 472 public function set_backpack_collections($backpackid, $collections) { 473 global $DB, $USER; 474 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'); 480 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; 490 } 491 if (!$DB->record_exists('badge_external', (array) $obj)) { 492 $DB->insert_record('badge_external', $obj); 493 } 494 } 495 $badgescache->delete($USER->id); 496 return true; 497 } 498 499 /** 500 * Create a badgeclass 501 * 502 * @param string $entityid The id of the entity. 503 * @param string $data The structure of the badge class. 504 * @return mixed 505 */ 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'); 510 } 511 512 return $this->curl_request('badgeclasses', null, $entityid, $data); 513 } 514 515 /** 516 * Create an issuer 517 * 518 * @param string $data The structure of the issuer. 519 * @return mixed 520 */ 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'); 525 } 526 527 return $this->curl_request('issuers', null, null, $data); 528 } 529 530 /** 531 * Delete any user access tokens in the session so we will attempt to get new ones. 532 * 533 * @return void 534 */ 535 public function clear_system_user_session() { 536 global $SESSION; 537 538 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); 539 unset($SESSION->$useridkey); 540 541 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN); 542 unset($SESSION->$expireskey); 543 } 544 545 /** 546 * Authenticate using the stored email and password and save the valid access tokens. 547 * 548 * @return integer The id of the authenticated user. 549 */ 550 public function authenticate() { 551 global $SESSION; 552 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; 568 } 569 } 570 } 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; 576 } 577 } 578 } 579 return $this->curl_request('user', $this->email); 580 } 581 582 /** 583 * Get all collections in this backpack. 584 * 585 * @return stdClass[] The collections. 586 */ 587 public function get_collections() { 588 global $PAGE; 589 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; 595 } 596 } else { 597 $result = $this->curl_request('collections'); 598 } 599 if ($result) { 600 return $result; 601 } 602 } 603 return []; 604 } 605 606 /** 607 * Get one collection by id. 608 * 609 * @param integer $collectionid 610 * @return stdClass The collection. 611 */ 612 public function get_collection_record($collectionid) { 613 global $DB; 614 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)); 619 } 620 } 621 622 /** 623 * Disconnect the backpack from this user. 624 * 625 * @param integer $userid The user in Moodle 626 * @param integer $backpackid The backpack to disconnect 627 * @return boolean 628 */ 629 public function disconnect_backpack($userid, $backpackid) { 630 global $DB, $USER; 631 632 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) { 633 // Can't change someone elses backpack settings. 634 return false; 635 } 636 637 $badgescache = cache::make('core', 'externalbadges'); 638 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; 643 } 644 645 /** 646 * Handle the response from getting a collection to map to an id. 647 * 648 * @param stdClass $data The response data. 649 * @return string The collection id. 650 */ 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; 656 } 657 } 658 659 /** 660 * Get the last error message returned during an authentication request. 661 * 662 * @return string 663 */ 664 public function get_authentication_error() { 665 return backpack_api_mapping::get_authentication_error(); 666 } 667 668 /** 669 * Get the list of badges in a collection. 670 * 671 * @param stdClass $collection The collection to deal with. 672 * @param boolean $expanded Fetch all the sub entities. 673 * @return stdClass[] 674 */ 675 public function get_badges($collection, $expanded = false) { 676 global $PAGE; 677 678 if ($this->authenticate()) { 679 if ($this->backpackapiversion == OPEN_BADGES_V1) { 680 if (empty($collection->collectionid)) { 681 return []; 682 } 683 $result = $this->curl_request('badges', $collection->collectionid); 684 return $result->badges; 685 } else { 686 if (empty($collection->entityid)) { 687 return []; 688 } 689 // Now we can make requests. 690 $badges = $this->curl_request('badges', $collection->entityid); 691 if (count($badges) == 0) { 692 return []; 693 } 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; 705 } 706 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion); 707 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]); 708 $remotebadge = $exporterinstance->export($output); 709 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); 714 715 $badgeclone = clone $remotebadge; 716 $badgeclone->issuer = $remoteissuer; 717 $remoteassertion->badge = $badgeclone; 718 $remotebadge->assertion = $remoteassertion; 719 $publicassertions[] = $remotebadge; 720 } 721 $badges = $publicassertions; 722 } 723 return $badges; 724 } 725 } 726 } 727 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body