Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Data generators for acceptance testing.
  19   *
  20   * @package   core
  21   * @category  test
  22   * @copyright 2012 David MonllaĆ³
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  
  31  /**
  32   * Behat data generator class for core entities.
  33   *
  34   * @package   core
  35   * @category  test
  36   * @copyright 2012 David MonllaĆ³
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class behat_core_generator extends behat_generator_base {
  40  
  41      protected function get_creatable_entities(): array {
  42          $entities = [
  43              'users' => [
  44                  'singular' => 'user',
  45                  'datagenerator' => 'user',
  46                  'required' => ['username'],
  47              ],
  48              'categories' => [
  49                  'singular' => 'category',
  50                  'datagenerator' => 'category',
  51                  'required' => ['idnumber'],
  52                  'switchids' => ['category' => 'parent'],
  53              ],
  54              'courses' => [
  55                  'singular' => 'course',
  56                  'datagenerator' => 'course',
  57                  'required' => ['shortname'],
  58                  'switchids' => ['category' => 'category'],
  59              ],
  60              'groups' => [
  61                  'singular' => 'group',
  62                  'datagenerator' => 'group',
  63                  'required' => ['idnumber', 'course'],
  64                  'switchids' => ['course' => 'courseid'],
  65              ],
  66              'groupings' => [
  67                  'singular' => 'grouping',
  68                  'datagenerator' => 'grouping',
  69                  'required' => ['idnumber', 'course'],
  70                  'switchids' => ['course' => 'courseid'],
  71              ],
  72              'course enrolments' => [
  73                  'singular' => 'course enrolment',
  74                  'datagenerator' => 'enrol_user',
  75                  'required' => ['user', 'course', 'role'],
  76                  'switchids' => ['user' => 'userid', 'course' => 'courseid', 'role' => 'roleid'],
  77              ],
  78              'custom field categories' => [
  79                  'singular' => 'custom field category',
  80                  'datagenerator' => 'custom_field_category',
  81                  'required' => ['name', 'component', 'area', 'itemid'],
  82                  'switchids' => [],
  83              ],
  84              'custom fields' => [
  85                  'singular' => 'custom field',
  86                  'datagenerator' => 'custom_field',
  87                  'required' => ['name', 'category', 'type', 'shortname'],
  88                  'switchids' => [],
  89              ],
  90              'permission overrides' => [
  91                  'singular' => 'permission override',
  92                  'datagenerator' => 'permission_override',
  93                  'required' => ['capability', 'permission', 'role', 'contextlevel', 'reference'],
  94                  'switchids' => ['role' => 'roleid'],
  95              ],
  96              'system role assigns' => [
  97                  'singular' => 'system role assignment',
  98                  'datagenerator' => 'system_role_assign',
  99                  'required' => ['user', 'role'],
 100                  'switchids' => ['user' => 'userid', 'role' => 'roleid'],
 101              ],
 102              'role assigns' => [
 103                  'singular' => 'role assignment',
 104                  'datagenerator' => 'role_assign',
 105                  'required' => ['user', 'role', 'contextlevel', 'reference'],
 106                  'switchids' => ['user' => 'userid', 'role' => 'roleid'],
 107              ],
 108              'activities' => [
 109                  'singular' => 'activity',
 110                  'datagenerator' => 'activity',
 111                  'required' => ['activity', 'course'],
 112                  'switchids' => ['course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid'],
 113              ],
 114              'blocks' => [
 115                  'singular' => 'block',
 116                  'datagenerator' => 'block_instance',
 117                  'required' => ['blockname', 'contextlevel', 'reference'],
 118              ],
 119              'group members' => [
 120                  'singular' => 'group member',
 121                  'datagenerator' => 'group_member',
 122                  'required' => ['user', 'group'],
 123                  'switchids' => ['user' => 'userid', 'group' => 'groupid'],
 124              ],
 125              'grouping groups' => [
 126                  'singular' => 'grouping group',
 127                  'datagenerator' => 'grouping_group',
 128                  'required' => ['grouping', 'group'],
 129                  'switchids' => ['grouping' => 'groupingid', 'group' => 'groupid'],
 130              ],
 131              'cohorts' => [
 132                  'singular' => 'cohort',
 133                  'datagenerator' => 'cohort',
 134                  'required' => ['idnumber'],
 135              ],
 136              'cohort members' => [
 137                  'singular' => 'cohort member',
 138                  'datagenerator' => 'cohort_member',
 139                  'required' => ['user', 'cohort'],
 140                  'switchids' => ['user' => 'userid', 'cohort' => 'cohortid'],
 141              ],
 142              'roles' => [
 143                  'singular' => 'role',
 144                  'datagenerator' => 'role',
 145                  'required' => ['shortname'],
 146              ],
 147              'role capabilities' => [
 148                  'singular' => 'role capability',
 149                  'datagenerator' => 'role_capability',
 150                  'required' => ['role'],
 151                  'switchids' => ['role' => 'roleid'],
 152              ],
 153              'grade categories' => [
 154                  'singular' => 'grade category',
 155                  'datagenerator' => 'grade_category',
 156                  'required' => ['fullname', 'course'],
 157                  'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
 158              ],
 159              'grade grades' => [
 160                  'singular' => 'grade grade',
 161                  'datagenerator' => 'grade_grade',
 162                  'required' => ['gradeitem'],
 163                  'switchids' => ['user' => 'userid', 'gradeitem' => 'itemid'],
 164              ],
 165              'grade items' => [
 166                  'singular' => 'grade item',
 167                  'datagenerator' => 'grade_item',
 168                  'required' => ['course'],
 169                  'switchids' => [
 170                      'scale' => 'scaleid',
 171                      'outcome' => 'outcomeid',
 172                      'course' => 'courseid',
 173                      'gradecategory' => 'categoryid',
 174                  ],
 175              ],
 176              'grade outcomes' => [
 177                  'singular' => 'grade outcome',
 178                  'datagenerator' => 'grade_outcome',
 179                  'required' => ['shortname', 'scale'],
 180                  'switchids' => ['course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid'],
 181              ],
 182              'scales' => [
 183                  'singular' => 'scale',
 184                  'datagenerator' => 'scale',
 185                  'required' => ['name', 'scale'],
 186                  'switchids' => ['course' => 'courseid'],
 187              ],
 188              'question categories' => [
 189                  'singular' => 'question category',
 190                  'datagenerator' => 'question_category',
 191                  'required' => ['name', 'contextlevel', 'reference'],
 192                  'switchids' => ['questioncategory' => 'parent'],
 193              ],
 194              'questions' => [
 195                  'singular' => 'question',
 196                  'datagenerator' => 'question',
 197                  'required' => ['qtype', 'questioncategory', 'name'],
 198                  'switchids' => ['questioncategory' => 'category', 'user' => 'createdby'],
 199              ],
 200              'tags' => [
 201                  'singular' => 'tag',
 202                  'datagenerator' => 'tag',
 203                  'required' => ['name'],
 204              ],
 205              'events' => [
 206                  'singular' => 'event',
 207                  'datagenerator' => 'event',
 208                  'required' => ['name', 'eventtype'],
 209                  'switchids' => [
 210                      'user' => 'userid',
 211                      'course' => 'courseid',
 212                      'category' => 'categoryid',
 213                  ],
 214              ],
 215              'message contacts' => [
 216                  'singular' => 'message contact',
 217                  'datagenerator' => 'message_contacts',
 218                  'required' => ['user', 'contact'],
 219                  'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
 220              ],
 221              'private messages' => [
 222                  'singular' => 'private message',
 223                  'datagenerator' => 'private_messages',
 224                  'required' => ['user', 'contact', 'message'],
 225                  'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
 226              ],
 227              'favourite conversations' => [
 228                  'singular' => 'favourite conversation',
 229                  'datagenerator' => 'favourite_conversations',
 230                  'required' => ['user', 'contact'],
 231                  'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
 232              ],
 233              'group messages' => [
 234                  'singular' => 'group message',
 235                  'datagenerator' => 'group_messages',
 236                  'required' => ['user', 'group', 'message'],
 237                  'switchids' => ['user' => 'userid', 'group' => 'groupid'],
 238              ],
 239              'muted group conversations' => [
 240                  'singular' => 'muted group conversation',
 241                  'datagenerator' => 'mute_group_conversations',
 242                  'required' => ['user', 'group', 'course'],
 243                  'switchids' => ['user' => 'userid', 'group' => 'groupid', 'course' => 'courseid'],
 244              ],
 245              'muted private conversations' => [
 246                  'singular' => 'muted private conversation',
 247                  'datagenerator' => 'mute_private_conversations',
 248                  'required' => ['user', 'contact'],
 249                  'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
 250              ],
 251              'language customisations' => [
 252                  'singular' => 'language customisation',
 253                  'datagenerator' => 'customlang',
 254                  'required' => ['component', 'stringid', 'value'],
 255              ],
 256              'analytics models' => [
 257                  'singular' => 'analytics model',
 258                  'datagenerator' => 'analytics_model',
 259                  'required' => ['target', 'indicators', 'timesplitting', 'enabled'],
 260              ],
 261              'user preferences' => [
 262                  'singular' => 'user preference',
 263                  'datagenerator' => 'user_preferences',
 264                  'required' => array('user', 'preference', 'value'),
 265                  'switchids' => array('user' => 'userid'),
 266              ],
 267              'contentbank contents' => [
 268                  'singular' => 'contentbank content',
 269                  'datagenerator' => 'contentbank_content',
 270                  'required' => array('contextlevel', 'reference', 'contenttype', 'user', 'contentname'),
 271                  'switchids' => array('user' => 'userid')
 272              ],
 273              'badge external backpacks' => [
 274                  'singular' => 'badge external backpack',
 275                  'datagenerator' => 'badge_external_backpack',
 276                  'required' => ['backpackapiurl', 'backpackweburl', 'apiversion']
 277              ],
 278              'setup backpacks connected' => [
 279                  'singular' => 'setup backpack connected',
 280                  'datagenerator' => 'setup_backpack_connected',
 281                  'required' => ['user', 'externalbackpack'],
 282                  'switchids' => ['user' => 'userid', 'externalbackpack' => 'externalbackpackid']
 283              ],
 284              'last access times' => [
 285                  'singular' => 'last access time',
 286                  'datagenerator' => 'last_access_times',
 287                  'required' => ['user', 'course', 'lastaccess'],
 288                  'switchids' => ['user' => 'userid', 'course' => 'courseid'],
 289              ],
 290              'notifications' => [
 291                  'singular' => 'notification',
 292                  'datagenerator' => 'notification',
 293                  'required' => ['subject', 'userfrom', 'userto'],
 294                  'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'],
 295              ],
 296          ];
 297  
 298          return $entities;
 299      }
 300  
 301      /**
 302       * Get the grade item id using a name.
 303       *
 304       * @param string $name
 305       * @return int The grade item id
 306       */
 307      protected function get_gradeitem_id(string $name): int {
 308          global $DB;
 309  
 310          if (!$id = $DB->get_field('grade_items', 'id', ['itemname' => $name])) {
 311              throw new Exception('The specified grade item with name "' . $name . '" could not be found.');
 312          }
 313  
 314          return $id;
 315      }
 316  
 317      /**
 318       * Remove any empty custom fields, to avoid errors when creating the course.
 319       *
 320       * @param array $data
 321       * @return array
 322       */
 323      protected function preprocess_course($data) {
 324          foreach ($data as $fieldname => $value) {
 325              if ($value === '' && strpos($fieldname, 'customfield_') === 0) {
 326                  unset($data[$fieldname]);
 327              }
 328          }
 329          return $data;
 330      }
 331  
 332      /**
 333       * If password is not set it uses the username.
 334       *
 335       * @param array $data
 336       * @return array
 337       */
 338      protected function preprocess_user($data) {
 339          if (!isset($data['password'])) {
 340              $data['password'] = $data['username'];
 341          }
 342          return $data;
 343      }
 344  
 345      /**
 346       * If contextlevel and reference are specified for cohort, transform them to the contextid.
 347       *
 348       * @param array $data
 349       * @return array
 350       */
 351      protected function preprocess_cohort($data) {
 352          if (isset($data['contextlevel'])) {
 353              if (!isset($data['reference'])) {
 354                  throw new Exception('If field contextlevel is specified, field reference must also be present');
 355              }
 356              $context = $this->get_context($data['contextlevel'], $data['reference']);
 357              unset($data['contextlevel']);
 358              unset($data['reference']);
 359              $data['contextid'] = $context->id;
 360          }
 361          return $data;
 362      }
 363  
 364      /**
 365       * Preprocesses the creation of a grade item. Converts gradetype text to a number.
 366       *
 367       * @param array $data
 368       * @return array
 369       */
 370      protected function preprocess_grade_item($data) {
 371          global $CFG;
 372          require_once("$CFG->libdir/grade/constants.php");
 373  
 374          if (isset($data['gradetype'])) {
 375              $data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype']));
 376          }
 377  
 378          if (!empty($data['category']) && !empty($data['courseid'])) {
 379              $cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid']));
 380              if (!$cat) {
 381                  throw new Exception('Could not resolve category with name "' . $data['category'] . '"');
 382              }
 383              unset($data['category']);
 384              $data['categoryid'] = $cat->id;
 385          }
 386  
 387          return $data;
 388      }
 389  
 390      /**
 391       * Adapter to modules generator.
 392       *
 393       * @throws Exception Custom exception for test writers
 394       * @param array $data
 395       * @return void
 396       */
 397      protected function process_activity($data) {
 398          global $DB, $CFG;
 399  
 400          // The the_following_exists() method checks that the field exists.
 401          $activityname = $data['activity'];
 402          unset($data['activity']);
 403  
 404          // Convert scale name into scale id (negative number indicates using scale).
 405          if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) {
 406              $data['grade'] = - $this->get_scale_id($data['grade']);
 407              require_once("$CFG->libdir/grade/constants.php");
 408  
 409              if (!isset($data['gradetype'])) {
 410                  $data['gradetype'] = GRADE_TYPE_SCALE;
 411              }
 412          }
 413  
 414          if (!array_key_exists('idnumber', $data)) {
 415              $data['idnumber'] = $data['name'];
 416              if (strlen($data['name']) > 100) {
 417                  throw new Exception(
 418                      "Activity '{$activityname}' cannot be used as the default idnumber. " .
 419                      "The idnumber has a max length of 100 chars. " .
 420                      "Please manually specify an idnumber."
 421                  );
 422              }
 423          }
 424  
 425          // We split $data in the activity $record and the course module $options.
 426          $cmoptions = array();
 427          $cmcolumns = $DB->get_columns('course_modules');
 428          foreach ($cmcolumns as $key => $value) {
 429              if (isset($data[$key])) {
 430                  $cmoptions[$key] = $data[$key];
 431              }
 432          }
 433  
 434          // Custom exception.
 435          try {
 436              $this->datagenerator->create_module($activityname, $data, $cmoptions);
 437          } catch (coding_exception $e) {
 438              throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' .
 439                      ' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead');
 440          }
 441      }
 442  
 443      /**
 444       * Add a block to a page.
 445       *
 446       * @param array $data should mostly match the fields of the block_instances table.
 447       *     The block type is specified by blockname.
 448       *     The parentcontextid is set from contextlevel and reference.
 449       *     Missing values are filled in by testing_block_generator::prepare_record.
 450       *     $data is passed to create_block as both $record and $options. Normally
 451       *     the keys are different, so this is a way to let people set values in either place.
 452       */
 453      protected function process_block_instance($data) {
 454  
 455          if (empty($data['blockname'])) {
 456              throw new Exception('\'blocks\' requires the field \'block\' type to be specified');
 457          }
 458  
 459          if (empty($data['contextlevel'])) {
 460              throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified');
 461          }
 462  
 463          if (!isset($data['reference'])) {
 464              throw new Exception('\'blocks\' requires the field \'reference\' to be specified');
 465          }
 466  
 467          $context = $this->get_context($data['contextlevel'], $data['reference']);
 468          $data['parentcontextid'] = $context->id;
 469  
 470          // Pass $data as both $record and $options. I think that is unlikely to
 471          // cause problems since the relevant key names are different.
 472          // $options is not used in most blocks I have seen, but where it is, it is necessary.
 473          $this->datagenerator->create_block($data['blockname'], $data, $data);
 474      }
 475  
 476      /**
 477       * Creates language customisation.
 478       *
 479       * @throws Exception
 480       * @throws dml_exception
 481       * @param array $data
 482       * @return void
 483       */
 484      protected function process_customlang($data) {
 485          global $CFG, $DB, $USER;
 486  
 487          require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/customlang/locallib.php');
 488          require_once($CFG->libdir . '/adminlib.php');
 489  
 490          if (empty($data['component'])) {
 491              throw new Exception('\'customlang\' requires the field \'component\' type to be specified');
 492          }
 493  
 494          if (empty($data['stringid'])) {
 495              throw new Exception('\'customlang\' requires the field \'stringid\' to be specified');
 496          }
 497  
 498          if (!isset($data['value'])) {
 499              throw new Exception('\'customlang\' requires the field \'value\' to be specified');
 500          }
 501  
 502          $now = time();
 503  
 504          tool_customlang_utils::checkout($USER->lang);
 505  
 506          $record = $DB->get_record_sql("SELECT s.*
 507                                           FROM {tool_customlang} s
 508                                           JOIN {tool_customlang_components} c ON s.componentid = c.id
 509                                          WHERE c.name = ? AND s.lang = ? AND s.stringid = ?",
 510                  array($data['component'], $USER->lang, $data['stringid']));
 511  
 512          if (empty($data['value']) && !is_null($record->local)) {
 513              $record->local = null;
 514              $record->modified = 1;
 515              $record->outdated = 0;
 516              $record->timecustomized = null;
 517              $DB->update_record('tool_customlang', $record);
 518              tool_customlang_utils::checkin($USER->lang);
 519          }
 520  
 521          if (!empty($data['value']) && $data['value'] != $record->local) {
 522              $record->local = $data['value'];
 523              $record->modified = 1;
 524              $record->outdated = 0;
 525              $record->timecustomized = $now;
 526              $DB->update_record('tool_customlang', $record);
 527              tool_customlang_utils::checkin($USER->lang);
 528          }
 529      }
 530  
 531      /**
 532       * Adapter to enrol_user() data generator.
 533       *
 534       * @throws Exception
 535       * @param array $data
 536       * @return void
 537       */
 538      protected function process_enrol_user($data) {
 539          global $SITE;
 540  
 541          if (empty($data['roleid'])) {
 542              throw new Exception('\'course enrolments\' requires the field \'role\' to be specified');
 543          }
 544  
 545          if (!isset($data['userid'])) {
 546              throw new Exception('\'course enrolments\' requires the field \'user\' to be specified');
 547          }
 548  
 549          if (!isset($data['courseid'])) {
 550              throw new Exception('\'course enrolments\' requires the field \'course\' to be specified');
 551          }
 552  
 553          if (!isset($data['enrol'])) {
 554              $data['enrol'] = 'manual';
 555          }
 556  
 557          if (!isset($data['timestart'])) {
 558              $data['timestart'] = 0;
 559          }
 560  
 561          if (!isset($data['timeend'])) {
 562              $data['timeend'] = 0;
 563          }
 564  
 565          if (!isset($data['status'])) {
 566              $data['status'] = null;
 567          } else {
 568              $status = strtolower($data['status']);
 569              switch ($status) {
 570                  case 'active':
 571                      $data['status'] = ENROL_USER_ACTIVE;
 572                      break;
 573                  case 'suspended':
 574                      $data['status'] = ENROL_USER_SUSPENDED;
 575                      break;
 576              }
 577          }
 578  
 579          // If the provided course shortname is the site shortname we consider it a system role assign.
 580          if ($data['courseid'] == $SITE->id) {
 581              // Frontpage course assign.
 582              $context = context_course::instance($data['courseid']);
 583              role_assign($data['roleid'], $data['userid'], $context->id);
 584  
 585          } else {
 586              // Course assign.
 587              $this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'],
 588                      $data['timestart'], $data['timeend'], $data['status']);
 589          }
 590  
 591      }
 592  
 593      /**
 594       * Allows/denies a capability at the specified context
 595       *
 596       * @throws Exception
 597       * @param array $data
 598       * @return void
 599       */
 600      protected function process_permission_override($data) {
 601  
 602          // Will throw an exception if it does not exist.
 603          $context = $this->get_context($data['contextlevel'], $data['reference']);
 604  
 605          switch ($data['permission']) {
 606              case get_string('allow', 'role'):
 607                  $permission = CAP_ALLOW;
 608                  break;
 609              case get_string('prevent', 'role'):
 610                  $permission = CAP_PREVENT;
 611                  break;
 612              case get_string('prohibit', 'role'):
 613                  $permission = CAP_PROHIBIT;
 614                  break;
 615              default:
 616                  throw new Exception('The \'' . $data['permission'] . '\' permission does not exist');
 617                  break;
 618          }
 619  
 620          if (is_null(get_capability_info($data['capability']))) {
 621              throw new Exception('The \'' . $data['capability'] . '\' capability does not exist');
 622          }
 623  
 624          role_change_permission($data['roleid'], $context, $data['capability'], $permission);
 625      }
 626  
 627      /**
 628       * Assigns a role to a user at system context
 629       *
 630       * Used by "system role assigns" can be deleted when
 631       * system role assign will be deprecated in favour of
 632       * "role assigns"
 633       *
 634       * @throws Exception
 635       * @param array $data
 636       * @return void
 637       */
 638      protected function process_system_role_assign($data) {
 639  
 640          if (empty($data['roleid'])) {
 641              throw new Exception('\'system role assigns\' requires the field \'role\' to be specified');
 642          }
 643  
 644          if (!isset($data['userid'])) {
 645              throw new Exception('\'system role assigns\' requires the field \'user\' to be specified');
 646          }
 647  
 648          $context = context_system::instance();
 649  
 650          $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
 651      }
 652  
 653      /**
 654       * Assigns a role to a user at the specified context
 655       *
 656       * @throws Exception
 657       * @param array $data
 658       * @return void
 659       */
 660      protected function process_role_assign($data) {
 661  
 662          if (empty($data['roleid'])) {
 663              throw new Exception('\'role assigns\' requires the field \'role\' to be specified');
 664          }
 665  
 666          if (!isset($data['userid'])) {
 667              throw new Exception('\'role assigns\' requires the field \'user\' to be specified');
 668          }
 669  
 670          if (empty($data['contextlevel'])) {
 671              throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified');
 672          }
 673  
 674          if (!isset($data['reference'])) {
 675              throw new Exception('\'role assigns\' requires the field \'reference\' to be specified');
 676          }
 677  
 678          // Getting the context id.
 679          $context = $this->get_context($data['contextlevel'], $data['reference']);
 680  
 681          $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
 682      }
 683  
 684      /**
 685       * Creates a role.
 686       *
 687       * @param array $data
 688       * @return void
 689       */
 690      protected function process_role($data) {
 691  
 692          // We require the user to fill the role shortname.
 693          if (empty($data['shortname'])) {
 694              throw new Exception('\'role\' requires the field \'shortname\' to be specified');
 695          }
 696  
 697          $this->datagenerator->create_role($data);
 698      }
 699  
 700      /**
 701       * Assign capabilities to a role.
 702       *
 703       * @param array $data
 704       */
 705      protected function process_role_capability($data): void {
 706          // We require the user to fill the role shortname.
 707          if (empty($data['roleid'])) {
 708              throw new Exception('\'role capability\' requires the field \'roleid\' to be specified');
 709          }
 710  
 711          $roleid = $data['roleid'];
 712          unset($data['roleid']);
 713  
 714          $this->datagenerator->create_role_capability($roleid, $data, \context_system::instance());
 715      }
 716  
 717      /**
 718       * Adds members to cohorts
 719       *
 720       * @param array $data
 721       * @return void
 722       */
 723      protected function process_cohort_member($data) {
 724          cohort_add_member($data['cohortid'], $data['userid']);
 725      }
 726  
 727      /**
 728       * Create a question category.
 729       *
 730       * @param array $data the row of data from the behat script.
 731       */
 732      protected function process_question_category($data) {
 733          global $DB;
 734  
 735          $context = $this->get_context($data['contextlevel'], $data['reference']);
 736  
 737          // The way this class works, we have already looked up the given parent category
 738          // name and found a matching category. However, it is possible, particularly
 739          // for the 'top' category, for there to be several categories with the
 740          // same name. So far one will have been picked at random, but we need
 741          // the one from the right context. So, if we have the wrong category, try again.
 742          // (Just fixing it here, rather than getting it right first time, is a bit
 743          // of a bodge, but in general this class assumes that names are unique,
 744          // and normally they are, so this was the easiest fix.)
 745          if (!empty($data['parent'])) {
 746              $foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST);
 747              if ($foundparent->contextid != $context->id) {
 748                  $rightparentid = $DB->get_field('question_categories', 'id',
 749                          ['contextid' => $context->id, 'name' => $foundparent->name]);
 750                  if (!$rightparentid) {
 751                      throw new Exception('The specified question category with name "' . $foundparent->name .
 752                              '" does not exist in context "' . $context->get_context_name() . '"."');
 753                  }
 754                  $data['parent'] = $rightparentid;
 755              }
 756          }
 757  
 758          $data['contextid'] = $context->id;
 759          $this->datagenerator->get_plugin_generator('core_question')->create_question_category($data);
 760      }
 761  
 762      /**
 763       * Create a question.
 764       *
 765       * Creating questions relies on the question/type/.../tests/helper.php mechanism.
 766       * We start with test_question_maker::get_question_form_data($data['qtype'], $data['template'])
 767       * and then overlay the values from any other fields of $data that are set.
 768       *
 769       * There is a special case that allows you to set qtype to 'missingtype'.
 770       * This creates an example of broken question, such as you might get if you
 771       * install a question type, create some questions of that type, and then
 772       * uninstall the question type (which is prevented through the UI but can
 773       * still happen). This special lets tests verify that these questions are
 774       * handled OK.
 775       *
 776       * @param array $data the row of data from the behat script.
 777       */
 778      protected function process_question($data) {
 779          global $DB;
 780  
 781          if (array_key_exists('questiontext', $data)) {
 782              $data['questiontext'] = array(
 783                      'text'   => $data['questiontext'],
 784                      'format' => FORMAT_HTML,
 785              );
 786          }
 787  
 788          if (array_key_exists('generalfeedback', $data)) {
 789              $data['generalfeedback'] = array(
 790                      'text'   => $data['generalfeedback'],
 791                      'format' => FORMAT_HTML,
 792              );
 793          }
 794  
 795          $which = null;
 796          if (!empty($data['template'])) {
 797              $which = $data['template'];
 798          }
 799  
 800          $missingtypespecialcase = false;
 801          if ($data['qtype'] === 'missingtype') {
 802              $data['qtype'] = 'essay'; // Actual type uses here does not matter. We just need any question.
 803              $missingtypespecialcase = true;
 804          }
 805  
 806          $questiondata = $this->datagenerator->get_plugin_generator('core_question')
 807              ->create_question($data['qtype'], $which, $data);
 808  
 809          if ($missingtypespecialcase) {
 810              $DB->set_field('question', 'qtype', 'unknownqtype', ['id' => $questiondata->id]);
 811          }
 812      }
 813  
 814      /**
 815       * Adds user to contacts
 816       *
 817       * @param array $data
 818       * @return void
 819       */
 820      protected function process_message_contacts($data) {
 821          \core_message\api::add_contact($data['userid'], $data['contactid']);
 822      }
 823  
 824      /**
 825       * Send a new message from user to contact in a private conversation
 826       *
 827       * @param array $data
 828       * @return void
 829       */
 830      protected function process_private_messages(array $data) {
 831          if (empty($data['format'])) {
 832              $data['format'] = 'FORMAT_PLAIN';
 833          }
 834  
 835          if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
 836              $conversation = \core_message\api::create_conversation(
 837                      \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
 838                      [$data['userid'], $data['contactid']]
 839              );
 840              $conversationid = $conversation->id;
 841          }
 842          \core_message\api::send_message_to_conversation(
 843                  $data['userid'],
 844                  $conversationid,
 845                  $data['message'],
 846                  constant($data['format'])
 847          );
 848      }
 849  
 850      /**
 851       * Send a new message from user to a group conversation
 852       *
 853       * @param array $data
 854       * @return void
 855       */
 856      protected function process_group_messages(array $data) {
 857          global $DB;
 858  
 859          if (empty($data['format'])) {
 860              $data['format'] = 'FORMAT_PLAIN';
 861          }
 862  
 863          $group = $DB->get_record('groups', ['id' => $data['groupid']]);
 864          $coursecontext = context_course::instance($group->courseid);
 865          if (!$conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $data['groupid'],
 866                  $coursecontext->id)) {
 867              $members = $DB->get_records_menu('groups_members', ['groupid' => $data['groupid']], '', 'userid, id');
 868              $conversation = \core_message\api::create_conversation(
 869                      \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
 870                      array_keys($members),
 871                      $group->name,
 872                      \core_message\api::MESSAGE_CONVERSATION_ENABLED,
 873                      'core_group',
 874                      'groups',
 875                      $group->id,
 876                      $coursecontext->id);
 877          }
 878          \core_message\api::send_message_to_conversation(
 879                  $data['userid'],
 880                  $conversation->id,
 881                  $data['message'],
 882                  constant($data['format'])
 883          );
 884      }
 885  
 886      /**
 887       * Mark a private conversation as favourite for user
 888       *
 889       * @param array $data
 890       * @return void
 891       */
 892      protected function process_favourite_conversations(array $data) {
 893          if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
 894              $conversation = \core_message\api::create_conversation(
 895                      \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
 896                      [$data['userid'], $data['contactid']]
 897              );
 898              $conversationid = $conversation->id;
 899          }
 900          \core_message\api::set_favourite_conversation($conversationid, $data['userid']);
 901      }
 902  
 903      /**
 904       * Mute an existing group conversation for user
 905       *
 906       * @param array $data
 907       * @return void
 908       */
 909      protected function process_mute_group_conversations(array $data) {
 910          if (groups_is_member($data['groupid'], $data['userid'])) {
 911              $context = context_course::instance($data['courseid']);
 912              $conversation = \core_message\api::get_conversation_by_area(
 913                      'core_group',
 914                      'groups',
 915                      $data['groupid'],
 916                      $context->id
 917              );
 918              if ($conversation) {
 919                  \core_message\api::mute_conversation($data['userid'], $conversation->id);
 920              }
 921          }
 922      }
 923  
 924      /**
 925       * Mute a private conversation for user
 926       *
 927       * @param array $data
 928       * @return void
 929       */
 930      protected function process_mute_private_conversations(array $data) {
 931          if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
 932              $conversation = \core_message\api::create_conversation(
 933                      \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
 934                      [$data['userid'], $data['contactid']]
 935              );
 936              $conversationid = $conversation->id;
 937          }
 938          \core_message\api::mute_conversation($data['userid'], $conversationid);
 939      }
 940  
 941      /**
 942       * Transform indicators string into array.
 943       *
 944       * @param array $data
 945       * @return array
 946       */
 947      protected function preprocess_analytics_model($data) {
 948          $data['indicators'] = explode(',', $data['indicators']);
 949          return $data;
 950      }
 951  
 952      /**
 953       * Creates an analytics model
 954       *
 955       * @param target $data
 956       * @return void
 957       */
 958      protected function process_analytics_model($data) {
 959          \core_analytics\manager::create_declared_model($data);
 960      }
 961  
 962      /**
 963       * Set a preference value for user
 964       *
 965       * @param array $data
 966       * @return void
 967       */
 968      protected function process_user_preferences(array $data) {
 969          set_user_preference($data['preference'], $data['value'], $data['userid']);
 970      }
 971  
 972      /**
 973       * Create content in the given context's content bank.
 974       *
 975       * @param array $data
 976       * @return void
 977       */
 978      protected function process_contentbank_content(array $data) {
 979          global $CFG;
 980  
 981          if (empty($data['contextlevel'])) {
 982              throw new Exception('contentbank_content requires the field contextlevel to be specified');
 983          }
 984  
 985          if (!isset($data['reference'])) {
 986              throw new Exception('contentbank_content requires the field reference to be specified');
 987          }
 988  
 989          if (empty($data['contenttype'])) {
 990              throw new Exception('contentbank_content requires the field contenttype to be specified');
 991          }
 992  
 993          $contenttypeclass = "\\".$data['contenttype']."\\contenttype";
 994          if (class_exists($contenttypeclass)) {
 995              $context = $this->get_context($data['contextlevel'], $data['reference']);
 996              $contenttype = new $contenttypeclass($context);
 997              $record = new stdClass();
 998              $record->usercreated = $data['userid'];
 999              $record->name = $data['contentname'];
1000              $content = $contenttype->create_content($record);
1001  
1002              if (!empty($data['filepath'])) {
1003                  $filename = basename($data['filepath']);
1004                  $fs = get_file_storage();
1005                  $filerecord = array(
1006                      'component' => 'contentbank',
1007                      'filearea' => 'public',
1008                      'contextid' => $context->id,
1009                      'userid' => $data['userid'],
1010                      'itemid' => $content->get_id(),
1011                      'filename' => $filename,
1012                      'filepath' => '/'
1013                  );
1014                  $fs->create_file_from_pathname($filerecord, $CFG->dirroot . $data['filepath']);
1015              }
1016          } else {
1017              throw new Exception('The specified "' . $data['contenttype'] . '" contenttype does not exist');
1018          }
1019      }
1020  
1021      /**
1022       * Create a exetrnal backpack.
1023       *
1024       * @param array $data
1025       */
1026      protected function process_badge_external_backpack(array $data) {
1027          global $DB;
1028          $DB->insert_record('badge_external_backpack', $data, true);
1029      }
1030  
1031      /**
1032       * Setup a backpack connected for user.
1033       *
1034       * @param array $data
1035       * @throws dml_exception
1036       */
1037      protected function process_setup_backpack_connected(array $data) {
1038          global $DB;
1039  
1040          if (empty($data['userid'])) {
1041              throw new Exception('\'setup backpack connected\' requires the field \'user\' to be specified');
1042          }
1043          if (empty($data['externalbackpackid'])) {
1044              throw new Exception('\'setup backpack connected\' requires the field \'externalbackpack\' to be specified');
1045          }
1046          // Dummy badge_backpack_oauth2 data.
1047          $timenow = time();
1048          $backpackoauth2 = new stdClass();
1049          $backpackoauth2->usermodified = $data['userid'];
1050          $backpackoauth2->timecreated = $timenow;
1051          $backpackoauth2->timemodified = $timenow;
1052          $backpackoauth2->userid = $data['userid'];
1053          $backpackoauth2->issuerid = 1;
1054          $backpackoauth2->externalbackpackid = $data['externalbackpackid'];
1055          $backpackoauth2->token = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
1056          $backpackoauth2->refreshtoken = '0123456789abcdefghijk';
1057          $backpackoauth2->expires = $timenow + 3600;
1058          $backpackoauth2->scope = 'https://purl.imsglobal.org/spec/ob/v2p1/scope/assertion.create';
1059          $backpackoauth2->scope .= ' https://purl.imsglobal.org/spec/ob/v2p1/scope/assertion.readonly offline_access';
1060          $DB->insert_record('badge_backpack_oauth2', $backpackoauth2);
1061  
1062          // Dummy badge_backpack data.
1063          $backpack = new stdClass();
1064          $backpack->userid = $data['userid'];
1065          $backpack->email = 'student@behat.moodle';
1066          $backpack->backpackuid = 0;
1067          $backpack->autosync = 0;
1068          $backpack->password = '';
1069          $backpack->externalbackpackid = $data['externalbackpackid'];
1070          $DB->insert_record('badge_backpack', $backpack);
1071      }
1072  
1073      /**
1074       * Creates notifications to specific user.
1075       *
1076       * @param array $data
1077       * @return void
1078       */
1079      protected function process_notification(array $data) {
1080          global $DB;
1081  
1082          $notification = new stdClass();
1083          $notification->useridfrom = $data['userfromid'];
1084          $notification->useridto = $data['usertoid'];
1085          $notification->subject = $data['subject'];
1086          $notification->fullmessage = $data['subject'] . ' description';
1087          $notification->smallmessage = $data['subject'] . ' description';
1088          $notification->fullmessagehtml = $data['subject'] . ' description';
1089  
1090          if ($data['timecreated'] !== 'null') {
1091              $notification->timecreated = $data['timecreated'];
1092          }
1093  
1094          if ($data['timeread'] !== 'null') {
1095              $notification->timeread = $data['timeread'];
1096          }
1097  
1098          if (!empty($data)) {
1099              $popupnotification = new stdClass();
1100              $popupnotification->notificationid = $DB->insert_record('notifications', $notification);
1101              $DB->insert_record('message_popup_notifications', $popupnotification);
1102          }
1103  
1104      }
1105  
1106      /**
1107       * Creates user last access data within given courses.
1108       *
1109       * @param array $data
1110       * @return void
1111       */
1112      protected function process_last_access_times(array $data) {
1113          global $DB;
1114  
1115          if (!isset($data['userid'])) {
1116              throw new Exception('\'last acces times\' requires the field \'user\' to be specified');
1117          }
1118  
1119          if (!isset($data['courseid'])) {
1120              throw new Exception('\'last acces times\' requires the field \'course\' to be specified');
1121          }
1122  
1123          if (!isset($data['lastaccess'])) {
1124              throw new Exception('\'last acces times\' requires the field \'lastaccess\' to be specified');
1125          }
1126  
1127          $userdata = [];
1128          $userdata['old'] = $DB->get_record('user', ['id' => $data['userid']], 'firstaccess, lastaccess, lastlogin, currentlogin');
1129          $userdata['new'] = [
1130              'firstaccess' => $userdata['old']->firstaccess,
1131              'lastaccess' => $userdata['old']->lastaccess,
1132              'lastlogin' => $userdata['old']->lastlogin,
1133              'currentlogin' => $userdata['old']->currentlogin,
1134          ];
1135  
1136          // Check for lastaccess data for this course.
1137          $lastaccessdata = [
1138              'userid' => $data['userid'],
1139              'courseid' => $data['courseid'],
1140          ];
1141  
1142          $lastaccessid = $DB->get_field('user_lastaccess', 'id', $lastaccessdata);
1143  
1144          $dbdata = (object) $lastaccessdata;
1145          $dbdata->timeaccess = $data['lastaccess'];
1146  
1147          // Set the course last access time.
1148          if ($lastaccessid) {
1149              $dbdata->id = $lastaccessid;
1150              $DB->update_record('user_lastaccess', $dbdata);
1151          } else {
1152              $DB->insert_record('user_lastaccess', $dbdata);
1153          }
1154  
1155          // Store changes to other user access times as needed.
1156  
1157          // Update first access if this is the user's first login, or this access is earlier than their current first access.
1158          if (empty($userdata['new']['firstaccess']) ||
1159                  $userdata['new']['firstaccess'] > $data['lastaccess']) {
1160              $userdata['new']['firstaccess'] = $data['lastaccess'];
1161          }
1162  
1163          // Update last access if it is the user's most recent access.
1164          if (empty($userdata['new']['lastaccess']) ||
1165                  $userdata['new']['lastaccess'] < $data['lastaccess']) {
1166              $userdata['new']['lastaccess'] = $data['lastaccess'];
1167          }
1168  
1169          // Update last and current login if it is the user's most recent access.
1170          if (empty($userdata['new']['lastlogin']) ||
1171                  $userdata['new']['lastlogin'] < $data['lastaccess']) {
1172              $userdata['new']['lastlogin'] = $data['lastaccess'];
1173              $userdata['new']['currentlogin'] = $data['lastaccess'];
1174          }
1175  
1176          $updatedata = [];
1177  
1178          if ($userdata['new']['firstaccess'] != $userdata['old']->firstaccess) {
1179              $updatedata['firstaccess'] = $userdata['new']['firstaccess'];
1180          }
1181  
1182          if ($userdata['new']['lastaccess'] != $userdata['old']->lastaccess) {
1183              $updatedata['lastaccess'] = $userdata['new']['lastaccess'];
1184          }
1185  
1186          if ($userdata['new']['lastlogin'] != $userdata['old']->lastlogin) {
1187              $updatedata['lastlogin'] = $userdata['new']['lastlogin'];
1188          }
1189  
1190          if ($userdata['new']['currentlogin'] != $userdata['old']->currentlogin) {
1191              $updatedata['currentlogin'] = $userdata['new']['currentlogin'];
1192          }
1193  
1194          // Only update user access data if there have been any changes.
1195          if (!empty($updatedata)) {
1196              $updatedata['id'] = $data['userid'];
1197              $updatedata = (object) $updatedata;
1198              $DB->update_record('user', $updatedata);
1199          }
1200      }
1201  }