Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Communicate with backpacks.** @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @author Yuliya Bozhko <yuliya.bozhko@totaralms.com>*/namespace core_badges;defined('MOODLE_INTERNAL') || die();require_once($CFG->libdir . '/filelib.php');use cache;use coding_exception;use core_badges\external\assertion_exporter;use core_badges\external\collection_exporter;use core_badges\external\issuer_exporter;use core_badges\external\badgeclass_exporter;use curl;use stdClass;use context_system;define('BADGE_ACCESS_TOKEN', 'access');define('BADGE_USER_ID_TOKEN', 'user_id');define('BADGE_BACKPACK_ID_TOKEN', 'backpack_id');define('BADGE_REFRESH_TOKEN', 'refresh');define('BADGE_EXPIRES_TOKEN', 'expires');/*** Class for communicating with backpacks.** @package core_badges* @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class backpack_api {/** @var string The email address of the issuer or the backpack owner. */private $email;/** @var string The base url used for api requests to this backpack. */private $backpackapiurl;/** @var integer The backpack api version to use. */private $backpackapiversion;/** @var string The password to authenticate requests. */private $password;/** @var boolean User or site api requests. */private $isuserbackpack;/** @var integer The id of the backpack we are talking to. */private $backpackid;/** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */private $mappings = [];/*** Create a wrapper to communicate with the backpack.** The resulting class can only do either site backpack communication or* user backpack communication.** @param stdClass $sitebackpack The site backpack record* @param mixed $userbackpack Optional - if passed it represents the users backpack.*/public function __construct($sitebackpack, $userbackpack = false) {global $CFG;$admin = get_admin();$this->backpackapiurl = $sitebackpack->backpackapiurl;$this->backpackapiversion = $sitebackpack->apiversion;$this->password = $sitebackpack->password;$this->email = $sitebackpack->backpackemail;$this->isuserbackpack = false;$this->backpackid = $sitebackpack->id;if (!empty($userbackpack)) {$this->isuserbackpack = true;$this->password = $userbackpack->password;$this->email = $userbackpack->email;}$this->define_mappings();// Clear the last authentication error.backpack_api_mapping::set_authentication_error('');}/*** Define the mappings supported by this usage and api version.*/private function define_mappings() {if ($this->backpackapiversion == OPEN_BADGES_V2) {if ($this->isuserbackpack) {$mapping = [];$mapping[] = ['collections', // Action.'[URL]/backpack/collections', // URL[], // Post params.'', // Request exporter.'core_badges\external\collection_exporter', // Response exporter.true, // Multiple.'get', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['user', // Action.'[SCHEME]://[HOST]/o/token', // URL['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.'', // Request exporter.'oauth_token_response', // Response exporter.false, // Multiple.'post', // Method.false, // JSON Encoded.false, // Auth required.];$mapping[] = ['assertion', // Action.// Badgr.io does not return the public information about a badge// if the issuer is associated with another user. We need to pass// the expand parameters which are not in any specification to get// additional information about the assertion in a single request.'[URL]/backpack/assertions/[PARAM2]?expand=badgeclass&expand=issuer',[], // Post params.'', // Request exporter.'core_badges\external\assertion_exporter', // Response exporter.false, // Multiple.'get', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['importbadge', // Action.// Badgr.io does not return the public information about a badge// if the issuer is associated with another user. We need to pass// the expand parameters which are not in any specification to get// additional information about the assertion in a single request.'[URL]/backpack/import',['url' => '[PARAM]'], // Post params.'', // Request exporter.'core_badges\external\assertion_exporter', // Response exporter.false, // Multiple.'post', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['badges', // Action.'[URL]/backpack/collections/[PARAM1]', // URL[], // Post params.'', // Request exporter.'core_badges\external\collection_exporter', // Response exporter.true, // Multiple.'get', // Method.true, // JSON Encoded.true // Auth required.];foreach ($mapping as $map) {$map[] = true; // User api function.$map[] = OPEN_BADGES_V2; // V2 function.$this->mappings[] = new backpack_api_mapping(...$map);}} else {$mapping = [];$mapping[] = ['user', // Action.'[SCHEME]://[HOST]/o/token', // URL['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params.'', // Request exporter.'oauth_token_response', // Response exporter.false, // Multiple.'post', // Method.false, // JSON Encoded.false // Auth required.];$mapping[] = ['issuers', // Action.'[URL]/issuers', // URL'[PARAM]', // Post params.'core_badges\external\issuer_exporter', // Request exporter.'core_badges\external\issuer_exporter', // Response exporter.false, // Multiple.'post', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['badgeclasses', // Action.'[URL]/issuers/[PARAM2]/badgeclasses', // URL'[PARAM]', // Post params.'core_badges\external\badgeclass_exporter', // Request exporter.'core_badges\external\badgeclass_exporter', // Response exporter.false, // Multiple.'post', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['assertions', // Action.'[URL]/badgeclasses/[PARAM2]/assertions', // URL'[PARAM]', // Post params.'core_badges\external\assertion_exporter', // Request exporter.'core_badges\external\assertion_exporter', // Response exporter.false, // Multiple.'post', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['updateassertion', // Action.'[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer','[PARAM]', // Post params.'core_badges\external\assertion_exporter', // Request exporter.'core_badges\external\assertion_exporter', // Response exporter.false, // Multiple.'put', // Method.true, // JSON Encoded.true // Auth required.];foreach ($mapping as $map) {$map[] = false; // Site api function.$map[] = OPEN_BADGES_V2; // V2 function.$this->mappings[] = new backpack_api_mapping(...$map);}}} else {if ($this->isuserbackpack) {$mapping = [];$mapping[] = ['user', // Action.'[URL]/displayer/convert/email', // URL['email' => '[EMAIL]'], // Post params.'', // Request exporter.'convert_email_response', // Response exporter.false, // Multiple.'post', // Method.false, // JSON Encoded.false // Auth required.];$mapping[] = ['groups', // Action.'[URL]/displayer/[PARAM1]/groups.json', // URL[], // Post params.'', // Request exporter.'', // Response exporter.false, // Multiple.'get', // Method.true, // JSON Encoded.true // Auth required.];$mapping[] = ['badges', // Action.'[URL]/displayer/[PARAM2]/group/[PARAM1].json', // URL[], // Post params.'', // Request exporter.'', // Response exporter.false, // Multiple.'get', // Method.true, // JSON Encoded.true // Auth required.];foreach ($mapping as $map) {$map[] = true; // User api function.$map[] = OPEN_BADGES_V1; // V1 function.$this->mappings[] = new backpack_api_mapping(...$map);}} else {$mapping = [];$mapping[] = ['user', // Action.'[URL]/displayer/convert/email', // URL['email' => '[EMAIL]'], // Post params.'', // Request exporter.'convert_email_response', // Response exporter.false, // Multiple.'post', // Method.false, // JSON Encoded.false // Auth required.];foreach ($mapping as $map) {$map[] = false; // Site api function.$map[] = OPEN_BADGES_V1; // V1 function.$this->mappings[] = new backpack_api_mapping(...$map);}}}}/*** Make an api request** @param string $action The api function.* @param string $collection An api parameter* @param string $entityid An api parameter* @param string $postdata The body of the api request.* @return mixed*/private function curl_request($action, $collection = null, $entityid = null, $postdata = null) {global $CFG, $SESSION;$curl = new curl();$authrequired = false;if ($this->backpackapiversion == OPEN_BADGES_V1) {$useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);if (isset($SESSION->$useridkey)) {if ($collection == null) {$collection = $SESSION->$useridkey;} else {$entityid = $SESSION->$useridkey;}}}foreach ($this->mappings as $mapping) {if ($mapping->is_match($action)) {return $mapping->request($this->backpackapiurl,$collection,$entityid,$this->email,$this->password,$postdata,$this->backpackid);}}throw new coding_exception('Unknown request');}/*** Get the id to use for requests with this api.** @return integer*/private function get_auth_user_id() {global $USER;if ($this->isuserbackpack) {return $USER->id;} else {// The access tokens for the system backpack are shared.return -1;}}/*** Get the name of the key to store this access token type.** @param string $type* @return string*/private function get_token_key($type) {// This should be removed when everything has a mapping.$prefix = 'badges_';if ($this->isuserbackpack) {$prefix .= 'user_backpack_';} else {$prefix .= 'site_backpack_';}$prefix .= $type . '_token';return $prefix;}/*** Normalise the return from a missing user request.** @param string $status* @return mixed*/private function check_status($status) {// V1 ONLY.switch($status) {case "missing":$response = array('status' => $status,'message' => get_string('error:nosuchuser', 'badges'));return $response;}return false;}/*** Make an api request to get an assertion** @param string $entityid The id of the assertion.* @return mixed*/public function get_assertion($entityid) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('assertion', null, $entityid);}/*** Create a badgeclass assertion.** @param string $entityid The id of the badge class.* @param string $data The structure of the badge class assertion.* @return mixed*/public function put_badgeclass_assertion($entityid, $data) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('assertions', null, $entityid, $data);}/*** Update a badgeclass assertion.** @param string $entityid The id of the badge class.* @param array $data The structure of the badge class assertion.* @return mixed*/public function update_assertion(string $entityid, array $data) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('updateassertion', null, $entityid, $data);}/*** Import a badge assertion into a backpack. This is used to handle cross domain backpacks.** @param string $data The structure of the badge class assertion.* @return mixed* @throws coding_exception*/public function import_badge_assertion(string $data) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('importbadge', null, null, $data);}/*** Select collections from a backpack.** @param string $backpackid The id of the backpack* @param stdClass[] $collections List of collections with collectionid or entityid.* @return boolean*/public function set_backpack_collections($backpackid, $collections) {global $DB, $USER;// Delete any previously selected collections.$sqlparams = array('backpack' => $backpackid);$select = 'backpackid = :backpack ';$DB->delete_records_select('badge_external', $select, $sqlparams);$badgescache = cache::make('core', 'externalbadges');// Insert selected collections if they are not in database yet.foreach ($collections as $collection) {$obj = new stdClass();$obj->backpackid = $backpackid;if ($this->backpackapiversion == OPEN_BADGES_V1) {$obj->collectionid = (int) $collection;} else {$obj->entityid = $collection;$obj->collectionid = -1;}if (!$DB->record_exists('badge_external', (array) $obj)) {$DB->insert_record('badge_external', $obj);}}$badgescache->delete($USER->id);return true;}/*** Create a badgeclass** @param string $entityid The id of the entity.* @param string $data The structure of the badge class.* @return mixed*/public function put_badgeclass($entityid, $data) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('badgeclasses', null, $entityid, $data);}/*** Create an issuer** @param string $data The structure of the issuer.* @return mixed*/public function put_issuer($data) {// V2 Only.if ($this->backpackapiversion == OPEN_BADGES_V1) {throw new coding_exception('Not supported in this backpack API');}return $this->curl_request('issuers', null, null, $data);}/*** Delete any user access tokens in the session so we will attempt to get new ones.** @return void*/public function clear_system_user_session() {global $SESSION;$useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);unset($SESSION->$useridkey);$expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);unset($SESSION->$expireskey);}/*** Authenticate using the stored email and password and save the valid access tokens.** @return mixed The id of the authenticated user as returned by the backpack. Can have* different formats - numeric, empty, object with 'error' property, etc.*/public function authenticate() {global $SESSION;$backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);$backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0;// If the backpack is changed we need to expire sessions.if ($backpackid == $this->backpackid) {if ($this->backpackapiversion == OPEN_BADGES_V2) {$useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);$authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;if ($authuserid == $this->get_auth_user_id()) {$expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);if (isset($SESSION->$expireskey)) {$expires = $SESSION->$expireskey;if ($expires > time()) {// We have a current access token for this user// that has not expired.return -1;}}}} else {$useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);$authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0;if (!empty($authuserid)) {return $authuserid;}}}return $this->curl_request('user', $this->email);}/*** Get all collections in this backpack.** @return stdClass[] The collections.*/public function get_collections() {global $PAGE;if ($this->authenticate()) {if ($this->backpackapiversion == OPEN_BADGES_V1) {$result = $this->curl_request('groups');if (isset($result->groups)) {$result = $result->groups;}} else {$result = $this->curl_request('collections');}if ($result) {return $result;}}return [];}/*** Get one collection by id.** @param integer $collectionid* @return stdClass The collection.*/public function get_collection_record($collectionid) {global $DB;if ($this->backpackapiversion == OPEN_BADGES_V1) {return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid));} else {return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid));}}/*** Disconnect the backpack from this user.** @param integer $userid The user in Moodle* @param integer $backpackid The backpack to disconnect* @return boolean*/public function disconnect_backpack($userid, $backpackid) {global $DB, $USER;if (\core\session\manager::is_loggedinas() || $userid != $USER->id) {// Can't change someone elses backpack settings.return false;}$badgescache = cache::make('core', 'externalbadges');$DB->delete_records('badge_external', array('backpackid' => $backpackid));$DB->delete_records('badge_backpack', array('userid' => $userid));$badgescache->delete($userid);$this->clear_system_user_session();return true;}/*** Handle the response from getting a collection to map to an id.** @param stdClass $data The response data.* @return string The collection id.*/public function get_collection_id_from_response($data) {if ($this->backpackapiversion == OPEN_BADGES_V1) {return $data->groupId;} else {return $data->entityId;}}/*** Get the last error message returned during an authentication request.** @return string*/public function get_authentication_error() {return backpack_api_mapping::get_authentication_error();}/*** Get the list of badges in a collection.** @param stdClass $collection The collection to deal with.* @param boolean $expanded Fetch all the sub entities.* @return stdClass[]*/public function get_badges($collection, $expanded = false) {global $PAGE;if ($this->authenticate()) {if ($this->backpackapiversion == OPEN_BADGES_V1) {if (empty($collection->collectionid)) {return [];}$result = $this->curl_request('badges', $collection->collectionid);return $result->badges;} else {if (empty($collection->entityid)) {return [];}// Now we can make requests.$badges = $this->curl_request('badges', $collection->entityid);if (count($badges) == 0) {return [];}$badges = $badges[0];if ($expanded) {$publicassertions = [];$context = context_system::instance();$output = $PAGE->get_renderer('core', 'badges');foreach ($badges->assertions as $assertion) {$remoteassertion = $this->get_assertion($assertion);// Remote badge was fetched nested in the assertion.$remotebadge = $remoteassertion->badgeclass;if (!$remotebadge) {continue;}$apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion);$exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]);$remotebadge = $exporterinstance->export($output);$remoteissuer = $remotebadge->issuer;$apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion);$exporterinstance = new issuer_exporter($apidata, ['context' => $context]);$remoteissuer = $exporterinstance->export($output);$badgeclone = clone $remotebadge;$badgeclone->issuer = $remoteissuer;$remoteassertion->badge = $badgeclone;$remotebadge->assertion = $remoteassertion;$publicassertions[] = $remotebadge;}$badges = $publicassertions;}return $badges;}}}}