See Release Notes
Long Term Support Release
<?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 2020 Tung Thai based on Totara Learning Solutions Ltd {@link http://www.totaralms.com/} dode * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Tung Thai <Tung.ThaiDuc@nashtechglobal.com> */ namespace core_badges; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); use cache; use coding_exception; use context_system; use moodle_url; use core_badges\backpack_api2p1_mapping; use core_badges\oauth2\client; use curl; use stdClass; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\discovery\imsbadgeconnect; /** * To process badges with backpack and control api request and this class using for Open Badge API v2.1 methods. * * @package core_badges * @copyright 2020 Tung Thai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backpack_api2p1 { /** @var object is the external backpack. */ private $externalbackpack; /** @var array define api mapping. */ private $mappings = []; /** @var false|null|stdClass|\core_badges\backpack_api2p1 to */ private $tokendata; /** @var null clienid. */ private $clientid = null; /** @var null version api of the backpack. */ protected $backpackapiversion; /** @var issuer The OAuth2 Issuer for this backpack */ protected issuer $issuer; /** @var endpoint The apiBase endpoint */ protected endpoint $apibase; /** * backpack_api2p1 constructor. * * @param object $externalbackpack object * @throws coding_exception error message */ public function __construct($externalbackpack) { if (!empty($externalbackpack)) { $this->externalbackpack = $externalbackpack; $this->backpackapiversion = $externalbackpack->apiversion;< $this->get_clientid = $this->get_clientid($externalbackpack->oauth2_issuerid);> $this->get_clientid($externalbackpack->oauth2_issuerid);if (!($this->tokendata = $this->get_stored_token($externalbackpack->id)) && $this->backpackapiversion != OPEN_BADGES_V2P1) { throw new coding_exception('Backpack incorrect'); } } $this->define_mappings(); } /** * Initialises or returns the OAuth2 issuer associated to this backpack. * * @return issuer */ protected function get_issuer(): issuer { if (!isset($this->issuer)) { $this->issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid); } return $this->issuer; } /** * Gets the apiBase url associated to this backpack. * * @return string */ protected function get_api_base_url(): string { if (!isset($this->apibase)) { $apibase = endpoint::get_record([ 'issuerid' => $this->externalbackpack->oauth2_issuerid, 'name' => 'apiBase', ]); if (empty($apibase)) { imsbadgeconnect::create_endpoints($this->get_issuer()); $apibase = endpoint::get_record([ 'issuerid' => $this->externalbackpack->oauth2_issuerid, 'name' => 'apiBase', ]); } $this->apibase = $apibase; } return $this->apibase->get('url'); } /** * Define the mappings supported by this usage and api version. */ private function define_mappings() { if ($this->backpackapiversion == OPEN_BADGES_V2P1) { $mapping = []; $mapping[] = [ 'post.assertions', // Action. '[URL]/assertions', // URL '[PARAM]', // Post params. false, // Multiple. 'post', // Method. true, // JSON Encoded. true // Auth required. ]; $mapping[] = [ 'get.assertions', // Action. '[URL]/assertions', // URL '[PARAM]', // Post params. false, // Multiple. 'get', // Method. true, // JSON Encoded. true // Auth required. ]; foreach ($mapping as $map) { $map[] = false; // Site api function. $map[] = OPEN_BADGES_V2P1; // V2 function. $this->mappings[] = new backpack_api2p1_mapping(...$map); } } } /** * Disconnect the backpack from this user. * * @param object $backpack to disconnect. * @return bool * @throws \dml_exception */ public function disconnect_backpack($backpack) { global $USER, $DB; if ($backpack) { $DB->delete_records_select('badge_external', 'backpackid = :backpack', ['backpack' => $backpack->id]); $DB->delete_records('badge_backpack', ['id' => $backpack->id]); $DB->delete_records('badge_backpack_oauth2', ['externalbackpackid' => $this->externalbackpack->id, 'userid' => $USER->id]); return true; } return false; } /** * Make an api request. * * @param string $action The api function. * @param string $postdata The body of the api request. * @return mixed */ public function curl_request($action, $postdata = null) { $tokenkey = $this->tokendata->token; foreach ($this->mappings as $mapping) { if ($mapping->is_match($action)) { return $mapping->request( $this->get_api_base_url(), $tokenkey, $postdata ); } } throw new coding_exception('Unknown request'); } /** * Get token. * * @param int $externalbackpackid ID of external backpack. * @return oauth2\badge_backpack_oauth2|false|stdClass|null */ protected function get_stored_token($externalbackpackid) { global $USER; $token = \core_badges\oauth2\badge_backpack_oauth2::get_record( ['externalbackpackid' => $externalbackpackid, 'userid' => $USER->id]); if ($token !== false) { $token = $token->to_record(); return $token; } return null; } /** * Get client id. * * @param int $issuerid id of Oauth2 service. * @throws coding_exception */ private function get_clientid($issuerid) { $issuer = \core\oauth2\api::get_issuer($issuerid); if (!empty($issuer)) { $this->issuer = $issuer; $this->clientid = $issuer->get('clientid'); } } /** * Export a badge to the backpack site. * * @param string $hash of badge issued. * @return array * @throws \moodle_exception * @throws coding_exception */ public function put_assertions($hash) { $data = []; if (!$hash) { return false; } $issuer = $this->get_issuer(); $client = new client($issuer, new moodle_url('/badges/mybadges.php'), '', $this->externalbackpack); if (!$client->is_logged_in()) { $redirecturl = new moodle_url('/badges/mybadges.php', ['error' => 'backpackexporterror']); redirect($redirecturl); } $this->tokendata = $this->get_stored_token($this->externalbackpack->id); $assertion = new \core_badges_assertion($hash, OPEN_BADGES_V2); $data['assertion'] = $assertion->get_badge_assertion(); $response = $this->curl_request('post.assertions', $data); if ($response && isset($response->status->statusCode) && $response->status->statusCode == 200) { $msg['status'] = \core\output\notification::NOTIFY_SUCCESS; $msg['message'] = get_string('addedtobackpack', 'badges'); } else { if ($response) { // Although the specification defines that status error is a string, some providers, like Badgr, are wrongly // returning an array. It has been reported, but adding these extra checks doesn't hurt, just in case. if ( property_exists($response, 'status') && is_object($response->status) && property_exists($response->status, 'error') ) { $statuserror = $response->status->error; if (is_array($statuserror)) { $statuserror = implode($statuserror); } } else if (property_exists($response, 'error')) { $statuserror = $response->error; if (property_exists($response, 'message')) { $statuserror .= '. Message: ' . $response->message; } } } else { $statuserror = 'Empty response'; } $data = [ 'badgename' => $data['assertion']['badge']['name'], 'error' => $statuserror, ]; $msg['status'] = \core\output\notification::NOTIFY_ERROR; $msg['message'] = get_string('backpackexporterrorwithinfo', 'badges', $data); } return $msg; } }