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 39 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   * 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  namespace core_badges;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  require_once($CFG->dirroot.'/lib/badgeslib.php');
  32  
  33  use context_system;
  34  use context_course;
  35  use context_user;
  36  use moodle_exception;
  37  use moodle_url;
  38  use core_text;
  39  use award_criteria;
  40  use core_php_time_limit;
  41  use html_writer;
  42  use stdClass;
  43  
  44  /**
  45   * Class that represents badge.
  46   *
  47   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  48   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class badge {
  51      /** @var int Badge id */
  52      public $id;
  53  
  54      /** @var string Badge name */
  55      public $name;
  56  
  57      /** @var string Badge description */
  58      public $description;
  59  
  60      /** @var integer Timestamp this badge was created */
  61      public $timecreated;
  62  
  63      /** @var integer Timestamp this badge was modified */
  64      public $timemodified;
  65  
  66      /** @var int The user who created this badge */
  67      public $usercreated;
  68  
  69      /** @var int The user who modified this badge */
  70      public $usermodified;
  71  
  72      /** @var string The name of the issuer of this badge */
  73      public $issuername;
  74  
  75      /** @var string The url of the issuer of this badge */
  76      public $issuerurl;
  77  
  78      /** @var string The email of the issuer of this badge */
  79      public $issuercontact;
  80  
  81      /** @var integer Timestamp this badge will expire */
  82      public $expiredate;
  83  
  84      /** @var integer Duration this badge is valid for */
  85      public $expireperiod;
  86  
  87      /** @var integer Site or course badge */
  88      public $type;
  89  
  90      /** @var integer The course this badge belongs to */
  91      public $courseid;
  92  
  93      /** @var string The message this badge includes. */
  94      public $message;
  95  
  96      /** @var string The subject of the message for this badge */
  97      public $messagesubject;
  98  
  99      /** @var int Is this badge image baked. */
 100      public $attachment;
 101  
 102      /** @var int Send a message when this badge is awarded. */
 103      public $notification;
 104  
 105      /** @var int Lifecycle status for this badge. */
 106      public $status = 0;
 107  
 108      /** @var int Timestamp to next run cron for this badge. */
 109      public $nextcron;
 110  
 111      /** @var int What backpack api version to use for this badge. */
 112      public $version;
 113  
 114      /** @var string What language is this badge written in. */
 115      public $language;
 116  
 117      /** @var string The author of the image for this badge. */
 118      public $imageauthorname;
 119  
 120      /** @var string The email of the author of the image for this badge. */
 121      public $imageauthoremail;
 122  
 123      /** @var string The url of the author of the image for this badge. */
 124      public $imageauthorurl;
 125  
 126      /** @var string The caption of the image for this badge. */
 127      public $imagecaption;
 128  
 129      /** @var array Badge criteria */
 130      public $criteria = array();
 131  
 132      /**
 133       * Constructs with badge details.
 134       *
 135       * @param int $badgeid badge ID.
 136       */
 137      public function __construct($badgeid) {
 138          global $DB;
 139          $this->id = $badgeid;
 140  
 141          $data = $DB->get_record('badge', array('id' => $badgeid));
 142  
 143          if (empty($data)) {
 144              throw new moodle_exception('error:nosuchbadge', 'badges', '', $badgeid);
 145          }
 146  
 147          foreach ((array)$data as $field => $value) {
 148              if (property_exists($this, $field)) {
 149                  $this->{$field} = $value;
 150              }
 151          }
 152  
 153          if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) {
 154              // For Open Badges 2 we need to use a single site issuer with no exceptions.
 155              $issuer = badges_get_default_issuer();
 156              $this->issuername = $issuer['name'];
 157              $this->issuercontact = $issuer['email'];
 158              $this->issuerurl = $issuer['url'];
 159          }
 160  
 161          $this->criteria = self::get_criteria();
 162      }
 163  
 164      /**
 165       * Use to get context instance of a badge.
 166       *
 167       * @return context instance.
 168       */
 169      public function get_context() {
 170          if ($this->type == BADGE_TYPE_SITE) {
 171              return context_system::instance();
 172          } else if ($this->type == BADGE_TYPE_COURSE) {
 173              return context_course::instance($this->courseid);
 174          } else {
 175              debugging('Something is wrong...');
 176          }
 177      }
 178  
 179      /**
 180       * Return array of aggregation methods
 181       *
 182       * @return array
 183       */
 184      public static function get_aggregation_methods() {
 185          return array(
 186                  BADGE_CRITERIA_AGGREGATION_ALL => get_string('all', 'badges'),
 187                  BADGE_CRITERIA_AGGREGATION_ANY => get_string('any', 'badges'),
 188          );
 189      }
 190  
 191      /**
 192       * Return array of accepted criteria types for this badge
 193       *
 194       * @return array
 195       */
 196      public function get_accepted_criteria() {
 197          global $CFG;
 198          $criteriatypes = array();
 199  
 200          if ($this->type == BADGE_TYPE_COURSE) {
 201              $criteriatypes = array(
 202                      BADGE_CRITERIA_TYPE_OVERALL,
 203                      BADGE_CRITERIA_TYPE_MANUAL,
 204                      BADGE_CRITERIA_TYPE_COURSE,
 205                      BADGE_CRITERIA_TYPE_BADGE,
 206                      BADGE_CRITERIA_TYPE_ACTIVITY,
 207                      BADGE_CRITERIA_TYPE_COMPETENCY
 208              );
 209          } else if ($this->type == BADGE_TYPE_SITE) {
 210              $criteriatypes = array(
 211                      BADGE_CRITERIA_TYPE_OVERALL,
 212                      BADGE_CRITERIA_TYPE_MANUAL,
 213                      BADGE_CRITERIA_TYPE_COURSESET,
 214                      BADGE_CRITERIA_TYPE_BADGE,
 215                      BADGE_CRITERIA_TYPE_PROFILE,
 216                      BADGE_CRITERIA_TYPE_COHORT,
 217                      BADGE_CRITERIA_TYPE_COMPETENCY
 218              );
 219          }
 220          $alltypes = badges_list_criteria();
 221          foreach ($criteriatypes as $index => $type) {
 222              if (!isset($alltypes[$type])) {
 223                  unset($criteriatypes[$index]);
 224              }
 225          }
 226  
 227          return $criteriatypes;
 228      }
 229  
 230      /**
 231       * Save/update badge information in 'badge' table only.
 232       * Cannot be used for updating awards and criteria settings.
 233       *
 234       * @return boolean Returns true on success.
 235       */
 236      public function save() {
 237          global $DB;
 238  
 239          $fordb = new stdClass();
 240          foreach (get_object_vars($this) as $k => $v) {
 241              $fordb->{$k} = $v;
 242          }
 243          unset($fordb->criteria);
 244  
 245          $fordb->timemodified = time();
 246          if ($DB->update_record_raw('badge', $fordb)) {
 247              // Trigger event, badge updated.
 248              $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
 249              $event = \core\event\badge_updated::create($eventparams);
 250              $event->trigger();
 251              return true;
 252          } else {
 253              throw new moodle_exception('error:save', 'badges');
 254              return false;
 255          }
 256      }
 257  
 258      /**
 259       * Creates and saves a clone of badge with all its properties.
 260       * Clone is not active by default and has 'Copy of' attached to its name.
 261       *
 262       * @return int ID of new badge.
 263       */
 264      public function make_clone() {
 265          global $DB, $USER, $PAGE;
 266  
 267          $fordb = new stdClass();
 268          foreach (get_object_vars($this) as $k => $v) {
 269              $fordb->{$k} = $v;
 270          }
 271  
 272          $fordb->name = get_string('copyof', 'badges', $this->name);
 273          $fordb->status = BADGE_STATUS_INACTIVE;
 274          $fordb->usercreated = $USER->id;
 275          $fordb->usermodified = $USER->id;
 276          $fordb->timecreated = time();
 277          $fordb->timemodified = time();
 278          unset($fordb->id);
 279  
 280          if ($fordb->notification > 1) {
 281              $fordb->nextcron = badges_calculate_message_schedule($fordb->notification);
 282          }
 283  
 284          $criteria = $fordb->criteria;
 285          unset($fordb->criteria);
 286  
 287          if ($new = $DB->insert_record('badge', $fordb, true)) {
 288              $newbadge = new badge($new);
 289  
 290              // Copy badge image.
 291              $fs = get_file_storage();
 292              if ($file = $fs->get_file($this->get_context()->id, 'badges', 'badgeimage', $this->id, '/', 'f3.png')) {
 293                  if ($imagefile = $file->copy_content_to_temp()) {
 294                      badges_process_badge_image($newbadge, $imagefile);
 295                  }
 296              }
 297  
 298              // Copy badge criteria.
 299              foreach ($this->criteria as $crit) {
 300                  $crit->make_clone($new);
 301              }
 302  
 303              // Trigger event, badge duplicated.
 304              $eventparams = array('objectid' => $new, 'context' => $PAGE->context);
 305              $event = \core\event\badge_duplicated::create($eventparams);
 306              $event->trigger();
 307  
 308              return $new;
 309          } else {
 310              throw new moodle_exception('error:clone', 'badges');
 311              return false;
 312          }
 313      }
 314  
 315      /**
 316       * Checks if badges is active.
 317       * Used in badge award.
 318       *
 319       * @return boolean A status indicating badge is active
 320       */
 321      public function is_active() {
 322          if (($this->status == BADGE_STATUS_ACTIVE) ||
 323              ($this->status == BADGE_STATUS_ACTIVE_LOCKED)) {
 324              return true;
 325          }
 326          return false;
 327      }
 328  
 329      /**
 330       * Use to get the name of badge status.
 331       *
 332       * @return string
 333       */
 334      public function get_status_name() {
 335          return get_string('badgestatus_' . $this->status, 'badges');
 336      }
 337  
 338      /**
 339       * Use to set badge status.
 340       * Only active badges can be earned/awarded/issued.
 341       *
 342       * @param int $status Status from BADGE_STATUS constants
 343       */
 344      public function set_status($status = 0) {
 345          $this->status = $status;
 346          $this->save();
 347          if ($status == BADGE_STATUS_ACTIVE) {
 348              // Trigger event, badge enabled.
 349              $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
 350              $event = \core\event\badge_enabled::create($eventparams);
 351              $event->trigger();
 352          } else if ($status == BADGE_STATUS_INACTIVE) {
 353              // Trigger event, badge disabled.
 354              $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
 355              $event = \core\event\badge_disabled::create($eventparams);
 356              $event->trigger();
 357          }
 358      }
 359  
 360      /**
 361       * Checks if badges is locked.
 362       * Used in badge award and editing.
 363       *
 364       * @return boolean A status indicating badge is locked
 365       */
 366      public function is_locked() {
 367          if (($this->status == BADGE_STATUS_ACTIVE_LOCKED) ||
 368                  ($this->status == BADGE_STATUS_INACTIVE_LOCKED)) {
 369              return true;
 370          }
 371          return false;
 372      }
 373  
 374      /**
 375       * Checks if badge has been awarded to users.
 376       * Used in badge editing.
 377       *
 378       * @return boolean A status indicating badge has been awarded at least once
 379       */
 380      public function has_awards() {
 381          global $DB;
 382          $awarded = $DB->record_exists_sql('SELECT b.uniquehash
 383                      FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
 384                      WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
 385  
 386          return $awarded;
 387      }
 388  
 389      /**
 390       * Gets list of users who have earned an instance of this badge.
 391       *
 392       * @return array An array of objects with information about badge awards.
 393       */
 394      public function get_awards() {
 395          global $DB;
 396  
 397          $awards = $DB->get_records_sql(
 398                  'SELECT b.userid, b.dateissued, b.uniquehash, u.firstname, u.lastname
 399                      FROM {badge_issued} b INNER JOIN {user} u
 400                          ON b.userid = u.id
 401                      WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
 402  
 403          return $awards;
 404      }
 405  
 406      /**
 407       * Indicates whether badge has already been issued to a user.
 408       *
 409       * @param int $userid User to check
 410       * @return boolean
 411       */
 412      public function is_issued($userid) {
 413          global $DB;
 414          return $DB->record_exists('badge_issued', array('badgeid' => $this->id, 'userid' => $userid));
 415      }
 416  
 417      /**
 418       * Issue a badge to user.
 419       *
 420       * @param int $userid User who earned the badge
 421       * @param boolean $nobake Not baking actual badges (for testing purposes)
 422       */
 423      public function issue($userid, $nobake = false) {
 424          global $DB, $CFG;
 425  
 426          $now = time();
 427          $issued = new stdClass();
 428          $issued->badgeid = $this->id;
 429          $issued->userid = $userid;
 430          $issued->uniquehash = sha1(rand() . $userid . $this->id . $now);
 431          $issued->dateissued = $now;
 432  
 433          if ($this->can_expire()) {
 434              $issued->dateexpire = $this->calculate_expiry($now);
 435          } else {
 436              $issued->dateexpire = null;
 437          }
 438  
 439          // Take into account user badges privacy settings.
 440          // If none set, badges default visibility is set to public.
 441          $issued->visible = get_user_preferences('badgeprivacysetting', 1, $userid);
 442  
 443          $result = $DB->insert_record('badge_issued', $issued, true);
 444  
 445          if ($result) {
 446              // Trigger badge awarded event.
 447              $eventdata = array (
 448                  'context' => $this->get_context(),
 449                  'objectid' => $this->id,
 450                  'relateduserid' => $userid,
 451                  'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result)
 452              );
 453              \core\event\badge_awarded::create($eventdata)->trigger();
 454  
 455              // Lock the badge, so that its criteria could not be changed any more.
 456              if ($this->status == BADGE_STATUS_ACTIVE) {
 457                  $this->set_status(BADGE_STATUS_ACTIVE_LOCKED);
 458              }
 459  
 460              // Update details in criteria_met table.
 461              $compl = $this->get_criteria_completions($userid);
 462              foreach ($compl as $c) {
 463                  $obj = new stdClass();
 464                  $obj->id = $c->id;
 465                  $obj->issuedid = $result;
 466                  $DB->update_record('badge_criteria_met', $obj, true);
 467              }
 468  
 469              if (!$nobake) {
 470                  // Bake a badge image.
 471                  $pathhash = badges_bake($issued->uniquehash, $this->id, $userid, true);
 472  
 473                  // Notify recipients and badge creators.
 474                  badges_notify_badge_award($this, $userid, $issued->uniquehash, $pathhash);
 475              }
 476          }
 477      }
 478  
 479      /**
 480       * Reviews all badge criteria and checks if badge can be instantly awarded.
 481       *
 482       * @return int Number of awards
 483       */
 484      public function review_all_criteria() {
 485          global $DB, $CFG;
 486          $awards = 0;
 487  
 488          // Raise timelimit as this could take a while for big web sites.
 489          core_php_time_limit::raise();
 490          raise_memory_limit(MEMORY_HUGE);
 491  
 492          foreach ($this->criteria as $crit) {
 493              // Overall criterion is decided when other criteria are reviewed.
 494              if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) {
 495                  continue;
 496              }
 497  
 498              list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql();
 499              // For site level badges, get all active site users who can earn this badge and haven't got it yet.
 500              if ($this->type == BADGE_TYPE_SITE) {
 501                  $sql = "SELECT DISTINCT u.id, bi.badgeid
 502                          FROM {user} u
 503                          {$extrajoin}
 504                          LEFT JOIN {badge_issued} bi
 505                              ON u.id = bi.userid AND bi.badgeid = :badgeid
 506                          WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere;
 507                  $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams);
 508                  $toearn = $DB->get_fieldset_sql($sql, $params);
 509              } else {
 510                  // For course level badges, get all users who already earned the badge in this course.
 511                  // Then find the ones who are enrolled in the course and don't have a badge yet.
 512                  $earned = $DB->get_fieldset_select(
 513                      'badge_issued',
 514                      'userid AS id',
 515                      'badgeid = :badgeid',
 516                      array('badgeid' => $this->id)
 517                  );
 518  
 519                  $wheresql = '';
 520                  $earnedparams = array();
 521                  if (!empty($earned)) {
 522                      list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false);
 523                      $wheresql = ' WHERE u.id ' . $earnedsql;
 524                  }
 525                  list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true);
 526                  $sql = "SELECT DISTINCT u.id
 527                          FROM {user} u
 528                          {$extrajoin}
 529                          JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere;
 530                  $params = array_merge($enrolledparams, $earnedparams, $extraparams);
 531                  $toearn = $DB->get_fieldset_sql($sql, $params);
 532              }
 533  
 534              foreach ($toearn as $uid) {
 535                  $reviewoverall = false;
 536                  if ($crit->review($uid, true)) {
 537                      $crit->mark_complete($uid);
 538                      if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) {
 539                          $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
 540                          $this->issue($uid);
 541                          $awards++;
 542                      } else {
 543                          $reviewoverall = true;
 544                      }
 545                  } else {
 546                      // Will be reviewed some other time.
 547                      $reviewoverall = false;
 548                  }
 549                  // Review overall if it is required.
 550                  if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) {
 551                      $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
 552                      $this->issue($uid);
 553                      $awards++;
 554                  }
 555              }
 556          }
 557  
 558          return $awards;
 559      }
 560  
 561      /**
 562       * Gets an array of completed criteria from 'badge_criteria_met' table.
 563       *
 564       * @param int $userid Completions for a user
 565       * @return array Records of criteria completions
 566       */
 567      public function get_criteria_completions($userid) {
 568          global $DB;
 569          $completions = array();
 570          $sql = "SELECT bcm.id, bcm.critid
 571                  FROM {badge_criteria_met} bcm
 572                      INNER JOIN {badge_criteria} bc ON bcm.critid = bc.id
 573                  WHERE bc.badgeid = :badgeid AND bcm.userid = :userid ";
 574          $completions = $DB->get_records_sql($sql, array('badgeid' => $this->id, 'userid' => $userid));
 575  
 576          return $completions;
 577      }
 578  
 579      /**
 580       * Checks if badges has award criteria set up.
 581       *
 582       * @return boolean A status indicating badge has at least one criterion
 583       */
 584      public function has_criteria() {
 585          if (count($this->criteria) > 0) {
 586              return true;
 587          }
 588          return false;
 589      }
 590  
 591      /**
 592       * Returns badge award criteria
 593       *
 594       * @return array An array of badge criteria
 595       */
 596      public function get_criteria() {
 597          global $DB;
 598          $criteria = array();
 599  
 600          if ($records = (array)$DB->get_records('badge_criteria', array('badgeid' => $this->id))) {
 601              foreach ($records as $record) {
 602                  $criteria[$record->criteriatype] = award_criteria::build((array)$record);
 603              }
 604          }
 605  
 606          return $criteria;
 607      }
 608  
 609      /**
 610       * Get aggregation method for badge criteria
 611       *
 612       * @param int $criteriatype If none supplied, get overall aggregation method (optional)
 613       * @return int One of BADGE_CRITERIA_AGGREGATION_ALL or BADGE_CRITERIA_AGGREGATION_ANY
 614       */
 615      public function get_aggregation_method($criteriatype = 0) {
 616          global $DB;
 617          $params = array('badgeid' => $this->id, 'criteriatype' => $criteriatype);
 618          $aggregation = $DB->get_field('badge_criteria', 'method', $params, IGNORE_MULTIPLE);
 619  
 620          if (!$aggregation) {
 621              return BADGE_CRITERIA_AGGREGATION_ALL;
 622          }
 623  
 624          return $aggregation;
 625      }
 626  
 627      /**
 628       * Checks if badge has expiry period or date set up.
 629       *
 630       * @return boolean A status indicating badge can expire
 631       */
 632      public function can_expire() {
 633          if ($this->expireperiod || $this->expiredate) {
 634              return true;
 635          }
 636          return false;
 637      }
 638  
 639      /**
 640       * Calculates badge expiry date based on either expirydate or expiryperiod.
 641       *
 642       * @param int $timestamp Time of badge issue
 643       * @return int A timestamp
 644       */
 645      public function calculate_expiry($timestamp) {
 646          $expiry = null;
 647  
 648          if (isset($this->expiredate)) {
 649              $expiry = $this->expiredate;
 650          } else if (isset($this->expireperiod)) {
 651              $expiry = $timestamp + $this->expireperiod;
 652          }
 653  
 654          return $expiry;
 655      }
 656  
 657      /**
 658       * Checks if badge has manual award criteria set.
 659       *
 660       * @return boolean A status indicating badge can be awarded manually
 661       */
 662      public function has_manual_award_criteria() {
 663          foreach ($this->criteria as $criterion) {
 664              if ($criterion->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
 665                  return true;
 666              }
 667          }
 668          return false;
 669      }
 670  
 671      /**
 672       * Fully deletes the badge or marks it as archived.
 673       *
 674       * @param boolean $archive Achive a badge without actual deleting of any data.
 675       */
 676      public function delete($archive = true) {
 677          global $DB;
 678  
 679          if ($archive) {
 680              $this->status = BADGE_STATUS_ARCHIVED;
 681              $this->save();
 682  
 683              // Trigger event, badge archived.
 684              $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
 685              $event = \core\event\badge_archived::create($eventparams);
 686              $event->trigger();
 687              return;
 688          }
 689  
 690          $fs = get_file_storage();
 691  
 692          // Remove all issued badge image files and badge awards.
 693          // Cannot bulk remove area files here because they are issued in user context.
 694          $awards = $this->get_awards();
 695          foreach ($awards as $award) {
 696              $usercontext = context_user::instance($award->userid);
 697              $fs->delete_area_files($usercontext->id, 'badges', 'userbadge', $this->id);
 698          }
 699          $DB->delete_records('badge_issued', array('badgeid' => $this->id));
 700  
 701          // Remove all badge criteria.
 702          $criteria = $this->get_criteria();
 703          foreach ($criteria as $criterion) {
 704              $criterion->delete();
 705          }
 706  
 707          // Delete badge images.
 708          $badgecontext = $this->get_context();
 709          $fs->delete_area_files($badgecontext->id, 'badges', 'badgeimage', $this->id);
 710  
 711          // Delete endorsements, competencies and related badges.
 712          $DB->delete_records('badge_endorsement', array('badgeid' => $this->id));
 713          $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
 714          $relatedparams = array(
 715              'badgeid' => $this->id,
 716              'relatedbadgeid' => $this->id
 717          );
 718          $DB->delete_records_select('badge_related', $relatedsql, $relatedparams);
 719          $DB->delete_records('badge_alignment', array('badgeid' => $this->id));
 720  
 721          // Finally, remove badge itself.
 722          $DB->delete_records('badge', array('id' => $this->id));
 723  
 724          // Trigger event, badge deleted.
 725          $eventparams = array('objectid' => $this->id,
 726              'context' => $this->get_context(),
 727              'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid)
 728              );
 729          $event = \core\event\badge_deleted::create($eventparams);
 730          $event->trigger();
 731      }
 732  
 733      /**
 734       * Add multiple related badges.
 735       *
 736       * @param array $relatedids Id of badges.
 737       */
 738      public function add_related_badges($relatedids) {
 739          global $DB;
 740          $relatedbadges = array();
 741          foreach ($relatedids as $relatedid) {
 742              $relatedbadge = new stdClass();
 743              $relatedbadge->badgeid = $this->id;
 744              $relatedbadge->relatedbadgeid = $relatedid;
 745              $relatedbadges[] = $relatedbadge;
 746          }
 747          $DB->insert_records('badge_related', $relatedbadges);
 748      }
 749  
 750      /**
 751       * Delete an related badge.
 752       *
 753       * @param int $relatedid Id related badge.
 754       * @return boolean A status for delete an related badge.
 755       */
 756      public function delete_related_badge($relatedid) {
 757          global $DB;
 758          $sql = "(badgeid = :badgeid AND relatedbadgeid = :relatedid) OR " .
 759                 "(badgeid = :relatedid2 AND relatedbadgeid = :badgeid2)";
 760          $params = ['badgeid' => $this->id, 'badgeid2' => $this->id, 'relatedid' => $relatedid, 'relatedid2' => $relatedid];
 761          return $DB->delete_records_select('badge_related', $sql, $params);
 762      }
 763  
 764      /**
 765       * Checks if badge has related badges.
 766       *
 767       * @return boolean A status related badge.
 768       */
 769      public function has_related() {
 770          global $DB;
 771          $sql = "SELECT DISTINCT b.id
 772                      FROM {badge_related} br
 773                      JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
 774                     WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
 775          return $DB->record_exists_sql($sql, ['badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id]);
 776      }
 777  
 778      /**
 779       * Get related badges of badge.
 780       *
 781       * @param boolean $activeonly Do not get the inactive badges when is true.
 782       * @return array Related badges information.
 783       */
 784      public function get_related_badges($activeonly = false) {
 785          global $DB;
 786  
 787          $params = array('badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id);
 788          $query = "SELECT DISTINCT b.id, b.name, b.version, b.language, b.type
 789                      FROM {badge_related} br
 790                      JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
 791                     WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
 792          if ($activeonly) {
 793              $query .= " AND b.status <> :status";
 794              $params['status'] = BADGE_STATUS_INACTIVE;
 795          }
 796          $relatedbadges = $DB->get_records_sql($query, $params);
 797          return $relatedbadges;
 798      }
 799  
 800      /**
 801       * Insert/update alignment information of badge.
 802       *
 803       * @param stdClass $alignment Data of a alignment.
 804       * @param int $alignmentid ID alignment.
 805       * @return bool|int A status/ID when insert or update data.
 806       */
 807      public function save_alignment($alignment, $alignmentid = 0) {
 808          global $DB;
 809  
 810          $record = $DB->record_exists('badge_alignment', array('id' => $alignmentid));
 811          if ($record) {
 812              $alignment->id = $alignmentid;
 813              return $DB->update_record('badge_alignment', $alignment);
 814          } else {
 815              return $DB->insert_record('badge_alignment', $alignment, true);
 816          }
 817      }
 818  
 819      /**
 820       * Delete a alignment of badge.
 821       *
 822       * @param int $alignmentid ID alignment.
 823       * @return boolean A status for delete a alignment.
 824       */
 825      public function delete_alignment($alignmentid) {
 826          global $DB;
 827          return $DB->delete_records('badge_alignment', array('badgeid' => $this->id, 'id' => $alignmentid));
 828      }
 829  
 830      /**
 831       * Get alignments of badge.
 832       *
 833       * @return array List content alignments.
 834       */
 835      public function get_alignments() {
 836          global $DB;
 837          return $DB->get_records('badge_alignment', array('badgeid' => $this->id));
 838      }
 839  
 840      /**
 841       * Insert/update Endorsement information of badge.
 842       *
 843       * @param stdClass $endorsement Data of an endorsement.
 844       * @return bool|int A status/ID when insert or update data.
 845       */
 846      public function save_endorsement($endorsement) {
 847          global $DB;
 848          $record = $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
 849          if ($record) {
 850              $endorsement->id = $record->id;
 851              return $DB->update_record('badge_endorsement', $endorsement);
 852          } else {
 853              return $DB->insert_record('badge_endorsement', $endorsement, true);
 854          }
 855      }
 856  
 857      /**
 858       * Get endorsement of badge.
 859       *
 860       * @return array|stdClass Endorsement information.
 861       */
 862      public function get_endorsement() {
 863          global $DB;
 864          return $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
 865      }
 866  
 867      /**
 868       * Markdown language support for criteria.
 869       *
 870       * @return string $output Markdown content to output.
 871       */
 872      public function markdown_badge_criteria() {
 873          $agg = $this->get_aggregation_methods();
 874          if (empty($this->criteria)) {
 875              return get_string('nocriteria', 'badges');
 876          }
 877          $overalldescr = '';
 878          $overall = $this->criteria[BADGE_CRITERIA_TYPE_OVERALL];
 879          if (!empty($overall->description)) {
 880                  $overalldescr = format_text($overall->description, $overall->descriptionformat,
 881                      array('context' => $this->get_context())) . '\n';
 882          }
 883          // Get the condition string.
 884          if (count($this->criteria) == 2) {
 885              $condition = get_string('criteria_descr', 'badges');
 886          } else {
 887              $condition = get_string('criteria_descr_' . BADGE_CRITERIA_TYPE_OVERALL, 'badges',
 888                  core_text::strtoupper($agg[$this->get_aggregation_method()]));
 889          }
 890          unset($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
 891          $items = array();
 892          // If only one criterion left, make sure its description goe to the top.
 893          if (count($this->criteria) == 1) {
 894              $c = reset($this->criteria);
 895              if (!empty($c->description)) {
 896                  $overalldescr = $c->description . '\n';
 897              }
 898              if (count($c->params) == 1) {
 899                  $items[] = ' * ' . get_string('criteria_descr_single_' . $c->criteriatype, 'badges') .
 900                      $c->get_details();
 901              } else {
 902                  $items[] = '* ' . get_string('criteria_descr_' . $c->criteriatype, 'badges',
 903                          core_text::strtoupper($agg[$this->get_aggregation_method($c->criteriatype)])) .
 904                      $c->get_details();
 905              }
 906          } else {
 907              foreach ($this->criteria as $type => $c) {
 908                  $criteriadescr = '';
 909                  if (!empty($c->description)) {
 910                      $criteriadescr = $c->description;
 911                  }
 912                  if (count($c->params) == 1) {
 913                      $items[] = ' * ' . get_string('criteria_descr_single_' . $type, 'badges') .
 914                          $c->get_details() . $criteriadescr;
 915                  } else {
 916                      $items[] = '* ' . get_string('criteria_descr_' . $type, 'badges',
 917                              core_text::strtoupper($agg[$this->get_aggregation_method($type)])) .
 918                          $c->get_details() . $criteriadescr;
 919                  }
 920              }
 921          }
 922          return strip_tags($overalldescr . $condition . html_writer::alist($items, array(), 'ul'));
 923      }
 924  
 925      /**
 926       * Define issuer information by format Open Badges specification version 2.
 927       *
 928       * @param int $obversion OB version to use.
 929       * @return array Issuer informations of the badge.
 930       */
 931      public function get_badge_issuer(?int $obversion = null) {
 932          global $DB;
 933  
 934          $issuer = [];
 935          if ($obversion == OPEN_BADGES_V1) {
 936              $data = $DB->get_record('badge', ['id' => $this->id]);
 937              $issuer['name'] = $data->issuername;
 938              $issuer['url'] = $data->issuerurl;
 939              $issuer['email'] = $data->issuercontact;
 940          } else {
 941              $issuer['name'] = $this->issuername;
 942              $issuer['url'] = $this->issuerurl;
 943              $issuer['email'] = $this->issuercontact;
 944              $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
 945              $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
 946              $issuer['id'] = $issueridurl->out(false);
 947              $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
 948          }
 949  
 950          return $issuer;
 951      }
 952  }