Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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  2020 Tung Thai based on Totara Learning Solutions Ltd {@link http://www.totaralms.com/} dode
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.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 context_system;
  34  use moodle_url;
  35  use core_badges\backpack_api2p1_mapping;
  36  use core_badges\oauth2\client;
  37  use curl;
  38  use stdClass;
  39  use core\oauth2\issuer;
  40  use core\oauth2\endpoint;
  41  use core\oauth2\discovery\imsbadgeconnect;
  42  
  43  /**
  44   * To process badges with backpack and control api request and this class using for Open Badge API v2.1 methods.
  45   *
  46   * @package   core_badges
  47   * @copyright  2020 Tung Thai
  48   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class backpack_api2p1 {
  51  
  52      /** @var object is the external backpack. */
  53      private $externalbackpack;
  54  
  55      /** @var array define api mapping. */
  56      private $mappings = [];
  57  
  58      /** @var false|null|stdClass|\core_badges\backpack_api2p1 to */
  59      private $tokendata;
  60  
  61      /** @var null clienid. */
  62      private $clientid = null;
  63  
  64      /** @var null version api of the backpack. */
  65      protected $backpackapiversion;
  66  
  67      /** @var issuer The OAuth2 Issuer for this backpack */
  68      protected issuer $issuer;
  69  
  70      /** @var endpoint The apiBase endpoint */
  71      protected endpoint $apibase;
  72  
  73      /**
  74       * backpack_api2p1 constructor.
  75       *
  76       * @param object $externalbackpack object
  77       * @throws coding_exception error message
  78       */
  79      public function __construct($externalbackpack) {
  80  
  81          if (!empty($externalbackpack)) {
  82              $this->externalbackpack = $externalbackpack;
  83              $this->backpackapiversion = $externalbackpack->apiversion;
  84              $this->get_clientid = $this->get_clientid($externalbackpack->oauth2_issuerid);
  85  
  86              if (!($this->tokendata = $this->get_stored_token($externalbackpack->id))
  87                  && $this->backpackapiversion != OPEN_BADGES_V2P1) {
  88                  throw new coding_exception('Backpack incorrect');
  89              }
  90          }
  91  
  92          $this->define_mappings();
  93      }
  94  
  95      /**
  96       * Initialises or returns the OAuth2 issuer associated to this backpack.
  97       *
  98       * @return issuer
  99       */
 100      protected function get_issuer(): issuer {
 101          if (!isset($this->issuer)) {
 102              $this->issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
 103          }
 104          return $this->issuer;
 105      }
 106  
 107      /**
 108       * Gets the apiBase url associated to this backpack.
 109       *
 110       * @return string
 111       */
 112      protected function get_api_base_url(): string {
 113          if (!isset($this->apibase)) {
 114              $apibase = endpoint::get_record([
 115                  'issuerid' => $this->externalbackpack->oauth2_issuerid,
 116                  'name' => 'apiBase',
 117              ]);
 118  
 119              if (empty($apibase)) {
 120                  imsbadgeconnect::create_endpoints($this->get_issuer());
 121                  $apibase = endpoint::get_record([
 122                      'issuerid' => $this->externalbackpack->oauth2_issuerid,
 123                      'name' => 'apiBase',
 124                  ]);
 125              }
 126  
 127              $this->apibase = $apibase;
 128          }
 129  
 130          return $this->apibase->get('url');
 131      }
 132  
 133  
 134      /**
 135       * Define the mappings supported by this usage and api version.
 136       */
 137      private function define_mappings() {
 138          if ($this->backpackapiversion == OPEN_BADGES_V2P1) {
 139  
 140              $mapping = [];
 141              $mapping[] = [
 142                  'post.assertions',                               // Action.
 143                  '[URL]/assertions',   // URL
 144                  '[PARAM]',                                  // Post params.
 145                  false,                                      // Multiple.
 146                  'post',                                     // Method.
 147                  true,                                       // JSON Encoded.
 148                  true                                        // Auth required.
 149              ];
 150  
 151              $mapping[] = [
 152                  'get.assertions',                               // Action.
 153                  '[URL]/assertions',   // URL
 154                  '[PARAM]',                                  // Post params.
 155                  false,                                      // Multiple.
 156                  'get',                                     // Method.
 157                  true,                                       // JSON Encoded.
 158                  true                                        // Auth required.
 159              ];
 160  
 161              foreach ($mapping as $map) {
 162                  $map[] = false; // Site api function.
 163                  $map[] = OPEN_BADGES_V2P1; // V2 function.
 164                  $this->mappings[] = new backpack_api2p1_mapping(...$map);
 165              }
 166  
 167          }
 168      }
 169  
 170      /**
 171       * Disconnect the backpack from this user.
 172       *
 173       * @param object $backpack to disconnect.
 174       * @return bool
 175       * @throws \dml_exception
 176       */
 177      public function disconnect_backpack($backpack) {
 178          global $USER, $DB;
 179  
 180          if ($backpack) {
 181              $DB->delete_records_select('badge_external', 'backpackid = :backpack', ['backpack' => $backpack->id]);
 182              $DB->delete_records('badge_backpack', ['id' => $backpack->id]);
 183              $DB->delete_records('badge_backpack_oauth2', ['externalbackpackid' => $this->externalbackpack->id,
 184                  'userid' => $USER->id]);
 185  
 186              return true;
 187          }
 188          return false;
 189      }
 190  
 191      /**
 192       * Make an api request.
 193       *
 194       * @param string $action The api function.
 195       * @param string $postdata The body of the api request.
 196       * @return mixed
 197       */
 198      public function curl_request($action, $postdata = null) {
 199          $tokenkey = $this->tokendata->token;
 200          foreach ($this->mappings as $mapping) {
 201              if ($mapping->is_match($action)) {
 202                  return $mapping->request(
 203                      $this->get_api_base_url(),
 204                      $tokenkey,
 205                      $postdata
 206                  );
 207              }
 208          }
 209  
 210          throw new coding_exception('Unknown request');
 211      }
 212  
 213      /**
 214       * Get token.
 215       *
 216       * @param int $externalbackpackid ID of external backpack.
 217       * @return oauth2\badge_backpack_oauth2|false|stdClass|null
 218       */
 219      protected function get_stored_token($externalbackpackid) {
 220          global $USER;
 221  
 222          $token = \core_badges\oauth2\badge_backpack_oauth2::get_record(
 223              ['externalbackpackid' => $externalbackpackid, 'userid' => $USER->id]);
 224          if ($token !== false) {
 225              $token = $token->to_record();
 226              return $token;
 227          }
 228          return null;
 229      }
 230  
 231      /**
 232       * Get client id.
 233       *
 234       * @param int $issuerid id of Oauth2 service.
 235       * @throws coding_exception
 236       */
 237      private function get_clientid($issuerid) {
 238          $issuer = \core\oauth2\api::get_issuer($issuerid);
 239          if (!empty($issuer)) {
 240              $this->issuer = $issuer;
 241              $this->clientid = $issuer->get('clientid');
 242          }
 243      }
 244  
 245      /**
 246       * Export a badge to the backpack site.
 247       *
 248       * @param string $hash of badge issued.
 249       * @return array
 250       * @throws \moodle_exception
 251       * @throws coding_exception
 252       */
 253      public function put_assertions($hash) {
 254          $data = [];
 255          if (!$hash) {
 256              return false;
 257          }
 258  
 259          $issuer = $this->get_issuer();
 260          $client = new client($issuer, new moodle_url('/badges/mybadges.php'), '', $this->externalbackpack);
 261          if (!$client->is_logged_in()) {
 262              $redirecturl = new moodle_url('/badges/mybadges.php', ['error' => 'backpackexporterror']);
 263              redirect($redirecturl);
 264          }
 265  
 266          $this->tokendata = $this->get_stored_token($this->externalbackpack->id);
 267  
 268          $assertion = new \core_badges_assertion($hash, OPEN_BADGES_V2);
 269          $data['assertion'] = $assertion->get_badge_assertion();
 270          $response = $this->curl_request('post.assertions', $data);
 271          if ($response && isset($response->status->statusCode) && $response->status->statusCode == 200) {
 272              $msg['status'] = \core\output\notification::NOTIFY_SUCCESS;
 273              $msg['message'] = get_string('addedtobackpack', 'badges');
 274          } else {
 275              if ($response) {
 276                  // Although the specification defines that status error is a string, some providers, like Badgr, are wrongly
 277                  // returning an array. It has been reported, but adding these extra checks doesn't hurt, just in case.
 278                  if (
 279                      property_exists($response, 'status') &&
 280                      is_object($response->status) &&
 281                      property_exists($response->status, 'error')
 282                  ) {
 283                      $statuserror = $response->status->error;
 284                      if (is_array($statuserror)) {
 285                          $statuserror = implode($statuserror);
 286                      }
 287                  } else if (property_exists($response, 'error')) {
 288                      $statuserror = $response->error;
 289                      if (property_exists($response, 'message')) {
 290                          $statuserror .= '. Message: ' . $response->message;
 291                      }
 292                  }
 293              } else {
 294                  $statuserror = 'Empty response';
 295              }
 296              $data = [
 297                  'badgename' => $data['assertion']['badge']['name'],
 298                  'error' => $statuserror,
 299              ];
 300  
 301              $msg['status'] = \core\output\notification::NOTIFY_ERROR;
 302              $msg['message'] = get_string('backpackexporterrorwithinfo', 'badges', $data);
 303          }
 304          return $msg;
 305      }
 306  }