Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
<?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->backpackapiurl = $sitebackpack->backpackapiurl;
$this->backpackapiversion = $sitebackpack->apiversion; $this->password = $sitebackpack->password;
< $this->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : '';
> $this->email = $sitebackpack->backpackemail;
$this->isuserbackpack = false; $this->backpackid = $sitebackpack->id; if (!empty($userbackpack)) {
< if ($userbackpack->externalbackpackid != $sitebackpack->id) { < throw new coding_exception('Incorrect backpack'); < }
$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. 'badges', // Action. > // Badgr.io does not return the public information about a badge '[URL]/backpack/collections/[PARAM1]', // URL > // if the issuer is associated with another user. We need to pass [], // Post params. > // the expand parameters which are not in any specification to get '', // Request exporter. > // additional information about the assertion in a single request. 'core_badges\external\collection_exporter', // Response exporter. > '[URL]/backpack/import', true, // Multiple. > ['url' => '[PARAM]'], // Post params. 'get', // Method. > '', // Request exporter. true, // JSON Encoded. > 'core_badges\external\assertion_exporter', // Response exporter. true // Auth required. > false, // Multiple. ]; > 'post', // Method. foreach ($mapping as $map) { > true, // JSON Encoded. $map[] = true; // User api function. > true // Auth required. $map[] = OPEN_BADGES_V2; // V2 function. > ]; $this->mappings[] = new backpack_api_mapping(...$map); > $mapping[] = [
} } 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[] = [ foreach ($mapping as $map) { > 'updateassertion', // Action. $map[] = false; // Site api function. > '[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer', $map[] = OPEN_BADGES_V2; // V2 function. > '[PARAM]', // Post params. $this->mappings[] = new backpack_api_mapping(...$map); > 'core_badges\external\assertion_exporter', // Request exporter. } > 'core_badges\external\assertion_exporter', // Response exporter. } > false, // Multiple. } else { > 'put', // Method. if ($this->isuserbackpack) { > true, // JSON Encoded. $mapping = []; > true // Auth required. $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. * Select collections from a backpack. > * * > * @param string $entityid The id of the badge class. * @param string $backpackid The id of the backpack > * @param array $data The structure of the badge class assertion. * @param stdClass[] $collections List of collections with collectionid or entityid. > * @return mixed * @return boolean > */ */ > public function update_assertion(string $entityid, array $data) { public function set_backpack_collections($backpackid, $collections) { > // V2 Only. global $DB, $USER; > if ($this->backpackapiversion == OPEN_BADGES_V1) { > throw new coding_exception('Not supported in this backpack API'); // Delete any previously selected collections. > } $sqlparams = array('backpack' => $backpackid); > $select = 'backpackid = :backpack '; > return $this->curl_request('updateassertion', null, $entityid, $data); $DB->delete_records_select('badge_external', $select, $sqlparams); > } $badgescache = cache::make('core', 'externalbadges'); > > /** // Insert selected collections if they are not in database yet. > * Import a badge assertion into a backpack. This is used to handle cross domain backpacks. foreach ($collections as $collection) { > * $obj = new stdClass(); > * @param string $data The structure of the badge class assertion. $obj->backpackid = $backpackid; > * @return mixed if ($this->backpackapiversion == OPEN_BADGES_V1) { > * @throws coding_exception $obj->collectionid = (int) $collection; > */ } else { > public function import_badge_assertion(string $data) { $obj->entityid = $collection; > // V2 Only. $obj->collectionid = -1; > if ($this->backpackapiversion == OPEN_BADGES_V1) { } > throw new coding_exception('Not supported in this backpack API'); if (!$DB->record_exists('badge_external', (array) $obj)) { > } $DB->insert_record('badge_external', $obj); > } > return $this->curl_request('importbadge', null, null, $data);
} $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 integer The id of the authenticated user. */ 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); 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; } } } }