Search moodle.org's
Developer Documentation

See Release Notes

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

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