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.
/lib/ -> badgeslib.php (source)

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   * Contains classes, functions and constants used in badges.
  19   *
  20   * @package    core
  21   * @subpackage badges
  22   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /* Include required award criteria library. */
  30  require_once($CFG->dirroot . '/badges/criteria/award_criteria.php');
  31  
  32  /* Include required user badge exporter */
  33  use core_badges\external\user_badge_exporter;
  34  
  35  /*
  36   * Number of records per page.
  37  */
  38  define('BADGE_PERPAGE', 50);
  39  
  40  /*
  41   * Badge award criteria aggregation method.
  42   */
  43  define('BADGE_CRITERIA_AGGREGATION_ALL', 1);
  44  
  45  /*
  46   * Badge award criteria aggregation method.
  47   */
  48  define('BADGE_CRITERIA_AGGREGATION_ANY', 2);
  49  
  50  /*
  51   * Inactive badge means that this badge cannot be earned and has not been awarded
  52   * yet. Its award criteria can be changed.
  53   */
  54  define('BADGE_STATUS_INACTIVE', 0);
  55  
  56  /*
  57   * Active badge means that this badge can we earned, but it has not been awarded
  58   * yet. Can be deactivated for the purpose of changing its criteria.
  59   */
  60  define('BADGE_STATUS_ACTIVE', 1);
  61  
  62  /*
  63   * Inactive badge can no longer be earned, but it has been awarded in the past and
  64   * therefore its criteria cannot be changed.
  65   */
  66  define('BADGE_STATUS_INACTIVE_LOCKED', 2);
  67  
  68  /*
  69   * Active badge means that it can be earned and has already been awarded to users.
  70   * Its criteria cannot be changed any more.
  71   */
  72  define('BADGE_STATUS_ACTIVE_LOCKED', 3);
  73  
  74  /*
  75   * Archived badge is considered deleted and can no longer be earned and is not
  76   * displayed in the list of all badges.
  77   */
  78  define('BADGE_STATUS_ARCHIVED', 4);
  79  
  80  /*
  81   * Badge type for site badges.
  82   */
  83  define('BADGE_TYPE_SITE', 1);
  84  
  85  /*
  86   * Badge type for course badges.
  87   */
  88  define('BADGE_TYPE_COURSE', 2);
  89  
  90  /*
  91   * Badge messaging schedule options.
  92   */
  93  define('BADGE_MESSAGE_NEVER', 0);
  94  define('BADGE_MESSAGE_ALWAYS', 1);
  95  define('BADGE_MESSAGE_DAILY', 2);
  96  define('BADGE_MESSAGE_WEEKLY', 3);
  97  define('BADGE_MESSAGE_MONTHLY', 4);
  98  
  99  /*
 100   * URL of backpack. Custom ones can be added.
 101   */
 102  define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2');
 103  define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
 104  
 105  /*
 106   * @deprecated since 3.9 (MDL-66357).
 107   */
 108  define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
 109  define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
 110  
 111  /*
 112   * Open Badges specifications.
 113   */
 114  define('OPEN_BADGES_V1', 1);
 115  define('OPEN_BADGES_V2', 2);
 116  define('OPEN_BADGES_V2P1', 2.1);
 117  
 118  /*
 119   * Only use for Open Badges 2.0 specification
 120   */
 121  define('OPEN_BADGES_V2_CONTEXT', 'https://w3id.org/openbadges/v2');
 122  define('OPEN_BADGES_V2_TYPE_ASSERTION', 'Assertion');
 123  define('OPEN_BADGES_V2_TYPE_BADGE', 'BadgeClass');
 124  define('OPEN_BADGES_V2_TYPE_ISSUER', 'Issuer');
 125  define('OPEN_BADGES_V2_TYPE_ENDORSEMENT', 'Endorsement');
 126  define('OPEN_BADGES_V2_TYPE_AUTHOR', 'Author');
 127  
 128  define('BACKPACK_MOVE_UP', -1);
 129  define('BACKPACK_MOVE_DOWN', 1);
 130  
 131  // Global badge class has been moved to the component namespace.
 132  class_alias('\core_badges\badge', 'badge');
 133  
 134  /**
 135   * Sends notifications to users about awarded badges.
 136   *
 137   * @param \core_badges\badge $badge Badge that was issued
 138   * @param int $userid Recipient ID
 139   * @param string $issued Unique hash of an issued badge
 140   * @param string $filepathhash File path hash of an issued badge for attachments
 141   */
 142  function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash) {
 143      global $CFG, $DB;
 144  
 145      $admin = get_admin();
 146      $userfrom = new stdClass();
 147      $userfrom->id = $admin->id;
 148      $userfrom->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : $admin->email;
 149      foreach (\core_user\fields::get_name_fields() as $addname) {
 150          $userfrom->$addname = !empty($CFG->badges_defaultissuername) ? '' : $admin->$addname;
 151      }
 152      $userfrom->firstname = !empty($CFG->badges_defaultissuername) ? $CFG->badges_defaultissuername : $admin->firstname;
 153      $userfrom->maildisplay = true;
 154  
 155      $issuedlink = html_writer::link(new moodle_url('/badges/badge.php', array('hash' => $issued)), $badge->name);
 156      $userto = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
 157  
 158      $params = new stdClass();
 159      $params->badgename = $badge->name;
 160      $params->username = fullname($userto);
 161      $params->badgelink = $issuedlink;
 162      $message = badge_message_from_template($badge->message, $params);
 163      $plaintext = html_to_text($message);
 164  
 165      // Notify recipient.
 166      $eventdata = new \core\message\message();
 167      $eventdata->courseid          = is_null($badge->courseid) ? SITEID : $badge->courseid; // Profile/site come with no courseid.
 168      $eventdata->component         = 'moodle';
 169      $eventdata->name              = 'badgerecipientnotice';
 170      $eventdata->userfrom          = $userfrom;
 171      $eventdata->userto            = $userto;
 172      $eventdata->notification      = 1;
 173      $eventdata->subject           = $badge->messagesubject;
 174      $eventdata->fullmessage       = $plaintext;
 175      $eventdata->fullmessageformat = FORMAT_HTML;
 176      $eventdata->fullmessagehtml   = $message;
 177      $eventdata->smallmessage      = '';
 178      $eventdata->customdata        = [
 179          'notificationiconurl' => moodle_url::make_pluginfile_url(
 180              $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
 181          'hash' => $issued,
 182      ];
 183  
 184      // Attach badge image if possible.
 185      if (!empty($CFG->allowattachments) && $badge->attachment && is_string($filepathhash)) {
 186          $fs = get_file_storage();
 187          $file = $fs->get_file_by_hash($filepathhash);
 188          $eventdata->attachment = $file;
 189          $eventdata->attachname = str_replace(' ', '_', $badge->name) . ".png";
 190  
 191          message_send($eventdata);
 192      } else {
 193          message_send($eventdata);
 194      }
 195  
 196      // Notify badge creator about the award if they receive notifications every time.
 197      if ($badge->notification == 1) {
 198          $userfrom = core_user::get_noreply_user();
 199          $userfrom->maildisplay = true;
 200  
 201          $creator = $DB->get_record('user', array('id' => $badge->usercreated), '*', MUST_EXIST);
 202          $a = new stdClass();
 203          $a->user = fullname($userto);
 204          $a->link = $issuedlink;
 205          $creatormessage = get_string('creatorbody', 'badges', $a);
 206          $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
 207  
 208          $eventdata = new \core\message\message();
 209          $eventdata->courseid          = $badge->courseid;
 210          $eventdata->component         = 'moodle';
 211          $eventdata->name              = 'badgecreatornotice';
 212          $eventdata->userfrom          = $userfrom;
 213          $eventdata->userto            = $creator;
 214          $eventdata->notification      = 1;
 215          $eventdata->subject           = $creatorsubject;
 216          $eventdata->fullmessage       = html_to_text($creatormessage);
 217          $eventdata->fullmessageformat = FORMAT_HTML;
 218          $eventdata->fullmessagehtml   = $creatormessage;
 219          $eventdata->smallmessage      = '';
 220          $eventdata->customdata        = [
 221              'notificationiconurl' => moodle_url::make_pluginfile_url(
 222                  $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
 223              'hash' => $issued,
 224          ];
 225  
 226          message_send($eventdata);
 227          $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $badge->id, 'userid' => $userid));
 228      }
 229  }
 230  
 231  /**
 232   * Caclulates date for the next message digest to badge creators.
 233   *
 234   * @param in $schedule Type of message schedule BADGE_MESSAGE_DAILY|BADGE_MESSAGE_WEEKLY|BADGE_MESSAGE_MONTHLY.
 235   * @return int Timestamp for next cron
 236   */
 237  function badges_calculate_message_schedule($schedule) {
 238      $nextcron = 0;
 239  
 240      switch ($schedule) {
 241          case BADGE_MESSAGE_DAILY:
 242              $tomorrow = new DateTime("1 day", core_date::get_server_timezone_object());
 243              $nextcron = $tomorrow->getTimestamp();
 244              break;
 245          case BADGE_MESSAGE_WEEKLY:
 246              $nextweek = new DateTime("1 week", core_date::get_server_timezone_object());
 247              $nextcron = $nextweek->getTimestamp();
 248              break;
 249          case BADGE_MESSAGE_MONTHLY:
 250              $nextmonth = new DateTime("1 month", core_date::get_server_timezone_object());
 251              $nextcron = $nextmonth->getTimestamp();
 252              break;
 253      }
 254  
 255      return $nextcron;
 256  }
 257  
 258  /**
 259   * Replaces variables in a message template and returns text ready to be emailed to a user.
 260   *
 261   * @param string $message Message body.
 262   * @return string Message with replaced values
 263   */
 264  function badge_message_from_template($message, $params) {
 265      $msg = $message;
 266      foreach ($params as $key => $value) {
 267          $msg = str_replace("%$key%", $value, $msg);
 268      }
 269  
 270      return $msg;
 271  }
 272  
 273  /**
 274   * Get all badges.
 275   *
 276   * @param int Type of badges to return
 277   * @param int Course ID for course badges
 278   * @param string $sort An SQL field to sort by
 279   * @param string $dir The sort direction ASC|DESC
 280   * @param int $page The page or records to return
 281   * @param int $perpage The number of records to return per page
 282   * @param int $user User specific search
 283   * @return array $badge Array of records matching criteria
 284   */
 285  function badges_get_badges($type, $courseid = 0, $sort = '', $dir = '', $page = 0, $perpage = BADGE_PERPAGE, $user = 0) {
 286      global $DB;
 287      $records = array();
 288      $params = array();
 289      $where = "b.status != :deleted AND b.type = :type ";
 290      $params['deleted'] = BADGE_STATUS_ARCHIVED;
 291  
 292      $userfields = array('b.id, b.name, b.status');
 293      $usersql = "";
 294      if ($user != 0) {
 295          $userfields[] = 'bi.dateissued';
 296          $userfields[] = 'bi.uniquehash';
 297          $usersql = " LEFT JOIN {badge_issued} bi ON b.id = bi.badgeid AND bi.userid = :userid ";
 298          $params['userid'] = $user;
 299          $where .= " AND (b.status = 1 OR b.status = 3) ";
 300      }
 301      $fields = implode(', ', $userfields);
 302  
 303      if ($courseid != 0 ) {
 304          $where .= "AND b.courseid = :courseid ";
 305          $params['courseid'] = $courseid;
 306      }
 307  
 308      $sorting = (($sort != '' && $dir != '') ? 'ORDER BY ' . $sort . ' ' . $dir : '');
 309      $params['type'] = $type;
 310  
 311      $sql = "SELECT $fields FROM {badge} b $usersql WHERE $where $sorting";
 312      $records = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
 313  
 314      $badges = array();
 315      foreach ($records as $r) {
 316          $badge = new badge($r->id);
 317          $badges[$r->id] = $badge;
 318          if ($user != 0) {
 319              $badges[$r->id]->dateissued = $r->dateissued;
 320              $badges[$r->id]->uniquehash = $r->uniquehash;
 321          } else {
 322              $badges[$r->id]->awards = $DB->count_records_sql('SELECT COUNT(b.userid)
 323                                          FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
 324                                          WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $badge->id));
 325              $badges[$r->id]->statstring = $badge->get_status_name();
 326          }
 327      }
 328      return $badges;
 329  }
 330  
 331  /**
 332   * Get badges for a specific user.
 333   *
 334   * @param int $userid User ID
 335   * @param int $courseid Badges earned by a user in a specific course
 336   * @param int $page The page or records to return
 337   * @param int $perpage The number of records to return per page
 338   * @param string $search A simple string to search for
 339   * @param bool $onlypublic Return only public badges
 340   * @return array of badges ordered by decreasing date of issue
 341   */
 342  function badges_get_user_badges($userid, $courseid = 0, $page = 0, $perpage = 0, $search = '', $onlypublic = false) {
 343      global $CFG, $DB;
 344  
 345      $params = array(
 346          'userid' => $userid
 347      );
 348      $sql = 'SELECT
 349                  bi.uniquehash,
 350                  bi.dateissued,
 351                  bi.dateexpire,
 352                  bi.id as issuedid,
 353                  bi.visible,
 354                  u.email,
 355                  b.*
 356              FROM
 357                  {badge} b,
 358                  {badge_issued} bi,
 359                  {user} u
 360              WHERE b.id = bi.badgeid
 361                  AND u.id = bi.userid
 362                  AND bi.userid = :userid';
 363  
 364      if (!empty($search)) {
 365          $sql .= ' AND (' . $DB->sql_like('b.name', ':search', false) . ') ';
 366          $params['search'] = '%'.$DB->sql_like_escape($search).'%';
 367      }
 368      if ($onlypublic) {
 369          $sql .= ' AND (bi.visible = 1) ';
 370      }
 371  
 372      if (empty($CFG->badges_allowcoursebadges)) {
 373          $sql .= ' AND b.courseid IS NULL';
 374      } else if ($courseid != 0) {
 375          $sql .= ' AND (b.courseid = :courseid) ';
 376          $params['courseid'] = $courseid;
 377      }
 378      $sql .= ' ORDER BY bi.dateissued DESC';
 379      $badges = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
 380  
 381      return $badges;
 382  }
 383  
 384  /**
 385   * Get badge by hash.
 386   *
 387   * @param string $hash
 388   * @return object|bool
 389   */
 390  function badges_get_badge_by_hash(string $hash): object|bool {
 391      global $DB;
 392      $sql = 'SELECT
 393                  bi.uniquehash,
 394                  bi.dateissued,
 395                  bi.userid,
 396                  bi.dateexpire,
 397                  bi.id as issuedid,
 398                  bi.visible,
 399                  u.email,
 400                  b.*
 401              FROM
 402                  {badge} b,
 403                  {badge_issued} bi,
 404                  {user} u
 405              WHERE b.id = bi.badgeid
 406                  AND u.id = bi.userid
 407                  AND ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40);
 408      $badge = $DB->get_record_sql($sql, ['hash' => $hash], IGNORE_MISSING);
 409      return $badge;
 410  }
 411  
 412  /**
 413   * Update badge instance to external functions.
 414   *
 415   * @param stdClass $badge
 416   * @param stdClass $user
 417   * @return object
 418   */
 419  function badges_prepare_badge_for_external(stdClass $badge, stdClass $user): object {
 420      global $PAGE, $USER;
 421      if ($badge->type == BADGE_TYPE_SITE) {
 422          $context = context_system::instance();
 423      } else {
 424          $context = context_course::instance($badge->courseid);
 425      }
 426      $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context);
 427      // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
 428      if ($USER->id != $user->id && !$canconfiguredetails) {
 429          $badge = (object) [
 430              'id'            => $badge->id,
 431              'name'          => $badge->name,
 432              'type'          => $badge->type,
 433              'description'   => $badge->description,
 434              'issuername'    => $badge->issuername,
 435              'issuerurl'     => $badge->issuerurl,
 436              'issuercontact' => $badge->issuercontact,
 437              'uniquehash'    => $badge->uniquehash,
 438              'dateissued'    => $badge->dateissued,
 439              'dateexpire'    => $badge->dateexpire,
 440              'version'       => $badge->version,
 441              'language'      => $badge->language,
 442              'imageauthorname'  => $badge->imageauthorname,
 443              'imageauthoremail' => $badge->imageauthoremail,
 444              'imageauthorurl'   => $badge->imageauthorurl,
 445              'imagecaption'     => $badge->imagecaption,
 446          ];
 447      }
 448  
 449      // Create a badge instance to be able to get the endorsement and other info.
 450      $badgeinstance = new badge($badge->id);
 451      $endorsement   = $badgeinstance->get_endorsement();
 452      $alignments    = $badgeinstance->get_alignments();
 453      $relatedbadges = $badgeinstance->get_related_badges();
 454  
 455      if (!$canconfiguredetails) {
 456          // Return only the properties visible by the user.
 457          if (!empty($alignments)) {
 458              foreach ($alignments as $alignment) {
 459                  unset($alignment->targetdescription);
 460                  unset($alignment->targetframework);
 461                  unset($alignment->targetcode);
 462              }
 463          }
 464  
 465          if (!empty($relatedbadges)) {
 466              foreach ($relatedbadges as $relatedbadge) {
 467                  unset($relatedbadge->version);
 468                  unset($relatedbadge->language);
 469                  unset($relatedbadge->type);
 470              }
 471          }
 472      }
 473  
 474      $related = [
 475          'context'       => $context,
 476          'endorsement'   => $endorsement ? $endorsement : null,
 477          'alignment'     => $alignments,
 478          'relatedbadges' => $relatedbadges,
 479      ];
 480  
 481      $exporter = new user_badge_exporter($badge, $related);
 482      return $exporter->export($PAGE->get_renderer('core'));
 483  }
 484  
 485  /**
 486   * Extends the course administration navigation with the Badges page
 487   *
 488   * @param navigation_node $coursenode
 489   * @param object $course
 490   */
 491  function badges_add_course_navigation(navigation_node $coursenode, stdClass $course) {
 492      global $CFG, $SITE;
 493  
 494      $coursecontext = context_course::instance($course->id);
 495      $isfrontpage = (!$coursecontext || $course->id == $SITE->id);
 496      $canmanage = has_any_capability(array('moodle/badges:viewawarded',
 497                                            'moodle/badges:createbadge',
 498                                            'moodle/badges:awardbadge',
 499                                            'moodle/badges:configurecriteria',
 500                                            'moodle/badges:configuremessages',
 501                                            'moodle/badges:configuredetails',
 502                                            'moodle/badges:deletebadge'), $coursecontext);
 503  
 504      if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) && !$isfrontpage && $canmanage) {
 505          $coursenode->add(get_string('coursebadges', 'badges'), null,
 506                  navigation_node::TYPE_CONTAINER, null, 'coursebadges',
 507                  new pix_icon('i/badge', get_string('coursebadges', 'badges')));
 508  
 509          $url = new moodle_url('/badges/index.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id));
 510  
 511          $coursenode->get('coursebadges')->add(get_string('managebadges', 'badges'), $url,
 512              navigation_node::TYPE_SETTING, null, 'coursebadges');
 513  
 514          if (has_capability('moodle/badges:createbadge', $coursecontext)) {
 515              $url = new moodle_url('/badges/newbadge.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id));
 516  
 517              $coursenode->get('coursebadges')->add(get_string('newbadge', 'badges'), $url,
 518                      navigation_node::TYPE_SETTING, null, 'newbadge');
 519          }
 520      }
 521  }
 522  
 523  /**
 524   * Triggered when badge is manually awarded.
 525   *
 526   * @param   object      $data
 527   * @return  boolean
 528   */
 529  function badges_award_handle_manual_criteria_review(stdClass $data) {
 530      $criteria = $data->crit;
 531      $userid = $data->userid;
 532      $badge = new badge($criteria->badgeid);
 533  
 534      if (!$badge->is_active() || $badge->is_issued($userid)) {
 535          return true;
 536      }
 537  
 538      if ($criteria->review($userid)) {
 539          $criteria->mark_complete($userid);
 540  
 541          if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
 542              $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
 543              $badge->issue($userid);
 544          }
 545      }
 546  
 547      return true;
 548  }
 549  
 550  /**
 551   * Process badge image from form data
 552   *
 553   * @param badge $badge Badge object
 554   * @param string $iconfile Original file
 555   */
 556  function badges_process_badge_image(badge $badge, $iconfile) {
 557      global $CFG, $USER;
 558      require_once($CFG->libdir. '/gdlib.php');
 559  
 560      if (!empty($CFG->gdversion)) {
 561          process_new_icon($badge->get_context(), 'badges', 'badgeimage', $badge->id, $iconfile, true);
 562          @unlink($iconfile);
 563  
 564          // Clean up file draft area after badge image has been saved.
 565          $context = context_user::instance($USER->id, MUST_EXIST);
 566          $fs = get_file_storage();
 567          $fs->delete_area_files($context->id, 'user', 'draft');
 568      }
 569  }
 570  
 571  /**
 572   * Print badge image.
 573   *
 574   * @param badge $badge Badge object
 575   * @param stdClass $context
 576   * @param string $size
 577   */
 578  function print_badge_image(badge $badge, stdClass $context, $size = 'small') {
 579      $fsize = ($size == 'small') ? 'f2' : 'f1';
 580  
 581      $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', $fsize, false);
 582      // Appending a random parameter to image link to forse browser reload the image.
 583      $imageurl->param('refresh', rand(1, 10000));
 584      $attributes = array('src' => $imageurl, 'alt' => s($badge->name), 'class' => 'activatebadge');
 585  
 586      return html_writer::empty_tag('img', $attributes);
 587  }
 588  
 589  /**
 590   * Bake issued badge.
 591   *
 592   * @param string $hash Unique hash of an issued badge.
 593   * @param int $badgeid ID of the original badge.
 594   * @param int $userid ID of badge recipient (optional).
 595   * @param boolean $pathhash Return file pathhash instead of image url (optional).
 596   * @return string|url Returns either new file path hash or new file URL
 597   */
 598  function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
 599      global $CFG, $USER;
 600      require_once (__DIR__ . '/../badges/lib/bakerlib.php');
 601  
 602      $badge = new badge($badgeid);
 603      $badge_context = $badge->get_context();
 604      $userid = ($userid) ? $userid : $USER->id;
 605      $user_context = context_user::instance($userid);
 606  
 607      $fs = get_file_storage();
 608      if (!$fs->file_exists($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png')) {
 609          if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f3.png')) {
 610              $contents = $file->get_content();
 611  
 612              $filehandler = new PNG_MetaDataHandler($contents);
 613              // For now, the site backpack OB version will be used as default.
 614              $obversion = badges_open_badges_backpack_api();
 615              $assertion = new core_badges_assertion($hash, $obversion);
 616              $assertionjson = json_encode($assertion->get_badge_assertion());
 617              if ($filehandler->check_chunks("iTXt", "openbadges")) {
 618                  // Add assertion URL iTXt chunk.
 619                  $newcontents = $filehandler->add_chunks("iTXt", "openbadges", $assertionjson);
 620                  $fileinfo = array(
 621                          'contextid' => $user_context->id,
 622                          'component' => 'badges',
 623                          'filearea' => 'userbadge',
 624                          'itemid' => $badge->id,
 625                          'filepath' => '/',
 626                          'filename' => $hash . '.png',
 627                  );
 628  
 629                  // Create a file with added contents.
 630                  $newfile = $fs->create_file_from_string($fileinfo, $newcontents);
 631                  if ($pathhash) {
 632                      return $newfile->get_pathnamehash();
 633                  }
 634              }
 635          } else {
 636              debugging('Error baking badge image!', DEBUG_DEVELOPER);
 637              return;
 638          }
 639      }
 640  
 641      // If file exists and we just need its path hash, return it.
 642      if ($pathhash) {
 643          $file = $fs->get_file($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png');
 644          return $file->get_pathnamehash();
 645      }
 646  
 647      $fileurl = moodle_url::make_pluginfile_url($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash, true);
 648      return $fileurl;
 649  }
 650  
 651  /**
 652   * Returns external backpack settings and badges from this backpack.
 653   *
 654   * This function first checks if badges for the user are cached and
 655   * tries to retrieve them from the cache. Otherwise, badges are obtained
 656   * through curl request to the backpack.
 657   *
 658   * @param int $userid Backpack user ID.
 659   * @param boolean $refresh Refresh badges collection in cache.
 660   * @return null|object Returns null is there is no backpack or object with backpack settings.
 661   */
 662  function get_backpack_settings($userid, $refresh = false) {
 663      global $DB;
 664  
 665      // Try to get badges from cache first.
 666      $badgescache = cache::make('core', 'externalbadges');
 667      $out = $badgescache->get($userid);
 668      if ($out !== false && !$refresh) {
 669          return $out;
 670      }
 671      // Get badges through curl request to the backpack.
 672      $record = $DB->get_record('badge_backpack', array('userid' => $userid));
 673      if ($record) {
 674          $sitebackpack = badges_get_site_backpack($record->externalbackpackid);
 675          $backpack = new \core_badges\backpack_api($sitebackpack, $record);
 676          $out = new stdClass();
 677          $out->backpackid = $sitebackpack->id;
 678  
 679          if ($collections = $DB->get_records('badge_external', array('backpackid' => $record->id))) {
 680              $out->totalcollections = count($collections);
 681              $out->totalbadges = 0;
 682              $out->badges = array();
 683              foreach ($collections as $collection) {
 684                  $badges = $backpack->get_badges($collection, true);
 685                  if (!empty($badges)) {
 686                      $out->badges = array_merge($out->badges, $badges);
 687                      $out->totalbadges += count($badges);
 688                  } else {
 689                      $out->badges = array_merge($out->badges, array());
 690                  }
 691              }
 692          } else {
 693              $out->totalbadges = 0;
 694              $out->totalcollections = 0;
 695          }
 696  
 697          $badgescache->set($userid, $out);
 698          return $out;
 699      }
 700  
 701      return null;
 702  }
 703  
 704  /**
 705   * Download all user badges in zip archive.
 706   *
 707   * @param int $userid ID of badge owner.
 708   */
 709  function badges_download($userid) {
 710      global $CFG, $DB;
 711      $context = context_user::instance($userid);
 712      $records = $DB->get_records('badge_issued', array('userid' => $userid));
 713  
 714      // Get list of files to download.
 715      $fs = get_file_storage();
 716      $filelist = array();
 717      foreach ($records as $issued) {
 718          $badge = new badge($issued->badgeid);
 719          // Need to make image name user-readable and unique using filename safe characters.
 720          $name =  $badge->name . ' ' . userdate($issued->dateissued, '%d %b %Y') . ' ' . hash('crc32', $badge->id);
 721          $name = str_replace(' ', '_', $name);
 722          $name = clean_param($name, PARAM_FILE);
 723          if ($file = $fs->get_file($context->id, 'badges', 'userbadge', $issued->badgeid, '/', $issued->uniquehash . '.png')) {
 724              $filelist[$name . '.png'] = $file;
 725          }
 726      }
 727  
 728      // Zip files and sent them to a user.
 729      $tempzip = tempnam($CFG->tempdir.'/', 'mybadges');
 730      $zipper = new zip_packer();
 731      if ($zipper->archive_to_pathname($filelist, $tempzip)) {
 732          send_temp_file($tempzip, 'badges.zip');
 733      } else {
 734          debugging("Problems with archiving the files.", DEBUG_DEVELOPER);
 735          die;
 736      }
 737  }
 738  
 739  /**
 740   * Checks if user has external backpack connected.
 741   *
 742   * @param int $userid ID of a user.
 743   * @return bool True|False whether backpack connection exists.
 744   */
 745  function badges_user_has_backpack($userid) {
 746      global $DB;
 747      return $DB->record_exists('badge_backpack', array('userid' => $userid));
 748  }
 749  
 750  /**
 751   * Handles what happens to the course badges when a course is deleted.
 752   *
 753   * @param int $courseid course ID.
 754   * @return void.
 755   */
 756  function badges_handle_course_deletion($courseid) {
 757      global $CFG, $DB;
 758      include_once $CFG->libdir . '/filelib.php';
 759  
 760      $systemcontext = context_system::instance();
 761      $coursecontext = context_course::instance($courseid);
 762      $fs = get_file_storage();
 763  
 764      // Move badges images to the system context.
 765      $fs->move_area_files_to_new_context($coursecontext->id, $systemcontext->id, 'badges', 'badgeimage');
 766  
 767      // Get all course badges.
 768      $badges = $DB->get_records('badge', array('type' => BADGE_TYPE_COURSE, 'courseid' => $courseid));
 769      foreach ($badges as $badge) {
 770          // Archive badges in this course.
 771          $toupdate = new stdClass();
 772          $toupdate->id = $badge->id;
 773          $toupdate->type = BADGE_TYPE_SITE;
 774          $toupdate->courseid = null;
 775          $toupdate->status = BADGE_STATUS_ARCHIVED;
 776          $DB->update_record('badge', $toupdate);
 777      }
 778  }
 779  
 780  /**
 781   * Create the site backpack with this data.
 782   *
 783   * @param stdClass $data The new backpack data.
 784   * @return boolean
 785   */
 786  function badges_create_site_backpack($data) {
 787      global $DB;
 788      $context = context_system::instance();
 789      require_capability('moodle/badges:manageglobalsettings', $context);
 790  
 791      $max = $DB->get_field_sql('SELECT MAX(sortorder) FROM {badge_external_backpack}');
 792      $data->sortorder = $max + 1;
 793  
 794      return badges_save_external_backpack($data);
 795  }
 796  
 797  /**
 798   * Update the backpack with this id.
 799   *
 800   * @param integer $id The backpack to edit
 801   * @param stdClass $data The new backpack data.
 802   * @return boolean
 803   */
 804  function badges_update_site_backpack($id, $data) {
 805      global $DB;
 806      $context = context_system::instance();
 807      require_capability('moodle/badges:manageglobalsettings', $context);
 808  
 809      if ($backpack = badges_get_site_backpack($id)) {
 810          $data->id = $id;
 811          return badges_save_external_backpack($data);
 812      }
 813      return false;
 814  }
 815  
 816  
 817  /**
 818   * Delete the backpack with this id.
 819   *
 820   * @param integer $id The backpack to delete.
 821   * @return boolean
 822   */
 823  function badges_delete_site_backpack($id) {
 824      global $DB;
 825  
 826      $context = context_system::instance();
 827      require_capability('moodle/badges:manageglobalsettings', $context);
 828  
 829      // Only remove site backpack if it's not the default one.
 830      $defaultbackpack = badges_get_site_primary_backpack();
 831      if ($defaultbackpack->id != $id && $DB->record_exists('badge_external_backpack', ['id' => $id])) {
 832          $transaction = $DB->start_delegated_transaction();
 833  
 834          // Remove connections for users to this backpack.
 835          $sql = "SELECT DISTINCT bb.id
 836                    FROM {badge_backpack} bb
 837                   WHERE bb.externalbackpackid = :backpackid";
 838          $params = ['backpackid' => $id];
 839          $userbackpacks = $DB->get_fieldset_sql($sql, $params);
 840          if ($userbackpacks) {
 841              // Delete user external collections references to this backpack.
 842              list($insql, $params) = $DB->get_in_or_equal($userbackpacks);
 843              $DB->delete_records_select('badge_external', "backpackid $insql", $params);
 844          }
 845          $DB->delete_records('badge_backpack', ['externalbackpackid' => $id]);
 846  
 847          // Delete backpack entry.
 848          $result = $DB->delete_records('badge_external_backpack', ['id' => $id]);
 849  
 850          $transaction->allow_commit();
 851  
 852          return $result;
 853      }
 854  
 855      return false;
 856  }
 857  
 858  /**
 859   * Perform the actual create/update of external bakpacks. Any checks on the validity of the id will need to be
 860   * performed before it reaches this function.
 861   *
 862   * @param stdClass $data The backpack data we are updating/inserting
 863   * @return int Returns the id of the new/updated record
 864   */
 865  function badges_save_external_backpack(stdClass $data) {
 866      global $DB;
 867      if ($data->apiversion == OPEN_BADGES_V2P1) {
 868          // Check if there is an existing issuer for the given backpackapiurl.
 869          foreach (core\oauth2\api::get_all_issuers() as $tmpissuer) {
 870              if ($data->backpackweburl == $tmpissuer->get('baseurl')) {
 871                  $issuer = $tmpissuer;
 872                  break;
 873              }
 874          }
 875  
 876          // Create the issuer if it doesn't exist yet.
 877          if (empty($issuer)) {
 878              $issuer = new \core\oauth2\issuer(0, (object) [
 879                  'name' => $data->backpackweburl,
 880                  'baseurl' => $data->backpackweburl,
 881                  // Note: This is required because the DB schema is broken and does not accept a null value when it should.
 882                  'image' => '',
 883              ]);
 884              $issuer->save();
 885          }
 886  
 887          // This can't be run from PHPUNIT because testing platforms need real URLs.
 888          // In the future, this request can be moved to the moodle-exttests repository.
 889          if (!PHPUNIT_TEST) {
 890              // Create/update the endpoints for the issuer.
 891              \core\oauth2\discovery\imsbadgeconnect::create_endpoints($issuer);
 892              $data->oauth2_issuerid = $issuer->get('id');
 893  
 894              $apibase = \core\oauth2\endpoint::get_record([
 895                  'issuerid' => $data->oauth2_issuerid,
 896                  'name' => 'apiBase',
 897              ]);
 898              $data->backpackapiurl = $apibase->get('url');
 899          }
 900      }
 901      $backpack = new stdClass();
 902  
 903      $backpack->apiversion = $data->apiversion;
 904      $backpack->backpackweburl = $data->backpackweburl;
 905      $backpack->backpackapiurl = $data->backpackapiurl;
 906      $backpack->oauth2_issuerid = $data->oauth2_issuerid ?? '';
 907      if (isset($data->sortorder)) {
 908          $backpack->sortorder = $data->sortorder;
 909      }
 910  
 911      if (empty($data->id)) {
 912          $backpack->id = $DB->insert_record('badge_external_backpack', $backpack);
 913      } else {
 914          $backpack->id = $data->id;
 915          $DB->update_record('badge_external_backpack', $backpack);
 916      }
 917      $data->externalbackpackid = $backpack->id;
 918  
 919      unset($data->id);
 920      badges_save_backpack_credentials($data);
 921      return $data->externalbackpackid;
 922  }
 923  
 924  /**
 925   * Create a backpack with the provided details. Stores the auth details of the backpack
 926   *
 927   * @param stdClass $data Backpack specific data.
 928   * @return int The id of the external backpack that the credentials correspond to
 929   */
 930  function badges_save_backpack_credentials(stdClass $data) {
 931      global $DB;
 932  
 933      if (isset($data->backpackemail) && isset($data->password)) {
 934          $backpack = new stdClass();
 935  
 936          $backpack->email = $data->backpackemail;
 937          $backpack->password = !empty($data->password) ? $data->password : '';
 938          $backpack->externalbackpackid = $data->externalbackpackid;
 939          $backpack->userid = $data->userid ?? 0;
 940          $backpack->backpackuid = $data->backpackuid ?? 0;
 941          $backpack->autosync = $data->autosync ?? 0;
 942  
 943          if (!empty($data->badgebackpack)) {
 944              $backpack->id = $data->badgebackpack;
 945          } else if (!empty($data->id)) {
 946              $backpack->id = $data->id;
 947          }
 948  
 949          if (empty($backpack->id)) {
 950              $backpack->id = $DB->insert_record('badge_backpack', $backpack);
 951          } else {
 952              $DB->update_record('badge_backpack', $backpack);
 953          }
 954  
 955          return $backpack->externalbackpackid;
 956      }
 957  
 958      return $data->externalbackpackid ?? 0;
 959  }
 960  
 961  /**
 962   * Is any backpack enabled that supports open badges V1?
 963   * @param int|null $backpackid Check the version of the given id OR if null the sitewide backpack
 964   * @return boolean
 965   */
 966  function badges_open_badges_backpack_api(?int $backpackid = null) {
 967      if (!$backpackid) {
 968          $backpack = badges_get_site_primary_backpack();
 969      } else {
 970          $backpack = badges_get_site_backpack($backpackid);
 971      }
 972  
 973      if (empty($backpack->apiversion)) {
 974          return OPEN_BADGES_V2;
 975      }
 976      return $backpack->apiversion;
 977  }
 978  
 979  /**
 980   * Get a site backpacks by id for a particular user or site (if userid is 0)
 981   *
 982   * @param int $id The backpack id.
 983   * @param int $userid The owner of the backpack, 0 if it's a sitewide backpack else a user's site backpack
 984   * @return stdClass
 985   */
 986  function badges_get_site_backpack($id, int $userid = 0) {
 987      global $DB;
 988  
 989      $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
 990                FROM {badge_external_backpack} beb
 991           LEFT JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid
 992               WHERE beb.id=:id";
 993  
 994      return $DB->get_record_sql($sql, ['id' => $id, 'userid' => $userid]);
 995  }
 996  
 997  /**
 998   * Get the user backpack for the currently logged in user OR the provided user
 999   *
1000   * @param int|null $userid The user whose backpack you're requesting for. If null, get the logged in user's backpack
1001   * @return mixed The user's backpack or none.
1002   * @throws dml_exception
1003   */
1004  function badges_get_user_backpack(?int $userid = 0) {
1005      global $DB;
1006  
1007      if (!$userid) {
1008          global $USER;
1009          $userid = $USER->id;
1010      }
1011  
1012      $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
1013                FROM {badge_external_backpack} beb
1014                JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid";
1015  
1016      return $DB->get_record_sql($sql, ['userid' => $userid]);
1017  }
1018  
1019  /**
1020   * Get the primary backpack for the site
1021   *
1022   * @return stdClass
1023   */
1024  function badges_get_site_primary_backpack() {
1025      global $DB;
1026  
1027      $sql = 'SELECT *
1028                FROM {badge_external_backpack}
1029               WHERE sortorder = (SELECT MIN(sortorder)
1030                                    FROM {badge_external_backpack} b2)';
1031      $firstbackpack = $DB->get_record_sql($sql, null, MUST_EXIST);
1032  
1033      return badges_get_site_backpack($firstbackpack->id);
1034  }
1035  
1036  /**
1037   * List the backpacks at site level.
1038   *
1039   * @return array(stdClass)
1040   */
1041  function badges_get_site_backpacks() {
1042      global $DB;
1043  
1044      $defaultbackpack = badges_get_site_primary_backpack();
1045      $all = $DB->get_records('badge_external_backpack', null, 'sortorder ASC');
1046      foreach ($all as $key => $bp) {
1047          if ($bp->id == $defaultbackpack->id) {
1048              $all[$key]->sitebackpack = true;
1049          } else {
1050              $all[$key]->sitebackpack = false;
1051          }
1052      }
1053  
1054      return $all;
1055  }
1056  
1057  /**
1058   * Moves the backpack in the list one position up or down.
1059   *
1060   * @param int $backpackid The backpack identifier to be moved.
1061   * @param int $direction The direction (BACKPACK_MOVE_UP/BACKPACK_MOVE_DOWN) where to move the backpack.
1062   *
1063   * @throws \moodle_exception if attempting to use invalid direction value.
1064   */
1065  function badges_change_sortorder_backpacks(int $backpackid, int $direction): void {
1066      global $DB;
1067  
1068      if ($direction != BACKPACK_MOVE_UP && $direction != BACKPACK_MOVE_DOWN) {
1069          throw new \coding_exception(
1070              'Must use a valid backpack API move direction constant (BACKPACK_MOVE_UP or BACKPACK_MOVE_DOWN)');
1071      }
1072  
1073      $backpacks = badges_get_site_backpacks();
1074      $backpacktoupdate = $backpacks[$backpackid];
1075  
1076      $currentsortorder = $backpacktoupdate->sortorder;
1077      $targetsortorder = $currentsortorder + $direction;
1078      if ($targetsortorder > 0 && $targetsortorder <= count($backpacks) ) {
1079          foreach ($backpacks as $backpack) {
1080              if ($backpack->sortorder == $targetsortorder) {
1081                  $backpack->sortorder = $backpack->sortorder - $direction;
1082                  $DB->update_record('badge_external_backpack', $backpack);
1083                  break;
1084              }
1085          }
1086          $backpacktoupdate->sortorder = $targetsortorder;
1087          $DB->update_record('badge_external_backpack', $backpacktoupdate);
1088      }
1089  }
1090  
1091  /**
1092   * List the supported badges api versions.
1093   *
1094   * @return array(version)
1095   */
1096  function badges_get_badge_api_versions() {
1097      return [
1098          (string)OPEN_BADGES_V1 => get_string('openbadgesv1', 'badges'),
1099          (string)OPEN_BADGES_V2 => get_string('openbadgesv2', 'badges'),
1100          (string)OPEN_BADGES_V2P1 => get_string('openbadgesv2p1', 'badges')
1101      ];
1102  }
1103  
1104  /**
1105   * Get the default issuer for a badge from this site.
1106   *
1107   * @return array
1108   */
1109  function badges_get_default_issuer() {
1110      global $CFG, $SITE;
1111  
1112      $sitebackpack = badges_get_site_primary_backpack();
1113      $issuer = array();
1114      $issuerurl = new moodle_url('/');
1115      $issuer['name'] = $CFG->badges_defaultissuername;
1116      if (empty($issuer['name'])) {
1117          $issuer['name'] = $SITE->fullname ? $SITE->fullname : $SITE->shortname;
1118      }
1119      $issuer['url'] = $issuerurl->out(false);
1120      $issuer['email'] = $sitebackpack->backpackemail ?: $CFG->badges_defaultissuercontact;
1121      $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
1122      $issuerid = new moodle_url('/badges/issuer_json.php');
1123      $issuer['id'] = $issuerid->out(false);
1124      $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
1125      return $issuer;
1126  }
1127  
1128  /**
1129   * Disconnect from the user backpack by deleting the user preferences.
1130   *
1131   * @param integer $userid The user to diconnect.
1132   * @return boolean
1133   */
1134  function badges_disconnect_user_backpack($userid) {
1135      global $USER;
1136  
1137      // We can only change backpack settings for our own real backpack.
1138      if ($USER->id != $userid ||
1139              \core\session\manager::is_loggedinas()) {
1140  
1141          return false;
1142      }
1143  
1144      unset_user_preference('badges_email_verify_secret');
1145      unset_user_preference('badges_email_verify_address');
1146      unset_user_preference('badges_email_verify_backpackid');
1147      unset_user_preference('badges_email_verify_password');
1148  
1149      return true;
1150  }
1151  
1152  /**
1153   * Used to remember which objects we connected with a backpack before.
1154   *
1155   * @param integer $sitebackpackid The site backpack to connect to.
1156   * @param string $type The type of this remote object.
1157   * @param string $internalid The id for this object on the Moodle site.
1158   * @param string $param The param we need to return. Defaults to the externalid.
1159   * @return mixed The id or false if it doesn't exist.
1160   */
1161  function badges_external_get_mapping($sitebackpackid, $type, $internalid, $param = 'externalid') {
1162      global $DB;
1163      // Return externalid if it exists.
1164      $params = [
1165          'sitebackpackid' => $sitebackpackid,
1166          'type' => $type,
1167          'internalid' => $internalid
1168      ];
1169  
1170      $record = $DB->get_record('badge_external_identifier', $params, $param, IGNORE_MISSING);
1171      if ($record) {
1172          return $record->$param;
1173      }
1174      return false;
1175  }
1176  
1177  /**
1178   * Save the info about which objects we connected with a backpack before.
1179   *
1180   * @param integer $sitebackpackid The site backpack to connect to.
1181   * @param string $type The type of this remote object.
1182   * @param string $internalid The id for this object on the Moodle site.
1183   * @param string $externalid The id of this object on the remote site.
1184   * @return boolean
1185   */
1186  function badges_external_create_mapping($sitebackpackid, $type, $internalid, $externalid) {
1187      global $DB;
1188  
1189      $params = [
1190          'sitebackpackid' => $sitebackpackid,
1191          'type' => $type,
1192          'internalid' => $internalid,
1193          'externalid' => $externalid
1194      ];
1195  
1196      return $DB->insert_record('badge_external_identifier', $params);
1197  }
1198  
1199  /**
1200   * Delete all external mapping information for a backpack.
1201   *
1202   * @param integer $sitebackpackid The site backpack to connect to.
1203   * @return boolean
1204   */
1205  function badges_external_delete_mappings($sitebackpackid) {
1206      global $DB;
1207  
1208      $params = ['sitebackpackid' => $sitebackpackid];
1209  
1210      return $DB->delete_records('badge_external_identifier', $params);
1211  }
1212  
1213  /**
1214   * Delete a specific external mapping information for a backpack.
1215   *
1216   * @param integer $sitebackpackid The site backpack to connect to.
1217   * @param string $type The type of this remote object.
1218   * @param string $internalid The id for this object on the Moodle site.
1219   * @return boolean
1220   */
1221  function badges_external_delete_mapping($sitebackpackid, $type, $internalid) {
1222      global $DB;
1223  
1224      $params = [
1225          'sitebackpackid' => $sitebackpackid,
1226          'type' => $type,
1227          'internalid' => $internalid
1228      ];
1229  
1230      $DB->delete_record('badge_external_identifier', $params);
1231  }
1232  
1233  /**
1234   * Create and send a verification email to the email address supplied.
1235   *
1236   * Since we're not sending this email to a user, email_to_user can't be used
1237   * but this function borrows largely the code from that process.
1238   *
1239   * @param string $email the email address to send the verification email to.
1240   * @param int $backpackid the id of the backpack to connect to
1241   * @param string $backpackpassword the user entered password to connect to this backpack
1242   * @return true if the email was sent successfully, false otherwise.
1243   */
1244  function badges_send_verification_email($email, $backpackid, $backpackpassword) {
1245      global $DB, $USER;
1246  
1247      // Store a user secret (badges_email_verify_secret) and the address (badges_email_verify_address) as users prefs.
1248      // The address will be used by edit_backpack_form for display during verification and to facilitate the resending
1249      // of verification emails to said address.
1250      $secret = random_string(15);
1251      set_user_preference('badges_email_verify_secret', $secret);
1252      set_user_preference('badges_email_verify_address', $email);
1253      set_user_preference('badges_email_verify_backpackid', $backpackid);
1254      set_user_preference('badges_email_verify_password', $backpackpassword);
1255  
1256      // To, from.
1257      $tempuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST);
1258      $tempuser->email = $email;
1259      $noreplyuser = core_user::get_noreply_user();
1260  
1261      // Generate the verification email body.
1262      $verificationurl = '/badges/backpackemailverify.php';
1263      $verificationurl = new moodle_url($verificationurl);
1264      $verificationpath = $verificationurl->out(false);
1265  
1266      $site = get_site();
1267      $link = $verificationpath . '?data='. $secret;
1268      // Hard-coded button styles, because CSS can't be used in emails.
1269      $buttonstyles = [
1270          'background-color: #0f6cbf',
1271          'border: none',
1272          'color: white',
1273          'padding: 12px',
1274          'text-align: center',
1275          'text-decoration: none',
1276          'display: inline-block',
1277          'font-size: 20px',
1278          'font-weight: 800',
1279          'margin: 4px 2px',
1280          'cursor: pointer',
1281          'border-radius: 8px',
1282      ];
1283      $button = html_writer::start_tag('center') .
1284          html_writer::tag(
1285              'button',
1286              get_string('verifyemail', 'badges'),
1287              ['style' => implode(';', $buttonstyles)]) .
1288          html_writer::end_tag('center');
1289      $args = [
1290          'link' => html_writer::link($link, $link),
1291          'buttonlink' => html_writer::link($link, $button),
1292          'sitename' => $site->fullname,
1293          'admin' => generate_email_signoff(),
1294          'userfirstname' => $USER->firstname,
1295      ];
1296  
1297      $messagesubject = get_string('backpackemailverifyemailsubject', 'badges', $site->fullname);
1298      $messagetext = get_string('backpackemailverifyemailbody', 'badges', $args);
1299      $messagehtml = text_to_html($messagetext, false, false, true);
1300  
1301      return email_to_user($tempuser, $noreplyuser, $messagesubject, $messagetext, $messagehtml);
1302  }
1303  
1304  /**
1305   * Return all the enabled criteria types for this site.
1306   *
1307   * @param boolean $enabled
1308   * @return array
1309   */
1310  function badges_list_criteria($enabled = true) {
1311      global $CFG;
1312  
1313      $types = array(
1314          BADGE_CRITERIA_TYPE_OVERALL    => 'overall',
1315          BADGE_CRITERIA_TYPE_ACTIVITY   => 'activity',
1316          BADGE_CRITERIA_TYPE_MANUAL     => 'manual',
1317          BADGE_CRITERIA_TYPE_SOCIAL     => 'social',
1318          BADGE_CRITERIA_TYPE_COURSE     => 'course',
1319          BADGE_CRITERIA_TYPE_COURSESET  => 'courseset',
1320          BADGE_CRITERIA_TYPE_PROFILE    => 'profile',
1321          BADGE_CRITERIA_TYPE_BADGE      => 'badge',
1322          BADGE_CRITERIA_TYPE_COHORT     => 'cohort',
1323          BADGE_CRITERIA_TYPE_COMPETENCY => 'competency',
1324      );
1325      if ($enabled) {
1326          foreach ($types as $key => $type) {
1327              $class = 'award_criteria_' . $type;
1328              $file = $CFG->dirroot . '/badges/criteria/' . $class . '.php';
1329              if (file_exists($file)) {
1330                  require_once($file);
1331  
1332                  if (!$class::is_enabled()) {
1333                      unset($types[$key]);
1334                  }
1335              }
1336          }
1337      }
1338      return $types;
1339  }
1340  
1341  /**
1342   * Check if any badge has records for competencies.
1343   *
1344   * @param array $competencyids Array of competencies ids.
1345   * @return boolean Return true if competencies were found in any badge.
1346   */
1347  function badge_award_criteria_competency_has_records_for_competencies($competencyids) {
1348      global $DB;
1349  
1350      list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED);
1351  
1352      $sql = "SELECT DISTINCT bc.badgeid
1353                  FROM {badge_criteria} bc
1354                  JOIN {badge_criteria_param} bcp ON bc.id = bcp.critid
1355                  WHERE bc.criteriatype = :criteriatype AND bcp.value $insql";
1356      $params['criteriatype'] = BADGE_CRITERIA_TYPE_COMPETENCY;
1357  
1358      return $DB->record_exists_sql($sql, $params);
1359  }
1360  
1361  /**
1362   * Creates single message for all notification and sends it out
1363   *
1364   * @param object $badge A badge which is notified about.
1365   */
1366  function badge_assemble_notification(stdClass $badge) {
1367      global $DB;
1368  
1369      $userfrom = core_user::get_noreply_user();
1370      $userfrom->maildisplay = true;
1371  
1372      if ($msgs = $DB->get_records_select('badge_issued', 'issuernotified IS NULL AND badgeid = ?', array($badge->id))) {
1373          // Get badge creator.
1374          $creator = $DB->get_record('user', array('id' => $badge->creator), '*', MUST_EXIST);
1375          $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
1376          $creatormessage = '';
1377  
1378          // Put all messages in one digest.
1379          foreach ($msgs as $msg) {
1380              $issuedlink = html_writer::link(new moodle_url('/badges/badge.php', array('hash' => $msg->uniquehash)), $badge->name);
1381              $recipient = $DB->get_record('user', array('id' => $msg->userid), '*', MUST_EXIST);
1382  
1383              $a = new stdClass();
1384              $a->user = fullname($recipient);
1385              $a->link = $issuedlink;
1386              $creatormessage .= get_string('creatorbody', 'badges', $a);
1387              $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $msg->badgeid, 'userid' => $msg->userid));
1388          }
1389  
1390          // Create a message object.
1391          $eventdata = new \core\message\message();
1392          $eventdata->courseid          = SITEID;
1393          $eventdata->component         = 'moodle';
1394          $eventdata->name              = 'badgecreatornotice';
1395          $eventdata->userfrom          = $userfrom;
1396          $eventdata->userto            = $creator;
1397          $eventdata->notification      = 1;
1398          $eventdata->subject           = $creatorsubject;
1399          $eventdata->fullmessage       = format_text_email($creatormessage, FORMAT_HTML);
1400          $eventdata->fullmessageformat = FORMAT_PLAIN;
1401          $eventdata->fullmessagehtml   = $creatormessage;
1402          $eventdata->smallmessage      = $creatorsubject;
1403  
1404          message_send($eventdata);
1405      }
1406  }
1407  
1408  /**
1409   * Attempt to authenticate with the site backpack credentials and return an error
1410   * if the authentication fails. If external backpacks are not enabled, this will
1411   * not perform any test.
1412   *
1413   * @return string
1414   */
1415  function badges_verify_site_backpack() {
1416      $defaultbackpack = badges_get_site_primary_backpack();
1417      return badges_verify_backpack($defaultbackpack->id);
1418  }
1419  
1420  /**
1421   * Attempt to authenticate with a backpack credentials and return an error
1422   * if the authentication fails.
1423   * If external backpacks are not enabled or the backpack version is different
1424   * from OBv2, this will not perform any test.
1425   *
1426   * @param int $backpackid Backpack identifier to verify.
1427   * @return string The result of the verification process.
1428   */
1429  function badges_verify_backpack(int $backpackid) {
1430      global $OUTPUT, $CFG;
1431  
1432      if (empty($CFG->badges_allowexternalbackpack)) {
1433          return '';
1434      }
1435  
1436      $backpack = badges_get_site_backpack($backpackid);
1437      if (empty($backpack->apiversion) || ($backpack->apiversion == OPEN_BADGES_V2)) {
1438          $backpackapi = new \core_badges\backpack_api($backpack);
1439  
1440          // Clear any cached access tokens in the session.
1441          $backpackapi->clear_system_user_session();
1442  
1443          // Now attempt a login with these credentials.
1444          $result = $backpackapi->authenticate();
1445          if (empty($result) || !empty($result->error)) {
1446              $warning = $backpackapi->get_authentication_error();
1447  
1448              $params = ['id' => $backpack->id, 'action' => 'edit'];
1449              $backpackurl = (new moodle_url('/badges/backpacks.php', $params))->out(false);
1450  
1451              $message = get_string('sitebackpackwarning', 'badges', ['url' => $backpackurl, 'warning' => $warning]);
1452              $icon = $OUTPUT->pix_icon('i/warning', get_string('warning', 'moodle'));
1453              return $OUTPUT->container($icon . $message, 'text-danger');
1454          }
1455      }
1456  
1457      return '';
1458  }
1459  
1460  /**
1461   * Generate a public badgr URL that conforms to OBv2. This is done because badgr responses do not currently conform to
1462   * the spec.
1463   *
1464   * WARNING: This is an extremely hacky way of implementing this and should be removed once the standards are conformed to.
1465   *
1466   * @param stdClass $backpack The Badgr backpack we are pushing to
1467   * @param string $type The type of object we are dealing with either Issuer, Assertion OR Badge.
1468   * @param string $externalid The externalid as provided by the backpack
1469   * @return string The public URL to access Badgr objects
1470   */
1471  function badges_generate_badgr_open_url($backpack, $type, $externalid) {
1472      if (badges_open_badges_backpack_api($backpack->id) == OPEN_BADGES_V2) {
1473          $entity = strtolower($type);
1474          if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
1475              $entity = "badge";
1476          }
1477          $url = new moodle_url($backpack->backpackapiurl);
1478          return "{$url->get_scheme()}://{$url->get_host()}/public/{$entity}s/$externalid";
1479  
1480      }
1481  }