Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * Data provider.
  19   *
  20   * @package    core_badges
  21   * @copyright  2018 Frédéric Massart
  22   * @author     Frédéric Massart <fred@branchup.tech>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core_badges\privacy;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  use badge;
  31  use context;
  32  use context_course;
  33  use context_helper;
  34  use context_system;
  35  use context_user;
  36  use core_text;
  37  use core_privacy\local\metadata\collection;
  38  use core_privacy\local\request\approved_contextlist;
  39  use core_privacy\local\request\transform;
  40  use core_privacy\local\request\writer;
  41  use core_privacy\local\request\userlist;
  42  use core_privacy\local\request\approved_userlist;
  43  
  44  require_once($CFG->libdir . '/badgeslib.php');
  45  
  46  /**
  47   * Data provider class.
  48   *
  49   * @package    core_badges
  50   * @copyright  2018 Frédéric Massart
  51   * @author     Frédéric Massart <fred@branchup.tech>
  52   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  53   */
  54  class provider implements
  55      \core_privacy\local\metadata\provider,
  56      \core_privacy\local\request\core_userlist_provider,
  57      \core_privacy\local\request\subsystem\provider {
  58  
  59      /**
  60       * Returns metadata.
  61       *
  62       * @param collection $collection The initialised collection to add items to.
  63       * @return collection A listing of user data stored through this system.
  64       */
  65      public static function get_metadata(collection $collection) : collection {
  66  
  67          $collection->add_database_table('badge', [
  68              'usercreated' => 'privacy:metadata:badge:usercreated',
  69              'usermodified' => 'privacy:metadata:badge:usermodified',
  70              'timecreated' => 'privacy:metadata:badge:timecreated',
  71              'timemodified' => 'privacy:metadata:badge:timemodified',
  72          ], 'privacy:metadata:badge');
  73  
  74          $collection->add_database_table('badge_issued', [
  75              'userid' => 'privacy:metadata:issued:userid',
  76              'dateissued' => 'privacy:metadata:issued:dateissued',
  77              'dateexpire' => 'privacy:metadata:issued:dateexpire',
  78          ], 'privacy:metadata:issued');
  79  
  80          $collection->add_database_table('badge_criteria_met', [
  81              'userid' => 'privacy:metadata:criteriamet:userid',
  82              'datemet' => 'privacy:metadata:criteriamet:datemet',
  83          ], 'privacy:metadata:criteriamet');
  84  
  85          $collection->add_database_table('badge_manual_award', [
  86              'recipientid' => 'privacy:metadata:manualaward:recipientid',
  87              'issuerid' => 'privacy:metadata:manualaward:issuerid',
  88              'issuerrole' => 'privacy:metadata:manualaward:issuerrole',
  89              'datemet' => 'privacy:metadata:manualaward:datemet',
  90          ], 'privacy:metadata:manualaward');
  91  
  92          $collection->add_database_table('badge_backpack', [
  93              'userid' => 'privacy:metadata:backpack:userid',
  94              'email' => 'privacy:metadata:backpack:email',
  95              'externalbackpackid' => 'privacy:metadata:backpack:externalbackpackid',
  96              'backpackuid' => 'privacy:metadata:backpack:backpackuid',
  97              // The columns autosync and password are not used.
  98          ], 'privacy:metadata:backpack');
  99  
 100          $collection->add_external_location_link('backpacks', [
 101              'name' => 'privacy:metadata:external:backpacks:badge',
 102              'description' => 'privacy:metadata:external:backpacks:description',
 103              'image' => 'privacy:metadata:external:backpacks:image',
 104              'url' => 'privacy:metadata:external:backpacks:url',
 105              'issuer' => 'privacy:metadata:external:backpacks:issuer',
 106          ], 'privacy:metadata:external:backpacks');
 107  
 108          $collection->add_database_table('badge_backpack_oauth2', [
 109              'userid' => 'privacy:metadata:backpackoauth2:userid',
 110              'usermodified' => 'privacy:metadata:backpackoauth2:usermodified',
 111              'token' => 'privacy:metadata:backpackoauth2:token',
 112              'issuerid' => 'privacy:metadata:backpackoauth2:issuerid',
 113              'scope' => 'privacy:metadata:backpackoauth2:scope',
 114          ], 'privacy:metadata:backpackoauth2');
 115  
 116          return $collection;
 117      }
 118  
 119      /**
 120       * Get the list of contexts that contain user information for the specified user.
 121       *
 122       * @param int $userid The user to search.
 123       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
 124       */
 125      public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
 126          $contextlist = new \core_privacy\local\request\contextlist();
 127  
 128          // Find the modifications we made on badges (course & system).
 129          $sql = "
 130              SELECT ctx.id
 131                FROM {badge} b
 132                JOIN {context} ctx
 133                  ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel)
 134                  OR (b.type = :typesite AND ctx.id = :syscontextid)
 135               WHERE b.usermodified = :userid1
 136                  OR b.usercreated = :userid2";
 137          $params = [
 138              'courselevel' => CONTEXT_COURSE,
 139              'syscontextid' => SYSCONTEXTID,
 140              'typecourse' => BADGE_TYPE_COURSE,
 141              'typesite' => BADGE_TYPE_SITE,
 142              'userid1' => $userid,
 143              'userid2' => $userid,
 144          ];
 145          $contextlist->add_from_sql($sql, $params);
 146  
 147          // Find where we've manually awarded a badge (recipient user context).
 148          $sql = "
 149              SELECT ctx.id
 150                FROM {badge_manual_award} bma
 151                JOIN {context} ctx
 152                  ON ctx.instanceid = bma.recipientid
 153                 AND ctx.contextlevel = :userlevel
 154               WHERE bma.issuerid = :userid";
 155          $params = [
 156              'userlevel' => CONTEXT_USER,
 157              'userid' => $userid,
 158          ];
 159          $contextlist->add_from_sql($sql, $params);
 160  
 161          // Now find where there is real user data (user context).
 162          $sql = "
 163              SELECT ctx.id
 164                FROM {context} ctx
 165           LEFT JOIN {badge_manual_award} bma
 166                  ON bma.recipientid = ctx.instanceid
 167           LEFT JOIN {badge_issued} bi
 168                  ON bi.userid = ctx.instanceid
 169           LEFT JOIN {badge_criteria_met} bcm
 170                  ON bcm.userid = ctx.instanceid
 171           LEFT JOIN {badge_backpack} bb
 172                  ON bb.userid = ctx.instanceid
 173               WHERE ctx.contextlevel = :userlevel
 174                 AND ctx.instanceid = :userid
 175                 AND (bma.id IS NOT NULL
 176                  OR bi.id IS NOT NULL
 177                  OR bcm.id IS NOT NULL
 178                  OR bb.id IS NOT NULL)";
 179          $params = [
 180              'userlevel' => CONTEXT_USER,
 181              'userid' => $userid,
 182          ];
 183          $contextlist->add_from_sql($sql, $params);
 184  
 185          return $contextlist;
 186      }
 187  
 188      /**
 189       * Get the list of users within a specific context.
 190       *
 191       * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
 192       */
 193      public static function get_users_in_context(userlist $userlist) {
 194          $context = $userlist->get_context();
 195  
 196          $allowedcontexts = [
 197              CONTEXT_COURSE,
 198              CONTEXT_SYSTEM,
 199              CONTEXT_USER
 200          ];
 201  
 202          if (!in_array($context->contextlevel, $allowedcontexts)) {
 203              return;
 204          }
 205  
 206          if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_SYSTEM) {
 207              // Find the modifications we made on badges (course & system).
 208              if ($context->contextlevel == CONTEXT_COURSE) {
 209                  $extrawhere = 'AND b.courseid = :courseid';
 210                  $params = [
 211                      'badgetype' => BADGE_TYPE_COURSE,
 212                      'courseid'  => $context->instanceid
 213                  ];
 214              } else {
 215                  $extrawhere = '';
 216                  $params = ['badgetype' => BADGE_TYPE_SITE];
 217              }
 218  
 219              $sql = "SELECT b.usermodified, b.usercreated
 220                        FROM {badge} b
 221                       WHERE b.type = :badgetype
 222                             $extrawhere";
 223  
 224              $userlist->add_from_sql('usermodified', $sql, $params);
 225              $userlist->add_from_sql('usercreated', $sql, $params);
 226          }
 227  
 228          if ($context->contextlevel == CONTEXT_USER) {
 229              // Find where we've manually awarded a badge (recipient user context).
 230              $params = [
 231                  'instanceid' => $context->instanceid
 232              ];
 233  
 234              $sql = "SELECT issuerid, recipientid
 235                        FROM {badge_manual_award}
 236                       WHERE recipientid = :instanceid";
 237  
 238              $userlist->add_from_sql('issuerid', $sql, $params);
 239              $userlist->add_from_sql('recipientid', $sql, $params);
 240  
 241              $sql = "SELECT userid
 242                        FROM {badge_issued}
 243                       WHERE userid = :instanceid";
 244  
 245              $userlist->add_from_sql('userid', $sql, $params);
 246  
 247              $sql = "SELECT userid
 248                        FROM {badge_criteria_met}
 249                       WHERE userid = :instanceid";
 250  
 251              $userlist->add_from_sql('userid', $sql, $params);
 252  
 253              $sql = "SELECT userid
 254                        FROM {badge_backpack}
 255                       WHERE userid = :instanceid";
 256  
 257              $userlist->add_from_sql('userid', $sql, $params);
 258          }
 259      }
 260  
 261      /**
 262       * Export all user data for the specified user, in the specified contexts.
 263       *
 264       * @param approved_contextlist $contextlist The approved contexts to export information for.
 265       */
 266      public static function export_user_data(approved_contextlist $contextlist) {
 267          global $DB;
 268  
 269          $userid = $contextlist->get_user()->id;
 270          $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 271              $level = $context->contextlevel;
 272              if ($level == CONTEXT_USER || $level == CONTEXT_COURSE) {
 273                  $carry[$level][] = $context->instanceid;
 274              } else if ($level == CONTEXT_SYSTEM) {
 275                  $carry[$level] = SYSCONTEXTID;
 276              }
 277              return $carry;
 278          }, [
 279              CONTEXT_COURSE => [],
 280              CONTEXT_USER => [],
 281              CONTEXT_SYSTEM => null,
 282          ]);
 283  
 284          $path = [get_string('badges', 'core_badges')];
 285          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
 286  
 287          // Export the badges we've created or modified.
 288          if (!empty($contexts[CONTEXT_SYSTEM]) || !empty($contexts[CONTEXT_COURSE])) {
 289              $sqls = [];
 290              $params = [];
 291  
 292              if (!empty($contexts[CONTEXT_SYSTEM])) {
 293                  $sqls[] = "b.type = :typesite";
 294                  $params['typesite'] = BADGE_TYPE_SITE;
 295              }
 296  
 297              if (!empty($contexts[CONTEXT_COURSE])) {
 298                  list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_COURSE], SQL_PARAMS_NAMED);
 299                  $sqls[] = "(b.type = :typecourse AND b.courseid $insql)";
 300                  $params = array_merge($params, ['typecourse' => BADGE_TYPE_COURSE], $inparams);
 301              }
 302  
 303              $sqlwhere = '(' . implode(' OR ', $sqls) . ')';
 304              $sql = "
 305                  SELECT b.*, COALESCE(b.courseid, 0) AS normalisedcourseid
 306                    FROM {badge} b
 307                   WHERE (b.usermodified = :userid1 OR b.usercreated = :userid2)
 308                     AND $sqlwhere
 309                ORDER BY b.courseid, b.id";
 310              $params = array_merge($params, ['userid1' => $userid, 'userid2' => $userid]);
 311              $recordset = $DB->get_recordset_sql($sql, $params);
 312              static::recordset_loop_and_export($recordset, 'normalisedcourseid', [], function($carry, $record) use ($userid) {
 313                  $carry[] = [
 314                      'name' => $record->name,
 315                      'created_on' => transform::datetime($record->timecreated),
 316                      'created_by_you' => transform::yesno($record->usercreated == $userid),
 317                      'modified_on' => transform::datetime($record->timemodified),
 318                      'modified_by_you' => transform::yesno($record->usermodified == $userid),
 319                  ];
 320                  return $carry;
 321              }, function($courseid, $data) use ($path) {
 322                  $context = $courseid ? context_course::instance($courseid) : context_system::instance();
 323                  writer::with_context($context)->export_data($path, (object) ['badges' => $data]);
 324              });
 325          }
 326  
 327          // Export the badges we've manually awarded.
 328          if (!empty($contexts[CONTEXT_USER])) {
 329              list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_USER], SQL_PARAMS_NAMED);
 330              $sql = "
 331                  SELECT bma.id, bma.recipientid, bma.datemet, b.name, b.courseid,
 332                         r.id AS roleid,
 333                         r.name AS rolename,
 334                         r.shortname AS roleshortname,
 335                         r.archetype AS rolearchetype,
 336                         $ctxfields
 337                    FROM {badge_manual_award} bma
 338                    JOIN {badge} b
 339                      ON b.id = bma.badgeid
 340                    JOIN {role} r
 341                      ON r.id = bma.issuerrole
 342                    JOIN {context} ctx
 343                      ON (COALESCE(b.courseid, 0) > 0 AND ctx.instanceid = b.courseid AND ctx.contextlevel = :courselevel)
 344                      OR (COALESCE(b.courseid, 0) = 0 AND ctx.id = :syscontextid)
 345                   WHERE bma.recipientid $insql
 346                     AND bma.issuerid = :userid
 347                ORDER BY bma.recipientid, bma.id";
 348              $params = array_merge($inparams, [
 349                  'courselevel' => CONTEXT_COURSE,
 350                  'syscontextid' => SYSCONTEXTID,
 351                  'userid' => $userid
 352              ]);
 353              $recordset = $DB->get_recordset_sql($sql, $params);
 354              static::recordset_loop_and_export($recordset, 'recipientid', [], function($carry, $record) use ($userid) {
 355  
 356                  // The only reason we fetch the context and role is to format the name of the role, which could be
 357                  // different to the standard name if the badge was created in a course.
 358                  context_helper::preload_from_record($record);
 359                  $context = $record->courseid ? context_course::instance($record->courseid) : context_system::instance();
 360                  $role = (object) [
 361                      'id' => $record->roleid,
 362                      'name' => $record->rolename,
 363                      'shortname' => $record->roleshortname,
 364                      'archetype' => $record->rolearchetype,
 365                      // Mock those two fields as they do not matter.
 366                      'sortorder' => 0,
 367                      'description' => ''
 368                  ];
 369  
 370                  $carry[] = [
 371                      'name' => $record->name,
 372                      'issued_by_you' => transform::yesno(true),
 373                      'issued_on' => transform::datetime($record->datemet),
 374                      'issuer_role' => role_get_name($role, $context),
 375                  ];
 376                  return $carry;
 377              }, function($userid, $data) use ($path) {
 378                  $context = context_user::instance($userid);
 379                  writer::with_context($context)->export_related_data($path, 'manual_awards', (object) ['badges' => $data]);
 380              });
 381          }
 382  
 383          // Export our data.
 384          if (in_array($userid, $contexts[CONTEXT_USER])) {
 385  
 386              // Export the badges.
 387              $uniqueid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)',
 388                  'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)', 'COALESCE(brb.id, 0)', 'COALESCE(ba.id, 0)']);
 389              $sql = "
 390                  SELECT $uniqueid AS uniqueid, b.id,
 391                         bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash,
 392                         bma.id AS bmaid, bma.datemet, bma.issuerid,
 393                         bcm.id AS bcmid,
 394                         c.fullname AS coursename,
 395                         be.id AS beid,
 396                         be.issuername AS beissuername,
 397                         be.issuerurl AS beissuerurl,
 398                         be.issueremail AS beissueremail,
 399                         be.claimid AS beclaimid,
 400                         be.claimcomment AS beclaimcomment,
 401                         be.dateissued AS bedateissued,
 402                         brb.id as rbid,
 403                         brb.badgeid as rbbadgeid,
 404                         brb.relatedbadgeid as rbrelatedbadgeid,
 405                         ba.id as baid,
 406                         ba.targetname as batargetname,
 407                         ba.targeturl as batargeturl,
 408                         ba.targetdescription as batargetdescription,
 409                         ba.targetframework as batargetframework,
 410                         ba.targetcode as batargetcode,
 411                         $ctxfields
 412                    FROM {badge} b
 413               LEFT JOIN {badge_issued} bi
 414                      ON bi.badgeid = b.id
 415                     AND bi.userid = :userid1
 416              LEFT JOIN {badge_related} brb
 417                      ON ( b.id = brb.badgeid OR b.id = brb.relatedbadgeid )
 418               LEFT JOIN {badge_alignment} ba
 419                      ON ( b.id = ba.badgeid )
 420               LEFT JOIN {badge_endorsement} be
 421                      ON be.badgeid = b.id
 422               LEFT JOIN {badge_manual_award} bma
 423                      ON bma.badgeid = b.id
 424                     AND bma.recipientid = :userid2
 425               LEFT JOIN {badge_criteria} bc
 426                      ON bc.badgeid = b.id
 427               LEFT JOIN {badge_criteria_met} bcm
 428                      ON bcm.critid = bc.id
 429                     AND bcm.userid = :userid3
 430               LEFT JOIN {course} c
 431                      ON c.id = b.courseid
 432                     AND b.type = :typecourse
 433               LEFT JOIN {context} ctx
 434                      ON ctx.instanceid = c.id
 435                     AND ctx.contextlevel = :courselevel
 436                   WHERE bi.id IS NOT NULL
 437                      OR bma.id IS NOT NULL
 438                      OR bcm.id IS NOT NULL
 439                ORDER BY b.id";
 440              $params = [
 441                  'userid1' => $userid,
 442                  'userid2' => $userid,
 443                  'userid3' => $userid,
 444                  'courselevel' => CONTEXT_COURSE,
 445                  'typecourse' => BADGE_TYPE_COURSE,
 446              ];
 447              $recordset = $DB->get_recordset_sql($sql, $params);
 448              static::recordset_loop_and_export($recordset, 'id', null, function($carry, $record) use ($userid) {
 449                  $badge = new badge($record->id);
 450  
 451                  // Export details of the badge.
 452                  if ($carry === null) {
 453                      $carry = [
 454                          'name' => $badge->name,
 455                          'version' => $badge->version,
 456                          'language' => $badge->language,
 457                          'imageauthorname' => $badge->imageauthorname,
 458                          'imageauthoremail' => $badge->imageauthoremail,
 459                          'imageauthorurl' => $badge->imageauthorurl,
 460                          'imagecaption' => $badge->imagecaption,
 461                          'issued' => null,
 462                          'manual_award' => null,
 463                          'criteria_met' => [],
 464                          'endorsement' => null,
 465                      ];
 466  
 467                      if ($badge->type == BADGE_TYPE_COURSE) {
 468                          context_helper::preload_from_record($record);
 469                          $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]);
 470                      }
 471  
 472                      if (!empty($record->beid)) {
 473                          $carry['endorsement'] = [
 474                              'issuername' => $record->beissuername,
 475                              'issuerurl' => $record->beissuerurl,
 476                              'issueremail' => $record->beissueremail,
 477                              'claimid' => $record->beclaimid,
 478                              'claimcomment' => $record->beclaimcomment,
 479                              'dateissued' => $record->bedateissued ? transform::datetime($record->bedateissued) : null
 480                          ];
 481                      }
 482  
 483                      if (!empty($record->biid)) {
 484                          $carry['issued'] = [
 485                              'issued_on' => transform::datetime($record->dateissued),
 486                              'expires_on' => $record->dateexpire ? transform::datetime($record->dateexpire) : null,
 487                              'unique_hash' => $record->uniquehash,
 488                          ];
 489                      }
 490  
 491                      if (!empty($record->bmaid)) {
 492                          $carry['manual_award'] = [
 493                              'awarded_on' => transform::datetime($record->datemet),
 494                              'issuer' => transform::user($record->issuerid)
 495                          ];
 496                      }
 497                  }
 498                  if (!empty($record->rbid)) {
 499                      if (empty($carry['related_badge'])) {
 500                          $carry['related_badge'] = [];
 501                      }
 502                      $rbid = $record->rbbadgeid;
 503                      if ($rbid == $record->id) {
 504                          $rbid = $record->rbrelatedbadgeid;
 505                      }
 506                      $exists = false;
 507                      foreach ($carry['related_badge'] as $related) {
 508                          if ($related['badgeid'] == $rbid) {
 509                              $exists = true;
 510                              break;
 511                          }
 512                      }
 513                      if (!$exists) {
 514                          $relatedbadge = new badge($rbid);
 515                          $carry['related_badge'][] = [
 516                              'badgeid' => $rbid,
 517                              'badgename' => $relatedbadge->name
 518                          ];
 519                      }
 520                  }
 521  
 522                  if (!empty($record->baid)) {
 523                      if (empty($carry['alignment'])) {
 524                          $carry['alignment'] = [];
 525                      }
 526                      $exists = false;
 527                      $newalignment = [
 528                          'targetname' => $record->batargetname,
 529                          'targeturl' => $record->batargeturl,
 530                          'targetdescription' => $record->batargetdescription,
 531                          'targetframework' => $record->batargetframework,
 532                          'targetcode' => $record->batargetcode,
 533                      ];
 534                      foreach ($carry['alignment'] as $alignment) {
 535                          if ($alignment == $newalignment) {
 536                              $exists = true;
 537                              break;
 538                          }
 539                      }
 540                      if (!$exists) {
 541                          $carry['alignment'][] = $newalignment;
 542                      }
 543                  }
 544  
 545                  // Export the details of the criteria met.
 546                  // We only do that once, when we find that a least one criteria was met.
 547                  // This is heavily based on the logic present in core_badges_renderer::render_issued_badge.
 548                  if (!empty($record->bcmid) && empty($carry['criteria_met'])) {
 549  
 550                      $agg = $badge->get_aggregation_methods();
 551                      $evidenceids = array_map(function($record) {
 552                          return $record->critid;
 553                      }, $badge->get_criteria_completions($userid));
 554  
 555                      $criteria = $badge->criteria;
 556                      unset($criteria[BADGE_CRITERIA_TYPE_OVERALL]);
 557  
 558                      $items = [];
 559                      foreach ($criteria as $type => $c) {
 560                          if (in_array($c->id, $evidenceids)) {
 561                              $details = $c->get_details(true);
 562                              if (count($c->params) == 1) {
 563                                  $items[] = get_string('criteria_descr_single_' . $type , 'core_badges') . ' ' . $details;
 564                              } else {
 565                                  $items[] = get_string('criteria_descr_' . $type , 'core_badges',
 566                                      core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . ' ' . $details;
 567                              }
 568                          }
 569                      }
 570                      $carry['criteria_met'] = $items;
 571                  }
 572                  return $carry;
 573              }, function($badgeid, $data) use ($path, $userid) {
 574                  $path = array_merge($path, ["{$data['name']} ({$badgeid})"]);
 575                  $writer = writer::with_context(context_user::instance($userid));
 576                  $writer->export_data($path, (object) $data);
 577                  $writer->export_area_files($path, 'badges', 'userbadge', $badgeid);
 578              });
 579  
 580              // Export the backpacks.
 581              $data = [];
 582              $recordset = $DB->get_recordset_select('badge_backpack', 'userid = :userid', ['userid' => $userid]);
 583              foreach ($recordset as $record) {
 584                  $data[] = [
 585                      'email' => $record->email,
 586                      'externalbackpackid' => $record->externalbackpackid,
 587                      'uid' => $record->backpackuid
 588                  ];
 589              }
 590              $recordset->close();
 591              if (!empty($data)) {
 592                  writer::with_context(context_user::instance($userid))->export_related_data($path, 'backpacks',
 593                      (object) ['backpacks' => $data]);
 594              }
 595          }
 596      }
 597  
 598      /**
 599       * Delete all data for all users in the specified context.
 600       *
 601       * @param context $context The specific context to delete data for.
 602       */
 603      public static function delete_data_for_all_users_in_context(context $context) {
 604          // We cannot delete the course or system data as it is needed by the system.
 605          if ($context->contextlevel != CONTEXT_USER) {
 606              return;
 607          }
 608  
 609          // Delete all the user data.
 610          static::delete_user_data($context->instanceid);
 611      }
 612  
 613      /**
 614       * Delete multiple users within a single context.
 615       *
 616       * @param approved_userlist $userlist The approved context and user information to delete information for.
 617       */
 618      public static function delete_data_for_users(approved_userlist $userlist) {
 619          $context = $userlist->get_context();
 620  
 621          if (!in_array($context->instanceid, $userlist->get_userids())) {
 622              return;
 623          }
 624  
 625          if ($context->contextlevel == CONTEXT_USER) {
 626              // We can only delete our own data in the user context, nothing in course or system.
 627              static::delete_user_data($context->instanceid);
 628          }
 629      }
 630  
 631      /**
 632       * Delete all user data for the specified user, in the specified contexts.
 633       *
 634       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 635       */
 636      public static function delete_data_for_user(approved_contextlist $contextlist) {
 637          $userid = $contextlist->get_user()->id;
 638          foreach ($contextlist->get_contexts() as $context) {
 639              if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
 640                  // We can only delete our own data in the user context, nothing in course or system.
 641                  static::delete_user_data($userid);
 642                  break;
 643              }
 644          }
 645      }
 646  
 647      /**
 648       * Delete all the data for a user.
 649       *
 650       * @param int $userid The user ID.
 651       * @return void
 652       */
 653      protected static function delete_user_data($userid) {
 654          global $DB;
 655  
 656          // Delete the stuff.
 657          $DB->delete_records('badge_manual_award', ['recipientid' => $userid]);
 658          $DB->delete_records('badge_criteria_met', ['userid' => $userid]);
 659          $DB->delete_records('badge_issued', ['userid' => $userid]);
 660  
 661          // Delete the backpacks and related stuff.
 662          $backpackids = $DB->get_fieldset_select('badge_backpack', 'id', 'userid = :userid', ['userid' => $userid]);
 663          if (!empty($backpackids)) {
 664              list($insql, $inparams) = $DB->get_in_or_equal($backpackids, SQL_PARAMS_NAMED);
 665              $DB->delete_records_select('badge_external', "backpackid $insql", $inparams);
 666              $DB->delete_records_select('badge_backpack', "id $insql", $inparams);
 667          }
 668      }
 669  
 670      /**
 671       * Loop and export from a recordset.
 672       *
 673       * @param \moodle_recordset $recordset The recordset.
 674       * @param string $splitkey The record key to determine when to export.
 675       * @param mixed $initial The initial data to reduce from.
 676       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
 677       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
 678       * @return void
 679       */
 680      protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
 681              callable $reducer, callable $export) {
 682  
 683          $data = $initial;
 684          $lastid = null;
 685  
 686          foreach ($recordset as $record) {
 687              if ($lastid !== null && $record->{$splitkey} != $lastid) {
 688                  $export($lastid, $data);
 689                  $data = $initial;
 690              }
 691              $data = $reducer($data, $record);
 692              $lastid = $record->{$splitkey};
 693          }
 694          $recordset->close();
 695  
 696          if ($lastid !== null) {
 697              $export($lastid, $data);
 698          }
 699      }
 700  }