Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * Badge assertion library.
  19   *
  20   * @package    core
  21   * @subpackage badges
  22   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Open Badges Assertions specification 1.0 {@link https://github.com/mozilla/openbadges-backpack/wiki/Assertions}
  31   *
  32   * Badge asserion is defined by three parts:
  33   * - Badge Assertion (information regarding a specific badge that was awarded to a badge earner)
  34   * - Badge Class (general information about a badge and what it is intended to represent)
  35   * - Issuer Class (general information of an issuing organisation)
  36   */
  37  require_once($CFG->libdir . '/badgeslib.php');
  38  require_once($CFG->dirroot . '/badges/renderer.php');
  39  
  40  /**
  41   * Class that represents badge assertion.
  42   *
  43   */
  44  class core_badges_assertion {
  45      /** @var object Issued badge information from database */
  46      private $_data;
  47  
  48      /** @var moodle_url Issued badge url */
  49      private $_url;
  50  
  51      /** @var int $obversion to control version JSON-LD. */
  52      private $_obversion = OPEN_BADGES_V2;
  53  
  54      /**
  55       * Constructs with issued badge unique hash.
  56       *
  57       * @param string $hash Badge unique hash from badge_issued table.
  58       * @param int $obversion to control version JSON-LD.
  59       */
  60      public function __construct($hash, $obversion = OPEN_BADGES_V2) {
  61          global $DB;
  62  
  63          $this->_data = $DB->get_record_sql('
  64              SELECT
  65                  bi.dateissued,
  66                  bi.dateexpire,
  67                  bi.uniquehash,
  68                  u.email,
  69                  b.*,
  70                  bb.email as backpackemail
  71              FROM
  72                  {badge} b
  73                  JOIN {badge_issued} bi
  74                      ON b.id = bi.badgeid
  75                  JOIN {user} u
  76                      ON u.id = bi.userid
  77                  LEFT JOIN {badge_backpack} bb
  78                      ON bb.userid = bi.userid
  79              WHERE ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40),
  80              array('hash' => $hash), IGNORE_MISSING);
  81  
  82          if ($this->_data) {
  83              $this->_url = new moodle_url('/badges/badge.php', array('hash' => $this->_data->uniquehash));
  84          } else {
  85              $this->_url = new moodle_url('/badges/badge.php');
  86          }
  87          $this->_obversion = $obversion;
  88      }
  89  
  90      /**
  91       * Get the local id for this badge.
  92       *
  93       * @return int
  94       */
  95      public function get_badge_id() {
  96          $badgeid = 0;
  97          if ($this->_data) {
  98              $badgeid = $this->_data->id;
  99          }
 100          return $badgeid;
 101      }
 102  
 103      /**
 104       * Get the local id for this badge assertion.
 105       *
 106       * @return string
 107       */
 108      public function get_assertion_hash() {
 109          $hash = '';
 110          if ($this->_data) {
 111              $hash = $this->_data->uniquehash;
 112          }
 113          return $hash;
 114      }
 115  
 116      /**
 117       * Get badge assertion.
 118       *
 119       * @param boolean $issued Include the nested badge issued information.
 120       * @param boolean $usesalt Hash the identity and include the salt information for the hash.
 121       * @return array Badge assertion.
 122       */
 123      public function get_badge_assertion($issued = true, $usesalt = true) {
 124          global $CFG;
 125          $assertion = array();
 126          if ($this->_data) {
 127              $hash = $this->_data->uniquehash;
 128              $email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
 129              $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
 130  
 131              if ($this->_obversion >= OPEN_BADGES_V2) {
 132                  $classurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id()));
 133              } else {
 134                  $classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
 135              }
 136  
 137              // Required.
 138              $assertion['uid'] = $hash;
 139              $assertion['recipient'] = array();
 140              if ($usesalt) {
 141                  $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
 142              } else {
 143                  $assertion['recipient']['identity'] = $email;
 144              }
 145              $assertion['recipient']['type'] = 'email'; // Currently the only supported type.
 146              $assertion['recipient']['hashed'] = true; // We are always hashing recipient.
 147              if ($usesalt) {
 148                  $assertion['recipient']['salt'] = $CFG->badges_badgesalt;
 149              }
 150              if ($issued) {
 151                  $assertion['badge'] = $classurl->out(false);
 152              }
 153              $assertion['verify'] = array();
 154              $assertion['verify']['type'] = 'hosted'; // 'Signed' is not implemented yet.
 155              $assertion['verify']['url'] = $assertionurl->out(false);
 156              $assertion['issuedOn'] = $this->_data->dateissued;
 157              if ($issued) {
 158                  $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL.
 159              }
 160              // Optional.
 161              if (!empty($this->_data->dateexpire)) {
 162                  $assertion['expires'] = $this->_data->dateexpire;
 163              }
 164              $tags = $this->get_tags();
 165              if (is_array($tags) && count($tags) > 0) {
 166                  $assertion['tags'] = $tags;
 167              }
 168              $this->embed_data_badge_version2($assertion, OPEN_BADGES_V2_TYPE_ASSERTION);
 169          }
 170          return $assertion;
 171      }
 172  
 173      /**
 174       * Get badge class information.
 175       *
 176       * @param boolean $issued Include the nested badge issuer information.
 177       * @return array Badge Class information.
 178       */
 179      public function get_badge_class($issued = true) {
 180          $class = [];
 181          if ($this->_data) {
 182              if (empty($this->_data->courseid)) {
 183                  $context = context_system::instance();
 184              } else {
 185                  $context = context_course::instance($this->_data->courseid);
 186              }
 187              // Required.
 188              $class['name'] = $this->_data->name;
 189              $class['description'] = $this->_data->description;
 190              $storage = get_file_storage();
 191              $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f3.png');
 192              if ($imagefile) {
 193                  $imagedata = base64_encode($imagefile->get_content());
 194              } else {
 195                  if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
 196                      // Unit tests the file might not exist yet.
 197                      $imagedata = '';
 198                  } else {
 199                      throw new coding_exception('Image file does not exist.');
 200                  }
 201              }
 202              $class['image'] = 'data:image/png;base64,' . $imagedata;
 203  
 204              $params = ['id' => $this->get_badge_id()];
 205              $badgeurl = new moodle_url('/badges/badgeclass.php', $params);
 206              $class['criteria'] = $badgeurl->out(false); // Currently badge URL.
 207              if ($issued) {
 208                  $params = ['id' => $this->get_badge_id(), 'obversion' => $this->_obversion];
 209                  $issuerurl = new moodle_url('/badges/issuer_json.php', $params);
 210                  $class['issuer'] = $issuerurl->out(false);
 211              }
 212              $tags = $this->get_tags();
 213              if (is_array($tags) && count($tags) > 0) {
 214                  $class['tags'] = $tags;
 215              }
 216              $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
 217              if (!$issued) {
 218                  unset($class['issuer']);
 219              }
 220          }
 221          return $class;
 222      }
 223  
 224      /**
 225       * Get badge issuer information.
 226       *
 227       * @return array Issuer information.
 228       */
 229      public function get_issuer() {
 230          global $CFG;
 231          $issuer = array();
 232          if ($this->_data) {
 233              // Required.
 234              if ($this->_obversion == OPEN_BADGES_V1) {
 235                  $issuer['name'] = $this->_data->issuername;
 236                  $issuer['url'] = $this->_data->issuerurl;
 237                  // Optional.
 238                  if (!empty($this->_data->issuercontact)) {
 239                      $issuer['email'] = $this->_data->issuercontact;
 240                  } else {
 241                      $issuer['email'] = $CFG->badges_defaultissuercontact;
 242                  }
 243              } else {
 244                  $badge = new badge($this->get_badge_id());
 245                  $issuer = $badge->get_badge_issuer();
 246              }
 247          }
 248          $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
 249          return $issuer;
 250      }
 251  
 252      /**
 253       * Get related badges of the badge.
 254       *
 255       * @param badge $badge Badge object.
 256       * @return array|bool List related badges.
 257       */
 258      public function get_related_badges(badge $badge) {
 259          global $DB;
 260          $arraybadges = array();
 261          $relatedbadges = $badge->get_related_badges(true);
 262          if ($relatedbadges) {
 263              foreach ($relatedbadges as $rb) {
 264                  $url = new moodle_url('/badges/badge_json.php', array('id' => $rb->id));
 265                  $arraybadges[] = array(
 266                      'id'        => $url->out(false),
 267                      'version'   => $rb->version,
 268                      '@language' => $rb->language
 269                  );
 270              }
 271          }
 272          return $arraybadges;
 273      }
 274  
 275      /**
 276       * Get endorsement of the badge.
 277       *
 278       * @return false|stdClass Endorsement information.
 279       */
 280      public function get_endorsement() {
 281          global $DB;
 282          $endorsement = array();
 283          $record = $DB->get_record_select('badge_endorsement', 'badgeid = ?', array($this->_data->id));
 284          return $record;
 285      }
 286  
 287      /**
 288       * Get criteria of badge class.
 289       *
 290       * @return array|string Criteria information.
 291       */
 292      public function get_criteria_badge_class() {
 293          $badge = new badge($this->_data->id);
 294          $narrative = $badge->markdown_badge_criteria();
 295          $params = ['id' => $this->get_badge_id()];
 296          $badgeurl = new moodle_url('/badges/badgeclass.php', $params);
 297          if (!empty($narrative)) {
 298              $criteria = [];
 299              $criteria['id'] = $badgeurl->out(false);
 300              $criteria['narrative'] = $narrative;
 301              return $criteria;
 302          } else {
 303              return $badgeurl->out(false);
 304          }
 305      }
 306  
 307      /**
 308       * Get alignment of the badge.
 309       *
 310       * @return array information.
 311       */
 312      public function get_alignments() {
 313          global $DB;
 314          $badgeid = $this->_data->id;
 315          $alignments = array();
 316          $items = $DB->get_records_select('badge_alignment', 'badgeid = ?', array($badgeid));
 317          foreach ($items as $item) {
 318              $alignment = array('targetName' => $item->targetname, 'targetUrl' => $item->targeturl);
 319              if ($item->targetdescription) {
 320                  $alignment['targetDescription'] = $item->targetdescription;
 321              }
 322              if ($item->targetframework) {
 323                  $alignment['targetFramework'] = $item->targetframework;
 324              }
 325              if ($item->targetcode) {
 326                  $alignment['targetCode'] = $item->targetcode;
 327              }
 328              $alignments[] = $alignment;
 329          }
 330          return $alignments;
 331      }
 332  
 333      /**
 334       * Embed data of Open Badges Specification Version 2.0 to json.
 335       *
 336       * @param array $json for assertion, badges, issuer.
 337       * @param string $type Content type.
 338       */
 339      protected function embed_data_badge_version2 (&$json, $type = OPEN_BADGES_V2_TYPE_ASSERTION) {
 340          // Specification Version 2.0.
 341          if ($this->_obversion >= OPEN_BADGES_V2) {
 342              $badge = new badge($this->_data->id);
 343              if (empty($this->_data->courseid)) {
 344                  $context = context_system::instance();
 345              } else {
 346                  $context = context_course::instance($this->_data->courseid);
 347              }
 348  
 349              $hash = $this->_data->uniquehash;
 350              $assertionsurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
 351              $classurl = new moodle_url(
 352                  '/badges/badge_json.php',
 353                  array('id' => $this->get_badge_id())
 354              );
 355              $issuerurl = new moodle_url('/badges/issuer_json.php', ['id' => $this->get_badge_id()]);
 356              // For assertion.
 357              if ($type == OPEN_BADGES_V2_TYPE_ASSERTION) {
 358                  $json['@context'] = OPEN_BADGES_V2_CONTEXT;
 359                  $json['type'] = OPEN_BADGES_V2_TYPE_ASSERTION;
 360                  $json['id'] = $assertionsurl->out(false);
 361                  $json['badge'] = $this->get_badge_class();
 362                  $json['issuedOn'] = date('c', $this->_data->dateissued);
 363                  if (!empty($this->_data->dateexpire)) {
 364                      $json['expires'] = date('c', $this->_data->dateexpire);
 365                  }
 366                  unset($json['uid']);
 367              }
 368              // For Badge.
 369              if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
 370                  $json['@context'] = OPEN_BADGES_V2_CONTEXT;
 371                  $json['id'] = $classurl->out(false);
 372                  $json['type'] = OPEN_BADGES_V2_TYPE_BADGE;
 373                  $json['version'] = $this->_data->version;
 374                  $json['criteria'] = $this->get_criteria_badge_class();
 375                  $json['issuer'] = $this->get_issuer();
 376                  $json['@language'] = $this->_data->language;
 377                  if (!empty($relatedbadges = $this->get_related_badges($badge))) {
 378                      $json['related'] = $relatedbadges;
 379                  }
 380                  if ($endorsement = $this->get_endorsement()) {
 381                      $endorsementurl = new moodle_url('/badges/endorsement_json.php', array('id' => $this->_data->id));
 382                      $json['endorsement'] = $endorsementurl->out(false);
 383                  }
 384                  if ($alignments = $this->get_alignments()) {
 385                      $json['alignments'] = $alignments;
 386                  }
 387                  if ($this->_data->imageauthorname ||
 388                          $this->_data->imageauthoremail ||
 389                          $this->_data->imageauthorurl ||
 390                          $this->_data->imagecaption) {
 391                      $storage = get_file_storage();
 392                      $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f3.png');
 393                      if ($imagefile) {
 394                          $imagedata = base64_encode($imagefile->get_content());
 395                      } else {
 396                          // The file might not exist in unit tests.
 397                          if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
 398                              $imagedata = '';
 399                          } else {
 400                              throw new coding_exception('Image file does not exist.');
 401                          }
 402                      }
 403                      $json['image'] = 'data:image/png;base64,' . $imagedata;
 404                  }
 405              }
 406  
 407              // For issuer.
 408              if ($type == OPEN_BADGES_V2_TYPE_ISSUER) {
 409                  $json['@context'] = OPEN_BADGES_V2_CONTEXT;
 410                  $json['id'] = $issuerurl->out(false);
 411                  $json['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
 412              }
 413          }
 414      }
 415  
 416      /**
 417       * Get tags of the badge.
 418       *
 419       * @return array tags.
 420       */
 421      public function get_tags(): array {
 422          return array_values(\core_tag_tag::get_item_tags_array('core_badges', 'badge', $this->get_badge_id()));
 423      }
 424  }