Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • /lib/ -> badgeslib.php (source)

    Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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