Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 mixed The id of the authenticated user as returned by the backpack. Can have 549 * different formats - numeric, empty, object with 'error' property, etc. 550 */ 551 public function authenticate() { 552 global $SESSION; 553 554 $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN); 555 $backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0; 556 // If the backpack is changed we need to expire sessions. 557 if ($backpackid == $this->backpackid) { 558 if ($this->backpackapiversion == OPEN_BADGES_V2) { 559 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); 560 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0; 561 if ($authuserid == $this->get_auth_user_id()) { 562 $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN); 563 if (isset($SESSION->$expireskey)) { 564 $expires = $SESSION->$expireskey; 565 if ($expires > time()) { 566 // We have a current access token for this user 567 // that has not expired. 568 return -1; 569 } 570 } 571 } 572 } else { 573 $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); 574 $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0; 575 if (!empty($authuserid)) { 576 return $authuserid; 577 } 578 } 579 } 580 return $this->curl_request('user', $this->email); 581 } 582 583 /** 584 * Get all collections in this backpack. 585 * 586 * @return stdClass[] The collections. 587 */ 588 public function get_collections() { 589 global $PAGE; 590 591 if ($this->authenticate()) { 592 if ($this->backpackapiversion == OPEN_BADGES_V1) { 593 $result = $this->curl_request('groups'); 594 if (isset($result->groups)) { 595 $result = $result->groups; 596 } 597 } else { 598 $result = $this->curl_request('collections'); 599 } 600 if ($result) { 601 return $result; 602 } 603 } 604 return []; 605 } 606 607 /** 608 * Get one collection by id. 609 * 610 * @param integer $collectionid 611 * @return stdClass The collection. 612 */ 613 public function get_collection_record($collectionid) { 614 global $DB; 615 616 if ($this->backpackapiversion == OPEN_BADGES_V1) { 617 return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid)); 618 } else { 619 return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid)); 620 } 621 } 622 623 /** 624 * Disconnect the backpack from this user. 625 * 626 * @param integer $userid The user in Moodle 627 * @param integer $backpackid The backpack to disconnect 628 * @return boolean 629 */ 630 public function disconnect_backpack($userid, $backpackid) { 631 global $DB, $USER; 632 633 if (\core\session\manager::is_loggedinas() || $userid != $USER->id) { 634 // Can't change someone elses backpack settings. 635 return false; 636 } 637 638 $badgescache = cache::make('core', 'externalbadges'); 639 640 $DB->delete_records('badge_external', array('backpackid' => $backpackid)); 641 $DB->delete_records('badge_backpack', array('userid' => $userid)); 642 $badgescache->delete($userid); 643 $this->clear_system_user_session(); 644 645 return true; 646 } 647 648 /** 649 * Handle the response from getting a collection to map to an id. 650 * 651 * @param stdClass $data The response data. 652 * @return string The collection id. 653 */ 654 public function get_collection_id_from_response($data) { 655 if ($this->backpackapiversion == OPEN_BADGES_V1) { 656 return $data->groupId; 657 } else { 658 return $data->entityId; 659 } 660 } 661 662 /** 663 * Get the last error message returned during an authentication request. 664 * 665 * @return string 666 */ 667 public function get_authentication_error() { 668 return backpack_api_mapping::get_authentication_error(); 669 } 670 671 /** 672 * Get the list of badges in a collection. 673 * 674 * @param stdClass $collection The collection to deal with. 675 * @param boolean $expanded Fetch all the sub entities. 676 * @return stdClass[] 677 */ 678 public function get_badges($collection, $expanded = false) { 679 global $PAGE; 680 681 if ($this->authenticate()) { 682 if ($this->backpackapiversion == OPEN_BADGES_V1) { 683 if (empty($collection->collectionid)) { 684 return []; 685 } 686 $result = $this->curl_request('badges', $collection->collectionid); 687 return $result->badges; 688 } else { 689 if (empty($collection->entityid)) { 690 return []; 691 } 692 // Now we can make requests. 693 $badges = $this->curl_request('badges', $collection->entityid); 694 if (count($badges) == 0) { 695 return []; 696 } 697 $badges = $badges[0]; 698 if ($expanded) { 699 $publicassertions = []; 700 $context = context_system::instance(); 701 $output = $PAGE->get_renderer('core', 'badges'); 702 foreach ($badges->assertions as $assertion) { 703 $remoteassertion = $this->get_assertion($assertion); 704 // Remote badge was fetched nested in the assertion. 705 $remotebadge = $remoteassertion->badgeclass; 706 if (!$remotebadge) { 707 continue; 708 } 709 $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion); 710 $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]); 711 $remotebadge = $exporterinstance->export($output); 712 713 $remoteissuer = $remotebadge->issuer; 714 $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion); 715 $exporterinstance = new issuer_exporter($apidata, ['context' => $context]); 716 $remoteissuer = $exporterinstance->export($output); 717 718 $badgeclone = clone $remotebadge; 719 $badgeclone->issuer = $remoteissuer; 720 $remoteassertion->badge = $badgeclone; 721 $remotebadge->assertion = $remoteassertion; 722 $publicassertions[] = $remotebadge; 723 } 724 $badges = $publicassertions; 725 } 726 return $badges; 727 } 728 } 729 } 730 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body