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