Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * External course functions unit tests
  19   *
  20   * @package    core_course
  21   * @category   external
  22   * @copyright  2012 Jerome Mouneyrac
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  use \core_external\external_api;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  
  32  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  33  
  34  /**
  35   * External course functions unit tests
  36   *
  37   * @package    core_course
  38   * @category   external
  39   * @copyright  2012 Jerome Mouneyrac
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class externallib_test extends externallib_advanced_testcase {
  43      //core_course_externallib_testcase
  44  
  45      /**
  46       * Tests set up
  47       */
  48      protected function setUp(): void {
  49          global $CFG;
  50          require_once($CFG->dirroot . '/course/externallib.php');
  51      }
  52  
  53      /**
  54       * Test create_categories
  55       */
  56      public function test_create_categories() {
  57  
  58          global $DB;
  59  
  60          $this->resetAfterTest(true);
  61  
  62          // Set the required capabilities by the external function
  63          $contextid = context_system::instance()->id;
  64          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
  65  
  66          // Create base categories.
  67          $category1 = new stdClass();
  68          $category1->name = 'Root Test Category 1';
  69          $category2 = new stdClass();
  70          $category2->name = 'Root Test Category 2';
  71          $category2->idnumber = 'rootcattest2';
  72          $category2->desc = 'Description for root test category 1';
  73          $category2->theme = 'classic';
  74          $categories = array(
  75              array('name' => $category1->name, 'parent' => 0),
  76              array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
  77                  'description' => $category2->desc, 'theme' => $category2->theme)
  78          );
  79  
  80          $createdcats = core_course_external::create_categories($categories);
  81  
  82          // We need to execute the return values cleaning process to simulate the web service server.
  83          $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
  84  
  85          // Initially confirm that base data was inserted correctly.
  86          $this->assertEquals($category1->name, $createdcats[0]['name']);
  87          $this->assertEquals($category2->name, $createdcats[1]['name']);
  88  
  89          // Save the ids.
  90          $category1->id = $createdcats[0]['id'];
  91          $category2->id = $createdcats[1]['id'];
  92  
  93          // Create on sub category.
  94          $category3 = new stdClass();
  95          $category3->name = 'Sub Root Test Category 3';
  96          $subcategories = array(
  97              array('name' => $category3->name, 'parent' => $category1->id)
  98          );
  99  
 100          $createdsubcats = core_course_external::create_categories($subcategories);
 101  
 102          // We need to execute the return values cleaning process to simulate the web service server.
 103          $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
 104  
 105          // Confirm that sub categories were inserted correctly.
 106          $this->assertEquals($category3->name, $createdsubcats[0]['name']);
 107  
 108          // Save the ids.
 109          $category3->id = $createdsubcats[0]['id'];
 110  
 111          // Calling the ws function should provide a new sortorder to give category1,
 112          // category2, category3. New course categories are ordered by id not name.
 113          $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
 114          $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
 115          $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
 116  
 117          // sortorder sequence (and sortorder) must be:
 118          // category 1
 119          //   category 3
 120          // category 2
 121          $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
 122          $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
 123  
 124          // Call without required capability
 125          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 126          $this->expectException('required_capability_exception');
 127          $createdsubcats = core_course_external::create_categories($subcategories);
 128  
 129      }
 130  
 131      /**
 132       * Test delete categories
 133       */
 134      public function test_delete_categories() {
 135          global $DB;
 136  
 137          $this->resetAfterTest(true);
 138  
 139          // Set the required capabilities by the external function
 140          $contextid = context_system::instance()->id;
 141          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
 142  
 143          $category1  = self::getDataGenerator()->create_category();
 144          $category2  = self::getDataGenerator()->create_category(
 145                  array('parent' => $category1->id));
 146          $category3  = self::getDataGenerator()->create_category();
 147          $category4  = self::getDataGenerator()->create_category(
 148                  array('parent' => $category3->id));
 149          $category5  = self::getDataGenerator()->create_category(
 150                  array('parent' => $category4->id));
 151  
 152          //delete category 1 and 2 + delete category 4, category 5 moved under category 3
 153          core_course_external::delete_categories(array(
 154              array('id' => $category1->id, 'recursive' => 1),
 155              array('id' => $category4->id)
 156          ));
 157  
 158          //check $category 1 and 2 are deleted
 159          $notdeletedcount = $DB->count_records_select('course_categories',
 160              'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
 161          $this->assertEquals(0, $notdeletedcount);
 162  
 163          //check that $category5 as $category3 for parent
 164          $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
 165          $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
 166  
 167           // Call without required capability
 168          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 169          $this->expectException('required_capability_exception');
 170          $createdsubcats = core_course_external::delete_categories(
 171                  array(array('id' => $category3->id)));
 172      }
 173  
 174      /**
 175       * Test get categories
 176       */
 177      public function test_get_categories() {
 178          global $DB;
 179  
 180          $this->resetAfterTest(true);
 181  
 182          $generatedcats = array();
 183          $category1data['idnumber'] = 'idnumbercat1';
 184          $category1data['name'] = 'Category 1 for PHPunit test';
 185          $category1data['description'] = 'Category 1 description';
 186          $category1data['descriptionformat'] = FORMAT_MOODLE;
 187          $category1  = self::getDataGenerator()->create_category($category1data);
 188          $generatedcats[$category1->id] = $category1;
 189          $category2  = self::getDataGenerator()->create_category(
 190                  array('parent' => $category1->id));
 191          $generatedcats[$category2->id] = $category2;
 192          $category6  = self::getDataGenerator()->create_category(
 193                  array('parent' => $category1->id, 'visible' => 0));
 194          $generatedcats[$category6->id] = $category6;
 195          $category3  = self::getDataGenerator()->create_category();
 196          $generatedcats[$category3->id] = $category3;
 197          $category4  = self::getDataGenerator()->create_category(
 198                  array('parent' => $category3->id));
 199          $generatedcats[$category4->id] = $category4;
 200          $category5  = self::getDataGenerator()->create_category(
 201                  array('parent' => $category4->id));
 202          $generatedcats[$category5->id] = $category5;
 203  
 204          // Set the required capabilities by the external function.
 205          $context = context_system::instance();
 206          $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
 207          $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 208  
 209          // Retrieve category1 + sub-categories except not visible ones
 210          $categories = core_course_external::get_categories(array(
 211              array('key' => 'id', 'value' => $category1->id),
 212              array('key' => 'visible', 'value' => 1)), 1);
 213  
 214          // We need to execute the return values cleaning process to simulate the web service server.
 215          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 216  
 217          // Check we retrieve the good total number of categories.
 218          $this->assertEquals(2, count($categories));
 219  
 220          // Check the return values
 221          foreach ($categories as $category) {
 222              $generatedcat = $generatedcats[$category['id']];
 223              $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
 224              $this->assertEquals($category['name'], $generatedcat->name);
 225              // Description was converted to the HTML format.
 226              $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
 227              $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
 228          }
 229  
 230          // Check categories by ids.
 231          $ids = implode(',', array_keys($generatedcats));
 232          $categories = core_course_external::get_categories(array(
 233              array('key' => 'ids', 'value' => $ids)), 0);
 234  
 235          // We need to execute the return values cleaning process to simulate the web service server.
 236          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 237  
 238          // Check we retrieve the good total number of categories.
 239          $this->assertEquals(6, count($categories));
 240          // Check ids.
 241          $returnedids = [];
 242          foreach ($categories as $category) {
 243              $returnedids[] = $category['id'];
 244          }
 245          // Sort the arrays upon comparision.
 246          $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids);
 247  
 248          // Check different params.
 249          $categories = core_course_external::get_categories(array(
 250              array('key' => 'id', 'value' => $category1->id),
 251              array('key' => 'ids', 'value' => $category1->id),
 252              array('key' => 'idnumber', 'value' => $category1->idnumber),
 253              array('key' => 'visible', 'value' => 1)), 0);
 254  
 255          // We need to execute the return values cleaning process to simulate the web service server.
 256          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 257  
 258          $this->assertEquals(1, count($categories));
 259  
 260          // Same query, but forcing a parameters clean.
 261          $categories = core_course_external::get_categories(array(
 262              array('key' => 'id', 'value' => "$category1->id"),
 263              array('key' => 'idnumber', 'value' => $category1->idnumber),
 264              array('key' => 'name', 'value' => $category1->name . "<br/>"),
 265              array('key' => 'visible', 'value' => '1')), 0);
 266          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 267  
 268          $this->assertEquals(1, count($categories));
 269  
 270          // Retrieve categories from parent.
 271          $categories = core_course_external::get_categories(array(
 272              array('key' => 'parent', 'value' => $category3->id)), 1);
 273          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 274  
 275          $this->assertEquals(2, count($categories));
 276  
 277          // Retrieve all categories.
 278          $categories = core_course_external::get_categories();
 279  
 280          // We need to execute the return values cleaning process to simulate the web service server.
 281          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 282  
 283          $this->assertEquals($DB->count_records('course_categories'), count($categories));
 284  
 285          $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 286  
 287          // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
 288          // It should retrieve all visible categories as well.
 289          set_config('maxcategorydepth', 2);
 290          $categories = core_course_external::get_categories();
 291  
 292          // We need to execute the return values cleaning process to simulate the web service server.
 293          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 294  
 295          $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
 296  
 297          // Call without required capability (it will fail cause of the search on idnumber).
 298          $this->expectException('moodle_exception');
 299          $categories = core_course_external::get_categories(array(
 300              array('key' => 'id', 'value' => $category1->id),
 301              array('key' => 'idnumber', 'value' => $category1->idnumber),
 302              array('key' => 'visible', 'value' => 1)), 0);
 303      }
 304  
 305      /**
 306       * Test update_categories
 307       */
 308      public function test_update_categories() {
 309          global $DB;
 310  
 311          $this->resetAfterTest(true);
 312  
 313          // Set the required capabilities by the external function
 314          $contextid = context_system::instance()->id;
 315          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
 316  
 317          // Create base categories.
 318          $category1data['idnumber'] = 'idnumbercat1';
 319          $category1data['name'] = 'Category 1 for PHPunit test';
 320          $category1data['description'] = 'Category 1 description';
 321          $category1data['descriptionformat'] = FORMAT_MOODLE;
 322          $category1  = self::getDataGenerator()->create_category($category1data);
 323          $category2  = self::getDataGenerator()->create_category(
 324                  array('parent' => $category1->id));
 325          $category3  = self::getDataGenerator()->create_category();
 326          $category4  = self::getDataGenerator()->create_category(
 327                  array('parent' => $category3->id));
 328          $category5  = self::getDataGenerator()->create_category(
 329                  array('parent' => $category4->id));
 330  
 331          // We update all category1 attribut.
 332          // Then we move cat4 and cat5 parent: cat3 => cat1
 333          $categories = array(
 334              array('id' => $category1->id,
 335                  'name' => $category1->name . '_updated',
 336                  'idnumber' => $category1->idnumber . '_updated',
 337                  'description' => $category1->description . '_updated',
 338                  'descriptionformat' => FORMAT_HTML,
 339                  'theme' => $category1->theme),
 340              array('id' => $category4->id, 'parent' => $category1->id));
 341  
 342          core_course_external::update_categories($categories);
 343  
 344          // Check the values were updated.
 345          $dbcategories = $DB->get_records_select('course_categories',
 346                  'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
 347                  . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
 348          $this->assertEquals($category1->name . '_updated',
 349                  $dbcategories[$category1->id]->name);
 350          $this->assertEquals($category1->idnumber . '_updated',
 351                  $dbcategories[$category1->id]->idnumber);
 352          $this->assertEquals($category1->description . '_updated',
 353                  $dbcategories[$category1->id]->description);
 354          $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
 355  
 356          // Check that category4 and category5 have been properly moved.
 357          $this->assertEquals('/' . $category1->id . '/' . $category4->id,
 358                  $dbcategories[$category4->id]->path);
 359          $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
 360                  $dbcategories[$category5->id]->path);
 361  
 362          // Call without required capability.
 363          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 364          $this->expectException('required_capability_exception');
 365          core_course_external::update_categories($categories);
 366      }
 367  
 368      /**
 369       * Test update_categories method for moving categories
 370       */
 371      public function test_update_categories_moving() {
 372          $this->resetAfterTest();
 373  
 374          // Create data.
 375          $categorya  = self::getDataGenerator()->create_category([
 376              'name' => 'CAT_A',
 377          ]);
 378          $categoryasub = self::getDataGenerator()->create_category([
 379              'name' => 'SUBCAT_A',
 380              'parent' => $categorya->id
 381          ]);
 382          $categoryb  = self::getDataGenerator()->create_category([
 383              'name' => 'CAT_B',
 384          ]);
 385  
 386          // Create a new test user.
 387          $testuser = self::getDataGenerator()->create_user();
 388          $this->setUser($testuser);
 389  
 390          // Set the capability for CAT_A only.
 391          $contextcata = context_coursecat::instance($categorya->id);
 392          $roleid = $this->assignUserCapability('moodle/category:manage', $contextcata->id);
 393  
 394          // Then we move SUBCAT_A parent: CAT_A => CAT_B.
 395          $categories = [
 396              [
 397                  'id' => $categoryasub->id,
 398                  'parent' => $categoryb->id
 399              ]
 400          ];
 401  
 402          $this->expectException('required_capability_exception');
 403          core_course_external::update_categories($categories);
 404      }
 405  
 406      /**
 407       * Test create_courses numsections
 408       */
 409      public function test_create_course_numsections() {
 410          global $DB;
 411  
 412          $this->resetAfterTest(true);
 413  
 414          // Set the required capabilities by the external function.
 415          $contextid = context_system::instance()->id;
 416          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
 417          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
 418  
 419          $numsections = 10;
 420          $category  = self::getDataGenerator()->create_category();
 421  
 422          // Create base categories.
 423          $course1['fullname'] = 'Test course 1';
 424          $course1['shortname'] = 'Testcourse1';
 425          $course1['categoryid'] = $category->id;
 426          $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
 427  
 428          $courses = array($course1);
 429  
 430          $createdcourses = core_course_external::create_courses($courses);
 431          foreach ($createdcourses as $createdcourse) {
 432              $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
 433              $modinfo = get_fast_modinfo($createdcourse['id']);
 434              $sections = $modinfo->get_section_info_all();
 435              $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
 436              $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
 437          }
 438      }
 439  
 440      /**
 441       * Test create_courses
 442       */
 443      public function test_create_courses() {
 444          global $DB;
 445  
 446          $this->resetAfterTest(true);
 447  
 448          // Enable course completion.
 449          set_config('enablecompletion', 1);
 450          // Enable course themes.
 451          set_config('allowcoursethemes', 1);
 452  
 453          // Custom fields.
 454          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
 455  
 456          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
 457              'categoryid' => $fieldcategory->get('id'),
 458              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL]];
 459          $field = self::getDataGenerator()->create_custom_field($customfield);
 460  
 461          // Set the required capabilities by the external function
 462          $contextid = context_system::instance()->id;
 463          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
 464          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
 465          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
 466  
 467          $category  = self::getDataGenerator()->create_category();
 468  
 469          // Create base categories.
 470          $course1['fullname'] = 'Test course 1';
 471          $course1['shortname'] = 'Testcourse1';
 472          $course1['categoryid'] = $category->id;
 473          $course2['fullname'] = 'Test course 2';
 474          $course2['shortname'] = 'Testcourse2';
 475          $course2['categoryid'] = $category->id;
 476          $course2['idnumber'] = 'testcourse2idnumber';
 477          $course2['summary'] = 'Description for course 2';
 478          $course2['summaryformat'] = FORMAT_MOODLE;
 479          $course2['format'] = 'weeks';
 480          $course2['showgrades'] = 1;
 481          $course2['newsitems'] = 3;
 482          $course2['startdate'] = 1420092000; // 01/01/2015.
 483          $course2['enddate'] = 1422669600; // 01/31/2015.
 484          $course2['numsections'] = 4;
 485          $course2['maxbytes'] = 100000;
 486          $course2['showreports'] = 1;
 487          $course2['visible'] = 0;
 488          $course2['hiddensections'] = 0;
 489          $course2['groupmode'] = 0;
 490          $course2['groupmodeforce'] = 0;
 491          $course2['defaultgroupingid'] = 0;
 492          $course2['enablecompletion'] = 1;
 493          $course2['completionnotify'] = 1;
 494          $course2['lang'] = 'en';
 495          $course2['forcetheme'] = 'classic';
 496          $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
 497          $course3['fullname'] = 'Test course 3';
 498          $course3['shortname'] = 'Testcourse3';
 499          $course3['categoryid'] = $category->id;
 500          $course3['format'] = 'topics';
 501          $course3options = array('numsections' => 8,
 502              'hiddensections' => 1,
 503              'coursedisplay' => 1);
 504          $course3['courseformatoptions'] = array();
 505          foreach ($course3options as $key => $value) {
 506              $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
 507          }
 508          $course4['fullname'] = 'Test course with custom fields';
 509          $course4['shortname'] = 'Testcoursecustomfields';
 510          $course4['categoryid'] = $category->id;
 511          $course4['customfields'] = [['shortname' => $customfield['shortname'], 'value' => 'Test value']];
 512          $courses = array($course4, $course1, $course2, $course3);
 513  
 514          $createdcourses = core_course_external::create_courses($courses);
 515  
 516          // We need to execute the return values cleaning process to simulate the web service server.
 517          $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
 518  
 519          // Check that right number of courses were created.
 520          $this->assertEquals(4, count($createdcourses));
 521  
 522          // Check that the courses were correctly created.
 523          foreach ($createdcourses as $createdcourse) {
 524              $courseinfo = course_get_format($createdcourse['id'])->get_course();
 525  
 526              if ($createdcourse['shortname'] == $course2['shortname']) {
 527                  $this->assertEquals($courseinfo->fullname, $course2['fullname']);
 528                  $this->assertEquals($courseinfo->shortname, $course2['shortname']);
 529                  $this->assertEquals($courseinfo->category, $course2['categoryid']);
 530                  $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
 531                  $this->assertEquals($courseinfo->summary, $course2['summary']);
 532                  $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
 533                  $this->assertEquals($courseinfo->format, $course2['format']);
 534                  $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
 535                  $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
 536                  $this->assertEquals($courseinfo->startdate, $course2['startdate']);
 537                  $this->assertEquals($courseinfo->enddate, $course2['enddate']);
 538                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
 539                  $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
 540                  $this->assertEquals($courseinfo->showreports, $course2['showreports']);
 541                  $this->assertEquals($courseinfo->visible, $course2['visible']);
 542                  $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
 543                  $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
 544                  $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
 545                  $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
 546                  $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
 547                  $this->assertEquals($courseinfo->lang, $course2['lang']);
 548                  $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
 549  
 550                  // We enabled completion at the beginning of the test.
 551                  $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
 552  
 553              } else if ($createdcourse['shortname'] == $course1['shortname']) {
 554                  $courseconfig = get_config('moodlecourse');
 555                  $this->assertEquals($courseinfo->fullname, $course1['fullname']);
 556                  $this->assertEquals($courseinfo->shortname, $course1['shortname']);
 557                  $this->assertEquals($courseinfo->category, $course1['categoryid']);
 558                  $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
 559                  $this->assertEquals($courseinfo->format, $courseconfig->format);
 560                  $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
 561                  $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
 562                  $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
 563                  $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
 564                  $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
 565                  $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
 566                  $this->assertEquals($courseinfo->defaultgroupingid, 0);
 567              } else if ($createdcourse['shortname'] == $course3['shortname']) {
 568                  $this->assertEquals($courseinfo->fullname, $course3['fullname']);
 569                  $this->assertEquals($courseinfo->shortname, $course3['shortname']);
 570                  $this->assertEquals($courseinfo->category, $course3['categoryid']);
 571                  $this->assertEquals($courseinfo->format, $course3['format']);
 572                  $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
 573                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
 574                      $course3options['numsections']);
 575                  $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
 576              } else if ($createdcourse['shortname'] == $course4['shortname']) {
 577                  $this->assertEquals($courseinfo->fullname, $course4['fullname']);
 578                  $this->assertEquals($courseinfo->shortname, $course4['shortname']);
 579                  $this->assertEquals($courseinfo->category, $course4['categoryid']);
 580  
 581                  $handler = core_course\customfield\course_handler::create();
 582                  $customfields = $handler->export_instance_data_object($createdcourse['id']);
 583                  $this->assertEquals((object)['test' => 'Test value'], $customfields);
 584              } else {
 585                  throw new moodle_exception('Unexpected shortname');
 586              }
 587          }
 588  
 589          // Call without required capability
 590          $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
 591          $this->expectException('required_capability_exception');
 592          $createdsubcats = core_course_external::create_courses($courses);
 593      }
 594  
 595      /**
 596       * Data provider for testing empty fields produce expected exceptions
 597       *
 598       * @see test_create_courses_empty_field
 599       * @see test_update_courses_empty_field
 600       *
 601       * @return array
 602       */
 603      public function course_empty_field_provider(): array {
 604          return [
 605              [[
 606                  'fullname' => '',
 607                  'shortname' => 'ws101',
 608              ], 'fullname'],
 609              [[
 610                  'fullname' => ' ',
 611                  'shortname' => 'ws101',
 612              ], 'fullname'],
 613              [[
 614                  'fullname' => 'Web Services',
 615                  'shortname' => '',
 616              ], 'shortname'],
 617              [[
 618                  'fullname' => 'Web Services',
 619                  'shortname' => ' ',
 620              ], 'shortname'],
 621          ];
 622      }
 623  
 624      /**
 625       * Test creating courses with empty fields throws an exception
 626       *
 627       * @param array $course
 628       * @param string $expectedemptyfield
 629       *
 630       * @dataProvider course_empty_field_provider
 631       */
 632      public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
 633          $this->resetAfterTest();
 634          $this->setAdminUser();
 635  
 636          // Create a category for the new course.
 637          $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
 638  
 639          $this->expectException(moodle_exception::class);
 640          $this->expectExceptionMessageMatches("/{$expectedemptyfield}/");
 641          core_course_external::create_courses([$course]);
 642      }
 643  
 644      /**
 645       * Test updating courses with empty fields returns warnings
 646       *
 647       * @param array $course
 648       * @param string $expectedemptyfield
 649       *
 650       * @dataProvider course_empty_field_provider
 651       */
 652      public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
 653          $this->resetAfterTest();
 654          $this->setAdminUser();
 655  
 656          // Create a course to update.
 657          $course['id'] = $this->getDataGenerator()->create_course()->id;
 658  
 659          $result = core_course_external::update_courses([$course]);
 660          $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
 661  
 662          $this->assertCount(1, $result['warnings']);
 663  
 664          $warning = reset($result['warnings']);
 665          $this->assertEquals('errorinvalidparam', $warning['warningcode']);
 666          $this->assertStringContainsString($expectedemptyfield, $warning['message']);
 667      }
 668  
 669      /**
 670       * Test delete_courses
 671       */
 672      public function test_delete_courses() {
 673          global $DB, $USER;
 674  
 675          $this->resetAfterTest(true);
 676  
 677          // Admin can delete a course.
 678          $this->setAdminUser();
 679          // Validate_context() will fail as the email is not set by $this->setAdminUser().
 680          $USER->email = 'emailtopass@example.com';
 681  
 682          $course1  = self::getDataGenerator()->create_course();
 683          $course2  = self::getDataGenerator()->create_course();
 684          $course3  = self::getDataGenerator()->create_course();
 685  
 686          // Delete courses.
 687          $result = core_course_external::delete_courses(array($course1->id, $course2->id));
 688          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 689          // Check for 0 warnings.
 690          $this->assertEquals(0, count($result['warnings']));
 691  
 692          // Check $course 1 and 2 are deleted.
 693          $notdeletedcount = $DB->count_records_select('course',
 694              'id IN ( ' . $course1->id . ',' . $course2->id . ')');
 695          $this->assertEquals(0, $notdeletedcount);
 696  
 697          // Try to delete non-existent course.
 698          $result = core_course_external::delete_courses(array($course1->id));
 699          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 700          // Check for 1 warnings.
 701          $this->assertEquals(1, count($result['warnings']));
 702  
 703          // Try to delete Frontpage course.
 704          $result = core_course_external::delete_courses(array(0));
 705          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 706          // Check for 1 warnings.
 707          $this->assertEquals(1, count($result['warnings']));
 708  
 709           // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
 710          $student1 = self::getDataGenerator()->create_user();
 711          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 712          $this->getDataGenerator()->enrol_user($student1->id,
 713                                                $course3->id,
 714                                                $studentrole->id);
 715          $this->setUser($student1);
 716          $result = core_course_external::delete_courses(array($course3->id));
 717          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 718          // Check for 1 warnings.
 719          $this->assertEquals(1, count($result['warnings']));
 720  
 721           // Fail when the user is not allow to access the course (enrolled) or is not admin.
 722          $this->setGuestUser();
 723          $this->expectException('require_login_exception');
 724  
 725          $result = core_course_external::delete_courses(array($course3->id));
 726          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 727      }
 728  
 729      /**
 730       * Test get_courses
 731       */
 732      public function test_get_courses () {
 733          global $DB;
 734  
 735          $this->resetAfterTest(true);
 736  
 737          $generatedcourses = array();
 738          $coursedata['idnumber'] = 'idnumbercourse1';
 739          // Adding tags here to check that format_string is applied.
 740          $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
 741          $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
 742          $coursedata['summary'] = 'Course 1 description';
 743          $coursedata['summaryformat'] = FORMAT_MOODLE;
 744          $course1  = self::getDataGenerator()->create_course($coursedata);
 745  
 746          $fieldcategory = self::getDataGenerator()->create_custom_field_category(
 747              ['name' => 'Other fields']);
 748  
 749          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
 750              'categoryid' => $fieldcategory->get('id')];
 751          $field = self::getDataGenerator()->create_custom_field($customfield);
 752  
 753          $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
 754  
 755          $generatedcourses[$course1->id] = $course1;
 756          $course2  = self::getDataGenerator()->create_course();
 757          $generatedcourses[$course2->id] = $course2;
 758          $course3  = self::getDataGenerator()->create_course(array('format' => 'topics'));
 759          $generatedcourses[$course3->id] = $course3;
 760          $course4  = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
 761          $generatedcourses[$course4->id] = $course4;
 762  
 763          // Set the required capabilities by the external function.
 764          $context = context_system::instance();
 765          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
 766          $this->assignUserCapability('moodle/course:update',
 767                  context_course::instance($course1->id)->id, $roleid);
 768          $this->assignUserCapability('moodle/course:update',
 769                  context_course::instance($course2->id)->id, $roleid);
 770          $this->assignUserCapability('moodle/course:update',
 771                  context_course::instance($course3->id)->id, $roleid);
 772          $this->assignUserCapability('moodle/course:update',
 773                  context_course::instance($course4->id)->id, $roleid);
 774  
 775          $courses = core_course_external::get_courses(array('ids' =>
 776              array($course1->id, $course2->id, $course4->id)));
 777  
 778          // We need to execute the return values cleaning process to simulate the web service server.
 779          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 780  
 781          // Check we retrieve the good total number of courses.
 782          $this->assertEquals(3, count($courses));
 783  
 784          foreach ($courses as $course) {
 785              $coursecontext = context_course::instance($course['id']);
 786              $dbcourse = $generatedcourses[$course['id']];
 787              $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
 788              $this->assertEquals(
 789                  $course['fullname'],
 790                  \core_external\util::format_string($dbcourse->fullname, $coursecontext->id)
 791              );
 792              $this->assertEquals(
 793                  $course['displayname'],
 794                  \core_external\util::format_string(get_course_display_name_for_list($dbcourse), $coursecontext->id)
 795              );
 796              // Summary was converted to the HTML format.
 797              $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
 798              $this->assertEquals($course['summaryformat'], FORMAT_HTML);
 799              $this->assertEquals($course['shortname'], \core_external\util::format_string($dbcourse->shortname, $coursecontext->id));
 800              $this->assertEquals($course['categoryid'], $dbcourse->category);
 801              $this->assertEquals($course['format'], $dbcourse->format);
 802              $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
 803              $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
 804              $this->assertEquals($course['startdate'], $dbcourse->startdate);
 805              $this->assertEquals($course['enddate'], $dbcourse->enddate);
 806              $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
 807              $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
 808              $this->assertEquals($course['showreports'], $dbcourse->showreports);
 809              $this->assertEquals($course['visible'], $dbcourse->visible);
 810              $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
 811              $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
 812              $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
 813              $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
 814              $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
 815              $this->assertEquals($course['lang'], $dbcourse->lang);
 816              $this->assertEquals($course['forcetheme'], $dbcourse->theme);
 817              $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
 818              if ($dbcourse->format === 'topics') {
 819                  $this->assertEquals($course['courseformatoptions'], array(
 820                      array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
 821                      array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
 822                  ));
 823              }
 824  
 825              // Assert custom field that we previously added to test course 4.
 826              if ($dbcourse->id == $course4->id) {
 827                  $this->assertEquals([
 828                      'shortname' => $customfield['shortname'],
 829                      'name' => $customfield['name'],
 830                      'type' => $customfield['type'],
 831                      'value' => $customfieldvalue['value'],
 832                      'valueraw' => $customfieldvalue['value'],
 833                  ], $course['customfields'][0]);
 834              }
 835          }
 836  
 837          // Get all courses in the DB
 838          $courses = core_course_external::get_courses(array());
 839  
 840          // We need to execute the return values cleaning process to simulate the web service server.
 841          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 842  
 843          $this->assertEquals($DB->count_records('course'), count($courses));
 844      }
 845  
 846      /**
 847       * Test retrieving courses returns custom field data
 848       */
 849      public function test_get_courses_customfields(): void {
 850          $this->resetAfterTest();
 851          $this->setAdminUser();
 852  
 853          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
 854          $datefield = $this->getDataGenerator()->create_custom_field([
 855              'categoryid' => $fieldcategory->get('id'),
 856              'shortname' => 'mydate',
 857              'name' => 'My date',
 858              'type' => 'date',
 859          ]);
 860  
 861          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
 862              [
 863                  'shortname' => $datefield->get('shortname'),
 864                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
 865              ],
 866          ]]);
 867  
 868          $courses = external_api::clean_returnvalue(
 869              core_course_external::get_courses_returns(),
 870              core_course_external::get_courses(['ids' => [$newcourse->id]])
 871          );
 872  
 873          $this->assertCount(1, $courses);
 874          $course = reset($courses);
 875  
 876          $this->assertArrayHasKey('customfields', $course);
 877          $this->assertCount(1, $course['customfields']);
 878  
 879          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
 880          $this->assertEquals([
 881              'name' => $datefield->get('name'),
 882              'shortname' => $datefield->get('shortname'),
 883              'type' => $datefield->get('type'),
 884              'value' => userdate(1580389200),
 885              'valueraw' => 1580389200,
 886          ], reset($course['customfields']));
 887      }
 888  
 889      /**
 890       * Test get_courses without capability
 891       */
 892      public function test_get_courses_without_capability() {
 893          $this->resetAfterTest(true);
 894  
 895          $course1 = $this->getDataGenerator()->create_course();
 896          $this->setUser($this->getDataGenerator()->create_user());
 897  
 898          // No permissions are required to get the site course.
 899          $courses = core_course_external::get_courses(array('ids' => [SITEID]));
 900          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 901  
 902          $this->assertEquals(1, count($courses));
 903          $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
 904          $this->assertEquals('site', $courses[0]['format']);
 905  
 906          // Requesting course without being enrolled or capability to view it will throw an exception.
 907          try {
 908              core_course_external::get_courses(array('ids' => [$course1->id]));
 909              $this->fail('Exception expected');
 910          } catch (moodle_exception $e) {
 911              $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
 912          }
 913      }
 914  
 915      /**
 916       * Test search_courses
 917       */
 918      public function test_search_courses () {
 919  
 920          global $DB;
 921  
 922          $this->resetAfterTest(true);
 923          $this->setAdminUser();
 924          $generatedcourses = array();
 925          $coursedata1['fullname'] = 'FIRST COURSE';
 926          $course1  = self::getDataGenerator()->create_course($coursedata1);
 927  
 928          $page = new moodle_page();
 929          $page->set_course($course1);
 930          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
 931  
 932          $coursedata2['fullname'] = 'SECOND COURSE';
 933          $course2  = self::getDataGenerator()->create_course($coursedata2);
 934  
 935          $page = new moodle_page();
 936          $page->set_course($course2);
 937          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
 938  
 939          // Search by name.
 940          $results = core_course_external::search_courses('search', 'FIRST');
 941          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 942          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
 943          $this->assertCount(1, $results['courses']);
 944  
 945          // Create the forum.
 946          $record = new stdClass();
 947          $record->introformat = FORMAT_HTML;
 948          $record->course = $course2->id;
 949          // Set Aggregate type = Average of ratings.
 950          $forum = self::getDataGenerator()->create_module('forum', $record);
 951  
 952          // Search by module.
 953          $results = core_course_external::search_courses('modulelist', 'forum');
 954          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 955          $this->assertEquals(1, $results['total']);
 956  
 957          // Enable coursetag option.
 958          set_config('block_tags_showcoursetags', true);
 959          // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
 960          core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
 961                  array('TAG-LABEL ON SECOND COURSE'));
 962          $taginstance = $DB->get_record('tag_instance',
 963                  array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
 964  
 965          // Search by tagid.
 966          $results = core_course_external::search_courses('tagid', $taginstance->tagid);
 967          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 968          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
 969  
 970          // Search by block (use news_items default block).
 971          $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
 972          $results = core_course_external::search_courses('blocklist', $blockid);
 973          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 974          $this->assertEquals(2, $results['total']);
 975  
 976          // Now as a normal user.
 977          $user = self::getDataGenerator()->create_user();
 978  
 979          // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
 980          $coursedata3['fullname'] = 'HIDDEN COURSE';
 981          $coursedata3['visible'] = 0;
 982          $course3  = self::getDataGenerator()->create_course($coursedata3);
 983          $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
 984  
 985          $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
 986          $this->setUser($user);
 987  
 988          $results = core_course_external::search_courses('search', 'FIRST');
 989          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 990          $this->assertCount(1, $results['courses']);
 991          $this->assertEquals(1, $results['total']);
 992          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
 993  
 994          // Check that we can see all courses without the limit to enrolled setting.
 995          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
 996          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 997          $this->assertCount(2, $results['courses']);
 998          $this->assertEquals(2, $results['total']);
 999  
1000          // Check that we only see our enrolled course when limiting.
1001          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
1002          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1003          $this->assertCount(1, $results['courses']);
1004          $this->assertEquals(1, $results['total']);
1005          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
1006  
1007          // Search by block (use news_items default block). Should fail (only admins allowed).
1008          $this->expectException('required_capability_exception');
1009          $results = core_course_external::search_courses('blocklist', $blockid);
1010      }
1011  
1012      /**
1013       * Test searching for courses returns custom field data
1014       */
1015      public function test_search_courses_customfields(): void {
1016          $this->resetAfterTest();
1017          $this->setAdminUser();
1018  
1019          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
1020          $datefield = $this->getDataGenerator()->create_custom_field([
1021              'categoryid' => $fieldcategory->get('id'),
1022              'shortname' => 'mydate',
1023              'name' => 'My date',
1024              'type' => 'date',
1025          ]);
1026  
1027          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
1028              [
1029                  'shortname' => $datefield->get('shortname'),
1030                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
1031              ],
1032          ]]);
1033  
1034          $result = external_api::clean_returnvalue(
1035              core_course_external::search_courses_returns(),
1036              core_course_external::search_courses('search', $newcourse->shortname)
1037          );
1038  
1039          $this->assertCount(1, $result['courses']);
1040          $course = reset($result['courses']);
1041  
1042          $this->assertArrayHasKey('customfields', $course);
1043          $this->assertCount(1, $course['customfields']);
1044  
1045          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1046          $this->assertEquals([
1047              'name' => $datefield->get('name'),
1048              'shortname' => $datefield->get('shortname'),
1049              'type' => $datefield->get('type'),
1050              'value' => userdate(1580389200),
1051              'valueraw' => 1580389200,
1052          ], reset($course['customfields']));
1053      }
1054  
1055      /**
1056       * Create a course with contents
1057       * @return array A list with the course object and course modules objects
1058       */
1059      private function prepare_get_course_contents_test() {
1060          global $DB, $CFG;
1061  
1062          $CFG->allowstealth = 1; // Allow stealth activities.
1063          $CFG->enablecompletion = true;
1064          // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1065          $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1066  
1067          $forumdescription = 'This is the forum description';
1068          $forum = $this->getDataGenerator()->create_module('forum',
1069              array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1070              array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1071          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1072          // Add discussions to the tracking forced forum.
1073          $record = new stdClass();
1074          $record->course = $course->id;
1075          $record->userid = 0;
1076          $record->forum = $forum->id;
1077          $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1078          $data = $this->getDataGenerator()->create_module('data',
1079              array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1080          $datacm = get_coursemodule_from_instance('data', $data->id);
1081          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1082          $pagecm = get_coursemodule_from_instance('page', $page->id);
1083          // This is an stealth page (set by visibleoncoursepage).
1084          $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1085          $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1086                  So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1087          $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1088              'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1089          $labelcm = get_coursemodule_from_instance('label', $label->id);
1090          $tomorrow = time() + DAYSECS;
1091          // Module with availability restrictions not met.
1092          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1093                  .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1094          $url = $this->getDataGenerator()->create_module('url',
1095              array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1096                  'popupwidth' => 100, 'popupheight' => 100),
1097              array('availability' => $availability));
1098          $urlcm = get_coursemodule_from_instance('url', $url->id);
1099          // Module for the last section.
1100          $this->getDataGenerator()->create_module('url',
1101              array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1102          // Module for section 1 with availability restrictions met.
1103          $yesterday = time() - DAYSECS;
1104          $this->getDataGenerator()->create_module('url',
1105              array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1106              array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1107  
1108          // Set the required capabilities by the external function.
1109          $context = context_course::instance($course->id);
1110          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1111          $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1112          $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1113  
1114          $conditions = array('course' => $course->id, 'section' => 2);
1115          $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1116  
1117          // Add date availability condition not met for section 3.
1118          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1119          $DB->set_field('course_sections', 'availability', $availability,
1120                  array('course' => $course->id, 'section' => 3));
1121  
1122          // Create resource for last section.
1123          $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1124              array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1125          // Set not visible last section.
1126          $DB->set_field('course_sections', 'visible', 0,
1127                  array('course' => $course->id, 'section' => 4));
1128  
1129          $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
1130              array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
1131              array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
1132          $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
1133          $sectionrecord = $DB->get_record('course_sections', $conditions);
1134          // Invalidate the section cache by given section number.
1135          course_modinfo::purge_course_section_cache_by_number($sectionrecord->course, $sectionrecord->section);
1136          rebuild_course_cache($course->id, true, true);
1137  
1138          return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
1139      }
1140  
1141      /**
1142       * Test get_course_contents
1143       */
1144      public function test_get_course_contents() {
1145          global $CFG;
1146          $this->resetAfterTest(true);
1147  
1148          $CFG->forum_allowforcedreadtracking = 1;
1149          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1150  
1151          // We first run the test as admin.
1152          $this->setAdminUser();
1153          $sections = core_course_external::get_course_contents($course->id, array());
1154          // We need to execute the return values cleaning process to simulate the web service server.
1155          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1156  
1157          $modinfo = get_fast_modinfo($course);
1158          $testexecuted = 0;
1159          foreach ($sections[0]['modules'] as $module) {
1160              if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1161                  $cm = $modinfo->cms[$forumcm->id];
1162                  $formattedtext = format_text($cm->content, FORMAT_HTML,
1163                      array('noclean' => true, 'para' => false, 'filter' => false));
1164                  $this->assertEquals($formattedtext, $module['description']);
1165                  $this->assertEquals($forumcm->instance, $module['instance']);
1166                  $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1167                  $this->assertStringContainsString('1 unread post', $module['afterlink']);
1168                  $this->assertFalse($module['noviewlink']);
1169                  $this->assertNotEmpty($module['description']);  // Module showdescription is on.
1170                  $testexecuted = $testexecuted + 2;
1171              } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1172                  $cm = $modinfo->cms[$labelcm->id];
1173                  $formattedtext = format_text($cm->content, FORMAT_HTML,
1174                      array('noclean' => true, 'para' => false, 'filter' => false));
1175                  $this->assertEquals($formattedtext, $module['description']);
1176                  $this->assertEquals($labelcm->instance, $module['instance']);
1177                  $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1178                  $this->assertTrue($module['noviewlink']);
1179                  $this->assertNotEmpty($module['description']);  // Label always prints the description.
1180                  $testexecuted = $testexecuted + 1;
1181              } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1182                  $this->assertStringContainsString('customcompletionrules', $module['customdata']);
1183                  $this->assertFalse($module['noviewlink']);
1184                  $this->assertArrayNotHasKey('description', $module);
1185                  $testexecuted = $testexecuted + 1;
1186              }
1187          }
1188          foreach ($sections[2]['modules'] as $module) {
1189              if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1190                  $this->assertStringContainsString('width=100,height=100', $module['onclick']);
1191                  $testexecuted = $testexecuted + 1;
1192              }
1193          }
1194  
1195          $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
1196          forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
1197  
1198          $this->assertEquals(5, $testexecuted);
1199          $this->assertEquals(0, $sections[0]['section']);
1200  
1201          $this->assertCount(6, $sections[0]['modules']);
1202          $this->assertCount(1, $sections[1]['modules']);
1203          $this->assertCount(1, $sections[2]['modules']);
1204          $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1205          $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1206          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1207          $this->assertEquals(1, $sections[1]['section']);
1208          $this->assertEquals(2, $sections[2]['section']);
1209          $this->assertEquals(3, $sections[3]['section']);
1210          $this->assertEquals(4, $sections[4]['section']);
1211          $this->assertStringContainsString('<iframe', $sections[2]['summary']);
1212          $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
1213          $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1214          try {
1215              $sections = core_course_external::get_course_contents($course->id,
1216                                                                      array(array("name" => "invalid", "value" => 1)));
1217              $this->fail('Exception expected due to invalid option.');
1218          } catch (moodle_exception $e) {
1219              $this->assertEquals('errorinvalidparam', $e->errorcode);
1220          }
1221      }
1222  
1223  
1224      /**
1225       * Test get_course_contents as student
1226       */
1227      public function test_get_course_contents_student() {
1228          global $DB;
1229          $this->resetAfterTest(true);
1230  
1231          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1232  
1233          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1234          $user = self::getDataGenerator()->create_user();
1235          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1236          $this->setUser($user);
1237  
1238          $sections = core_course_external::get_course_contents($course->id, array());
1239          // We need to execute the return values cleaning process to simulate the web service server.
1240          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1241  
1242          $this->assertCount(4, $sections); // Nothing for the not visible section.
1243          $this->assertCount(6, $sections[0]['modules']);
1244          $this->assertCount(1, $sections[1]['modules']);
1245          $this->assertCount(1, $sections[2]['modules']);
1246          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1247  
1248          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1249          $this->assertEquals(1, $sections[1]['section']);
1250          $this->assertEquals(2, $sections[2]['section']);
1251          $this->assertEquals(3, $sections[3]['section']);
1252          // The module with the availability restriction met is returning contents.
1253          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1254          // The module with the availability restriction not met is not returning contents.
1255          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1256  
1257          // Now include flag for returning stealth information (fake section).
1258          $sections = core_course_external::get_course_contents($course->id,
1259              array(array("name" => "includestealthmodules", "value" => 1)));
1260          // We need to execute the return values cleaning process to simulate the web service server.
1261          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1262  
1263          $this->assertCount(5, $sections); // Include fake section with stealth activities.
1264          $this->assertCount(6, $sections[0]['modules']);
1265          $this->assertCount(1, $sections[1]['modules']);
1266          $this->assertCount(1, $sections[2]['modules']);
1267          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1268          $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1269          $this->assertEquals(-1, $sections[4]['id']);
1270      }
1271  
1272      /**
1273       * Test get_course_contents excluding modules
1274       */
1275      public function test_get_course_contents_excluding_modules() {
1276          $this->resetAfterTest(true);
1277  
1278          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1279  
1280          // Test exclude modules.
1281          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1282  
1283          // We need to execute the return values cleaning process to simulate the web service server.
1284          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1285  
1286          $this->assertEmpty($sections[0]['modules']);
1287          $this->assertEmpty($sections[1]['modules']);
1288      }
1289  
1290      /**
1291       * Test get_course_contents excluding contents
1292       */
1293      public function test_get_course_contents_excluding_contents() {
1294          $this->resetAfterTest(true);
1295  
1296          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1297  
1298          // Test exclude modules.
1299          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1300  
1301          // We need to execute the return values cleaning process to simulate the web service server.
1302          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1303  
1304          foreach ($sections as $section) {
1305              foreach ($section['modules'] as $module) {
1306                  // Only resources return contents.
1307                  if (isset($module['contents'])) {
1308                      $this->assertEmpty($module['contents']);
1309                  }
1310              }
1311          }
1312      }
1313  
1314      /**
1315       * Test get_course_contents filtering by section number
1316       */
1317      public function test_get_course_contents_section_number() {
1318          $this->resetAfterTest(true);
1319  
1320          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1321  
1322          // Test exclude modules.
1323          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1324  
1325          // We need to execute the return values cleaning process to simulate the web service server.
1326          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1327  
1328          $this->assertCount(1, $sections);
1329          $this->assertCount(6, $sections[0]['modules']);
1330      }
1331  
1332      /**
1333       * Test get_course_contents filtering by cmid
1334       */
1335      public function test_get_course_contents_cmid() {
1336          $this->resetAfterTest(true);
1337  
1338          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1339  
1340          // Test exclude modules.
1341          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1342  
1343          // We need to execute the return values cleaning process to simulate the web service server.
1344          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1345  
1346          $this->assertCount(4, $sections);
1347          $this->assertCount(1, $sections[0]['modules']);
1348          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1349      }
1350  
1351  
1352      /**
1353       * Test get_course_contents filtering by cmid and section
1354       */
1355      public function test_get_course_contents_section_cmid() {
1356          $this->resetAfterTest(true);
1357  
1358          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1359  
1360          // Test exclude modules.
1361          $sections = core_course_external::get_course_contents($course->id, array(
1362                                                                          array("name" => "cmid", "value" => $forumcm->id),
1363                                                                          array("name" => "sectionnumber", "value" => 0)
1364                                                                          ));
1365  
1366          // We need to execute the return values cleaning process to simulate the web service server.
1367          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1368  
1369          $this->assertCount(1, $sections);
1370          $this->assertCount(1, $sections[0]['modules']);
1371          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1372      }
1373  
1374      /**
1375       * Test get_course_contents filtering by modname
1376       */
1377      public function test_get_course_contents_modname() {
1378          $this->resetAfterTest(true);
1379  
1380          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1381  
1382          // Test exclude modules.
1383          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1384  
1385          // We need to execute the return values cleaning process to simulate the web service server.
1386          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1387  
1388          $this->assertCount(4, $sections);
1389          $this->assertCount(2, $sections[0]['modules']);
1390          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1391      }
1392  
1393      /**
1394       * Test get_course_contents filtering by modname
1395       */
1396      public function test_get_course_contents_modid() {
1397          $this->resetAfterTest(true);
1398  
1399          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1400  
1401          // Test exclude modules.
1402          $sections = core_course_external::get_course_contents($course->id, array(
1403                                                                              array("name" => "modname", "value" => "page"),
1404                                                                              array("name" => "modid", "value" => $pagecm->instance),
1405                                                                              ));
1406  
1407          // We need to execute the return values cleaning process to simulate the web service server.
1408          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1409  
1410          $this->assertCount(4, $sections);
1411          $this->assertCount(1, $sections[0]['modules']);
1412          $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1413          $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1414      }
1415  
1416      /**
1417       * Test get_course_contents returns downloadcontent value.
1418       */
1419      public function test_get_course_contents_downloadcontent() {
1420          $this->resetAfterTest();
1421  
1422          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1423  
1424          // Test exclude modules.
1425          $sections = core_course_external::get_course_contents($course->id, [
1426              ['name' => 'modname', 'value' => 'page'],
1427              ['name' => 'modid', 'value' => $pagecm->instance]
1428          ]);
1429  
1430          // We need to execute the return values cleaning process to simulate the web service server.
1431          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1432          $this->assertCount(1, $sections[0]['modules']);
1433          $this->assertEquals('page', $sections[0]['modules'][0]['modname']);
1434          $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']);
1435          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']);
1436      }
1437  
1438      /**
1439       * Test get course contents completion manual
1440       */
1441      public function test_get_course_contents_completion_manual() {
1442          global $CFG;
1443          $this->resetAfterTest(true);
1444  
1445          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1446              $this->prepare_get_course_contents_test();
1447          availability_completion\condition::wipe_static_cache();
1448  
1449          // Test activity not completed yet.
1450          $result = core_course_external::get_course_contents($course->id, array(
1451              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1452          // We need to execute the return values cleaning process to simulate the web service server.
1453          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1454  
1455          $completiondata = $result[0]['modules'][0]["completiondata"];
1456          $this->assertCount(1, $result[0]['modules']);
1457          $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1458          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1459          $this->assertEquals(0, $completiondata['state']);
1460          $this->assertEquals(0, $completiondata['timecompleted']);
1461          $this->assertEmpty($completiondata['overrideby']);
1462          $this->assertFalse($completiondata['valueused']);
1463          $this->assertTrue($completiondata['hascompletion']);
1464          $this->assertFalse($completiondata['isautomatic']);
1465          $this->assertFalse($completiondata['istrackeduser']);
1466          $this->assertTrue($completiondata['uservisible']);
1467  
1468          // Set activity completed.
1469          core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1470  
1471          $result = core_course_external::get_course_contents($course->id, array(
1472              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1473          // We need to execute the return values cleaning process to simulate the web service server.
1474          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1475  
1476          $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1477          $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1478          $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1479  
1480          // Test activity with completion value that is used in an availability condition.
1481          $result = core_course_external::get_course_contents($course->id, array(
1482                  array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1483          // We need to execute the return values cleaning process to simulate the web service server.
1484          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1485  
1486          $completiondata = $result[0]['modules'][0]["completiondata"];
1487          $this->assertCount(1, $result[0]['modules']);
1488          $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1489          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1490          $this->assertEquals(0, $completiondata['state']);
1491          $this->assertEquals(0, $completiondata['timecompleted']);
1492          $this->assertEmpty($completiondata['overrideby']);
1493          $this->assertTrue($completiondata['valueused']);
1494          $this->assertTrue($completiondata['hascompletion']);
1495          $this->assertFalse($completiondata['isautomatic']);
1496          $this->assertFalse($completiondata['istrackeduser']);
1497          $this->assertTrue($completiondata['uservisible']);
1498  
1499          // Disable completion.
1500          $CFG->enablecompletion = 0;
1501          $result = core_course_external::get_course_contents($course->id, array(
1502              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1503          // We need to execute the return values cleaning process to simulate the web service server.
1504          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1505  
1506          $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1507      }
1508  
1509      /**
1510       * Test get course contents completion auto
1511       */
1512      public function test_get_course_contents_completion_auto() {
1513          global $CFG;
1514          $this->resetAfterTest(true);
1515  
1516          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1517              $this->prepare_get_course_contents_test();
1518          availability_completion\condition::wipe_static_cache();
1519  
1520          // Test activity not completed yet.
1521          $result = core_course_external::get_course_contents($course->id, [
1522              [
1523                  "name" => "modname",
1524                  "value" => "forum"
1525              ],
1526              [
1527                  "name" => "modid",
1528                  "value" => $forumcompleteautocm->instance
1529              ]
1530          ]);
1531          // We need to execute the return values cleaning process to simulate the web service server.
1532          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1533  
1534          $forummod = $result[0]['modules'][0];
1535          $completiondata = $forummod["completiondata"];
1536          $this->assertCount(1, $result[0]['modules']);
1537          $this->assertEquals("forum", $forummod["modname"]);
1538          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
1539          $this->assertEquals(0, $completiondata['state']);
1540          $this->assertEquals(0, $completiondata['timecompleted']);
1541          $this->assertEmpty($completiondata['overrideby']);
1542          $this->assertFalse($completiondata['valueused']);
1543          $this->assertTrue($completiondata['hascompletion']);
1544          $this->assertTrue($completiondata['isautomatic']);
1545          $this->assertFalse($completiondata['istrackeduser']);
1546          $this->assertTrue($completiondata['uservisible']);
1547          $this->assertCount(1, $completiondata['details']);
1548      }
1549  
1550      /**
1551       * Test mimetype is returned for resources with showtype set.
1552       */
1553      public function test_get_course_contents_including_mimetype() {
1554          $this->resetAfterTest(true);
1555  
1556          $this->setAdminUser();
1557          $course = self::getDataGenerator()->create_course();
1558  
1559          $record = new stdClass();
1560          $record->course = $course->id;
1561          $record->showtype = 1;
1562          $resource = self::getDataGenerator()->create_module('resource', $record);
1563  
1564          $result = core_course_external::get_course_contents($course->id);
1565          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1566          $this->assertCount(1, $result[0]['modules']);   // One module, first section.
1567          $customdata = json_decode($result[0]['modules'][0]['customdata']);
1568          $displayoptions = unserialize($customdata->displayoptions);
1569          $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
1570      }
1571  
1572      /**
1573       * Test contents info is returned.
1574       */
1575      public function test_get_course_contents_contentsinfo() {
1576          global $USER;
1577  
1578          $this->resetAfterTest(true);
1579          $this->setAdminUser();
1580          $timenow = time();
1581  
1582          $course = self::getDataGenerator()->create_course();
1583  
1584          $record = new stdClass();
1585          $record->course = $course->id;
1586          // One resource with one file.
1587          $resource1 = self::getDataGenerator()->create_module('resource', $record);
1588  
1589          // More type of files.
1590          $record->files = file_get_unused_draft_itemid();
1591          $usercontext = context_user::instance($USER->id);
1592          $extensions = array('txt', 'png', 'pdf');
1593          $fs = get_file_storage();
1594          foreach ($extensions as $key => $extension) {
1595              // Add actual file there.
1596              $filerecord = array('component' => 'user', 'filearea' => 'draft',
1597                      'contextid' => $usercontext->id, 'itemid' => $record->files,
1598                      'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1599              $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1600          }
1601  
1602          // Create file reference.
1603          $repos = repository::get_instances(array('type' => 'user'));
1604          $userrepository = reset($repos);
1605  
1606          // Create a user private file.
1607          $userfilerecord = new stdClass;
1608          $userfilerecord->contextid = $usercontext->id;
1609          $userfilerecord->component = 'user';
1610          $userfilerecord->filearea  = 'private';
1611          $userfilerecord->itemid    = 0;
1612          $userfilerecord->filepath  = '/';
1613          $userfilerecord->filename  = 'userfile.txt';
1614          $userfilerecord->source    = 'test';
1615          $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1616          $userfileref = $fs->pack_reference($userfilerecord);
1617  
1618          // Clone latest "normal" file.
1619          $filerefrecord = clone (object) $filerecord;
1620          $filerefrecord->filename = 'testref.txt';
1621          $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1622          // Set main file pointing to the file reference.
1623          file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1624              $filerefrecord->filename, 1);
1625  
1626          // Once the reference has been created, create the file resource.
1627          $resource2 = self::getDataGenerator()->create_module('resource', $record);
1628  
1629          $result = core_course_external::get_course_contents($course->id);
1630          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1631          $this->assertCount(2, $result[0]['modules']);
1632          foreach ($result[0]['modules'] as $module) {
1633              if ($module['instance'] == $resource1->id) {
1634                  $this->assertEquals(1, $module['contentsinfo']['filescount']);
1635                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1636                  $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1637                  $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1638              } else {
1639                  $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1640                  $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1641                      $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1642                  $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1643                  $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1644                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1645                  $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1646              }
1647          }
1648      }
1649  
1650      /**
1651       * Test get_course_contents when hidden sections are displayed.
1652       */
1653      public function test_get_course_contents_hiddensections() {
1654          global $DB;
1655          $this->resetAfterTest(true);
1656  
1657          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1658          // Force returning hidden sections.
1659          $course->hiddensections = 0;
1660          update_course($course);
1661  
1662          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1663          $user = self::getDataGenerator()->create_user();
1664          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1665          $this->setUser($user);
1666  
1667          $sections = core_course_external::get_course_contents($course->id, array());
1668          // We need to execute the return values cleaning process to simulate the web service server.
1669          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1670  
1671          $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1672          $this->assertCount(6, $sections[0]['modules']);
1673          $this->assertCount(1, $sections[1]['modules']);
1674          $this->assertCount(1, $sections[2]['modules']);
1675          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1676          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1677  
1678          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1679          $this->assertEquals(1, $sections[1]['section']);
1680          $this->assertEquals(2, $sections[2]['section']);
1681          $this->assertEquals(3, $sections[3]['section']);
1682          // The module with the availability restriction met is returning contents.
1683          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1684          // The module with the availability restriction not met is not returning contents.
1685          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1686  
1687          // Now include flag for returning stealth information (fake section).
1688          $sections = core_course_external::get_course_contents($course->id,
1689              array(array("name" => "includestealthmodules", "value" => 1)));
1690          // We need to execute the return values cleaning process to simulate the web service server.
1691          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1692  
1693          $this->assertCount(6, $sections); // Include fake section with stealth activities.
1694          $this->assertCount(6, $sections[0]['modules']);
1695          $this->assertCount(1, $sections[1]['modules']);
1696          $this->assertCount(1, $sections[2]['modules']);
1697          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1698          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1699          $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1700          $this->assertEquals(-1, $sections[5]['id']);
1701      }
1702  
1703      /**
1704       * Test get course contents dates.
1705       */
1706      public function test_get_course_contents_dates() {
1707          $this->resetAfterTest(true);
1708  
1709          $this->setAdminUser();
1710          set_config('enablecourserelativedates', 1);
1711  
1712          // Course with just main section.
1713          $timenow = time();
1714          $course = self::getDataGenerator()->create_course(
1715              ['numsections' => 0, 'relativedatesmode' => true, 'startdate' => $timenow - DAYSECS]);
1716  
1717          $teacher = self::getDataGenerator()->create_user();
1718          self::getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1719  
1720          $this->setUser($teacher);
1721  
1722          // Create resource (empty dates).
1723          $resource = self::getDataGenerator()->create_module('resource', ['course' => $course->id]);
1724          // Create activities with dates.
1725          $resource = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'duedate' => $timenow]);
1726          $resource = self::getDataGenerator()->create_module('choice',
1727              ['course' => $course->id, 'timeopen' => $timenow, 'timeclose' => $timenow + DAYSECS]);
1728          $resource = self::getDataGenerator()->create_module('assign',
1729              ['course' => $course->id, 'allowsubmissionsfromdate' => $timenow]);
1730  
1731          $result = core_course_external::get_course_contents($course->id);
1732          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1733  
1734          foreach ($result[0]['modules'] as $module) {
1735              if ($module['modname'] == 'resource') {
1736                  $this->assertEmpty($module['dates']);
1737              } else if ($module['modname'] == 'forum') {
1738                  $this->assertCount(1, $module['dates']);
1739                  $this->assertEquals('duedate', $module['dates'][0]['dataid']);
1740                  $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1741              } else if ($module['modname'] == 'choice') {
1742                  $this->assertCount(2, $module['dates']);
1743                  $this->assertEquals('timeopen', $module['dates'][0]['dataid']);
1744                  $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1745                  $this->assertEquals('timeclose', $module['dates'][1]['dataid']);
1746                  $this->assertEquals($timenow + DAYSECS, $module['dates'][1]['timestamp']);
1747              } else if ($module['modname'] == 'assign') {
1748                  $this->assertCount(1, $module['dates']);
1749                  $this->assertEquals('allowsubmissionsfromdate', $module['dates'][0]['dataid']);
1750                  $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1751                  $this->assertEquals($course->startdate, $module['dates'][0]['relativeto']);
1752              }
1753          }
1754      }
1755  
1756      /**
1757       * Test get_course_contents for courses with invalid course format.
1758       */
1759      public function test_get_course_contents_invalid_format() {
1760          global $DB;
1761          $this->resetAfterTest();
1762  
1763          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1764  
1765          $DB->set_field('course', 'format', 'fakeformat', ['id' => $course->id]);
1766  
1767          // WS should falback to default course format (topics) and avoid exceptions (but debugging will happen).
1768          $result = core_course_external::get_course_contents($course->id);
1769          $this->assertDebuggingCalled();
1770          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1771      }
1772  
1773      /**
1774       * Test duplicate_course
1775       */
1776      public function test_duplicate_course() {
1777          $this->resetAfterTest(true);
1778  
1779          // Create one course with three modules.
1780          $course  = self::getDataGenerator()->create_course();
1781          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1782          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1783          $forumcontext = context_module::instance($forum->cmid);
1784          $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1785          $datacontext = context_module::instance($data->cmid);
1786          $datacm = get_coursemodule_from_instance('page', $data->id);
1787          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1788          $pagecontext = context_module::instance($page->cmid);
1789          $pagecm = get_coursemodule_from_instance('page', $page->id);
1790  
1791          // Set the required capabilities by the external function.
1792          $coursecontext = context_course::instance($course->id);
1793          $categorycontext = context_coursecat::instance($course->category);
1794          $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1795          $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1796          $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1797          $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1798          $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1799          // Optional capabilities to copy user data.
1800          $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1801          $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1802  
1803          $newcourse['fullname'] = 'Course duplicate';
1804          $newcourse['shortname'] = 'courseduplicate';
1805          $newcourse['categoryid'] = $course->category;
1806          $newcourse['visible'] = true;
1807          $newcourse['options'][] = array('name' => 'users', 'value' => true);
1808  
1809          $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1810                  $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1811  
1812          // We need to execute the return values cleaning process to simulate the web service server.
1813          $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1814  
1815          // Check that the course has been duplicated.
1816          $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1817      }
1818  
1819      /**
1820       * Test update_courses
1821       */
1822      public function test_update_courses() {
1823          global $DB, $CFG, $USER, $COURSE;
1824  
1825          // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1826          // trick because we are both updating and getting (for testing) course information
1827          // in the same request and core_course_external::update_courses()
1828          // is overwriting $COURSE all over the time with OLD values, so later
1829          // use of get_course() fetches those OLD values instead of the updated ones.
1830          // See MDL-39723 for more info.
1831          $origcourse = clone($COURSE);
1832  
1833          $this->resetAfterTest(true);
1834  
1835          // Set the required capabilities by the external function.
1836          $contextid = context_system::instance()->id;
1837          $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1838          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1839          $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1840          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1841          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1842          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1843          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1844          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1845          $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1846          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1847  
1848          // Create category and courses.
1849          $category1  = self::getDataGenerator()->create_category();
1850          $category2  = self::getDataGenerator()->create_category();
1851  
1852          $originalcourse1 = self::getDataGenerator()->create_course();
1853          self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1854  
1855          $originalcourse2 = self::getDataGenerator()->create_course();
1856          self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1857  
1858          // Course with custom fields.
1859          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
1860          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
1861              'categoryid' => $fieldcategory->get('id'),
1862              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]];
1863          $field = self::getDataGenerator()->create_custom_field($customfield);
1864  
1865          $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']);
1866          self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
1867  
1868          // Course values to be updated.
1869          $course1['id'] = $originalcourse1->id;
1870          $course1['fullname'] = 'Updated test course 1';
1871          $course1['shortname'] = 'Udestedtestcourse1';
1872          $course1['categoryid'] = $category1->id;
1873  
1874          $course2['id'] = $originalcourse2->id;
1875          $course2['fullname'] = 'Updated test course 2';
1876          $course2['shortname'] = 'Updestedtestcourse2';
1877          $course2['categoryid'] = $category2->id;
1878          $course2['idnumber'] = 'Updatedidnumber2';
1879          $course2['summary'] = 'Updaated description for course 2';
1880          $course2['summaryformat'] = FORMAT_HTML;
1881          $course2['format'] = 'topics';
1882          $course2['showgrades'] = 1;
1883          $course2['newsitems'] = 3;
1884          $course2['startdate'] = 1420092000; // 01/01/2015.
1885          $course2['enddate'] = 1422669600; // 01/31/2015.
1886          $course2['maxbytes'] = 100000;
1887          $course2['showreports'] = 1;
1888          $course2['visible'] = 0;
1889          $course2['hiddensections'] = 0;
1890          $course2['groupmode'] = 0;
1891          $course2['groupmodeforce'] = 0;
1892          $course2['defaultgroupingid'] = 0;
1893          $course2['enablecompletion'] = 1;
1894          $course2['lang'] = 'en';
1895          $course2['forcetheme'] = 'classic';
1896  
1897          $course3['id'] = $originalcourse3->id;
1898          $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value'];
1899          $course3['customfields'] = [$updatedcustomfieldvalue];
1900          $courses = array($course1, $course2, $course3);
1901  
1902          $updatedcoursewarnings = core_course_external::update_courses($courses);
1903          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1904                  $updatedcoursewarnings);
1905          $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1906  
1907          // Check that right number of courses were created.
1908          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1909  
1910          // Check that the courses were correctly created.
1911          foreach ($courses as $course) {
1912              $courseinfo = course_get_format($course['id'])->get_course();
1913              $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
1914              if ($course['id'] == $course2['id']) {
1915                  $this->assertEquals($course2['fullname'], $courseinfo->fullname);
1916                  $this->assertEquals($course2['shortname'], $courseinfo->shortname);
1917                  $this->assertEquals($course2['categoryid'], $courseinfo->category);
1918                  $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
1919                  $this->assertEquals($course2['summary'], $courseinfo->summary);
1920                  $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
1921                  $this->assertEquals($course2['format'], $courseinfo->format);
1922                  $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
1923                  $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
1924                  $this->assertEquals($course2['startdate'], $courseinfo->startdate);
1925                  $this->assertEquals($course2['enddate'], $courseinfo->enddate);
1926                  $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
1927                  $this->assertEquals($course2['showreports'], $courseinfo->showreports);
1928                  $this->assertEquals($course2['visible'], $courseinfo->visible);
1929                  $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
1930                  $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
1931                  $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
1932                  $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
1933                  $this->assertEquals($course2['lang'], $courseinfo->lang);
1934  
1935                  if (!empty($CFG->allowcoursethemes)) {
1936                      $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
1937                  }
1938  
1939                  $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
1940                  $this->assertEquals(['test' => null], (array)$customfields);
1941              } else if ($course['id'] == $course1['id']) {
1942                  $this->assertEquals($course1['fullname'], $courseinfo->fullname);
1943                  $this->assertEquals($course1['shortname'], $courseinfo->shortname);
1944                  $this->assertEquals($course1['categoryid'], $courseinfo->category);
1945                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1946                  $this->assertEquals('topics', $courseinfo->format);
1947                  $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
1948                  $this->assertEquals(0, $courseinfo->newsitems);
1949                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1950                  $this->assertEquals(['test' => null], (array)$customfields);
1951              } else if ($course['id'] == $course3['id']) {
1952                  $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
1953              } else {
1954                  throw new moodle_exception('Unexpected shortname');
1955              }
1956          }
1957  
1958          $courses = array($course1);
1959          // Try update course without update capability.
1960          $user = self::getDataGenerator()->create_user();
1961          $this->setUser($user);
1962          $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
1963          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1964          $updatedcoursewarnings = core_course_external::update_courses($courses);
1965          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1966                                                                      $updatedcoursewarnings);
1967          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1968  
1969          // Try update course category without capability.
1970          $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
1971          $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1972          $user = self::getDataGenerator()->create_user();
1973          $this->setUser($user);
1974          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1975          $course1['categoryid'] = $category2->id;
1976          $courses = array($course1);
1977          $updatedcoursewarnings = core_course_external::update_courses($courses);
1978          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1979                                                                      $updatedcoursewarnings);
1980          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1981  
1982          // Try update course fullname without capability.
1983          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1984          $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1985          $user = self::getDataGenerator()->create_user();
1986          $this->setUser($user);
1987          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1988          $updatedcoursewarnings = core_course_external::update_courses($courses);
1989          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1990                                                                      $updatedcoursewarnings);
1991          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1992          $course1['fullname'] = 'Testing fullname without permission';
1993          $courses = array($course1);
1994          $updatedcoursewarnings = core_course_external::update_courses($courses);
1995          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1996                                                                      $updatedcoursewarnings);
1997          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1998  
1999          // Try update course shortname without capability.
2000          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2001          $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2002          $user = self::getDataGenerator()->create_user();
2003          $this->setUser($user);
2004          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2005          $updatedcoursewarnings = core_course_external::update_courses($courses);
2006          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2007                                                                      $updatedcoursewarnings);
2008          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2009          $course1['shortname'] = 'Testing shortname without permission';
2010          $courses = array($course1);
2011          $updatedcoursewarnings = core_course_external::update_courses($courses);
2012          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2013                                                                      $updatedcoursewarnings);
2014          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2015  
2016          // Try update course idnumber without capability.
2017          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2018          $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2019          $user = self::getDataGenerator()->create_user();
2020          $this->setUser($user);
2021          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2022          $updatedcoursewarnings = core_course_external::update_courses($courses);
2023          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2024                                                                      $updatedcoursewarnings);
2025          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2026          $course1['idnumber'] = 'NEWIDNUMBER';
2027          $courses = array($course1);
2028          $updatedcoursewarnings = core_course_external::update_courses($courses);
2029          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2030                                                                      $updatedcoursewarnings);
2031          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2032  
2033          // Try update course summary without capability.
2034          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2035          $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2036          $user = self::getDataGenerator()->create_user();
2037          $this->setUser($user);
2038          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2039          $updatedcoursewarnings = core_course_external::update_courses($courses);
2040          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2041                                                                      $updatedcoursewarnings);
2042          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2043          $course1['summary'] = 'New summary';
2044          $courses = array($course1);
2045          $updatedcoursewarnings = core_course_external::update_courses($courses);
2046          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2047                                                                      $updatedcoursewarnings);
2048          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2049  
2050          // Try update course with invalid summary format.
2051          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2052          $user = self::getDataGenerator()->create_user();
2053          $this->setUser($user);
2054          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2055          $updatedcoursewarnings = core_course_external::update_courses($courses);
2056          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2057                                                                      $updatedcoursewarnings);
2058          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2059          $course1['summaryformat'] = 10;
2060          $courses = array($course1);
2061          $updatedcoursewarnings = core_course_external::update_courses($courses);
2062          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2063                                                                      $updatedcoursewarnings);
2064          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2065  
2066          // Try update course visibility without capability.
2067          $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
2068          $user = self::getDataGenerator()->create_user();
2069          $this->setUser($user);
2070          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2071          $course1['summaryformat'] = FORMAT_MOODLE;
2072          $courses = array($course1);
2073          $updatedcoursewarnings = core_course_external::update_courses($courses);
2074          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2075                                                                      $updatedcoursewarnings);
2076          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2077          $course1['visible'] = 0;
2078          $courses = array($course1);
2079          $updatedcoursewarnings = core_course_external::update_courses($courses);
2080          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2081                                                                      $updatedcoursewarnings);
2082          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2083  
2084          // Try update course custom fields without capability.
2085          $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
2086          $user = self::getDataGenerator()->create_user();
2087          $this->setUser($user);
2088          self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
2089  
2090          $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value'];
2091          $course3['customfields'] = [$newupdatedcustomfieldvalue];
2092  
2093          core_course_external::update_courses([$course3]);
2094  
2095          // Custom field was not updated.
2096          $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
2097          $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
2098      }
2099  
2100      /**
2101       * Test delete course_module.
2102       */
2103      public function test_delete_modules() {
2104          global $DB;
2105  
2106          // Ensure we reset the data after this test.
2107          $this->resetAfterTest(true);
2108  
2109          // Create a user.
2110          $user = self::getDataGenerator()->create_user();
2111  
2112          // Set the tests to run as the user.
2113          self::setUser($user);
2114  
2115          // Create a course to add the modules.
2116          $course = self::getDataGenerator()->create_course();
2117  
2118          // Create two test modules.
2119          $record = new stdClass();
2120          $record->course = $course->id;
2121          $module1 = self::getDataGenerator()->create_module('forum', $record);
2122          $module2 = self::getDataGenerator()->create_module('assign', $record);
2123  
2124          // Check the forum was correctly created.
2125          $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
2126  
2127          // Check the assignment was correctly created.
2128          $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
2129  
2130          // Check data exists in the course modules table.
2131          $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2132                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2133  
2134          // Enrol the user in the course.
2135          $enrol = enrol_get_plugin('manual');
2136          $enrolinstances = enrol_get_instances($course->id, true);
2137          foreach ($enrolinstances as $courseenrolinstance) {
2138              if ($courseenrolinstance->enrol == "manual") {
2139                  $instance = $courseenrolinstance;
2140                  break;
2141              }
2142          }
2143          $enrol->enrol_user($instance, $user->id);
2144  
2145          // Assign capabilities to delete module 1.
2146          $modcontext = context_module::instance($module1->cmid);
2147          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
2148  
2149          // Assign capabilities to delete module 2.
2150          $modcontext = context_module::instance($module2->cmid);
2151          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
2152          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
2153  
2154          // Deleting these module instances.
2155          core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2156  
2157          // Check the forum was deleted.
2158          $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
2159  
2160          // Check the assignment was deleted.
2161          $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
2162  
2163          // Check we retrieve no data in the course modules table.
2164          $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2165                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2166  
2167          // Call with non-existent course module id and ensure exception thrown.
2168          try {
2169              core_course_external::delete_modules(array('1337'));
2170              $this->fail('Exception expected due to missing course module.');
2171          } catch (dml_missing_record_exception $e) {
2172              $this->assertEquals('invalidcoursemodule', $e->errorcode);
2173          }
2174  
2175          // Create two modules.
2176          $module1 = self::getDataGenerator()->create_module('forum', $record);
2177          $module2 = self::getDataGenerator()->create_module('assign', $record);
2178  
2179          // Since these modules were recreated the user will not have capabilities
2180          // to delete them, ensure exception is thrown if they try.
2181          try {
2182              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2183              $this->fail('Exception expected due to missing capability.');
2184          } catch (moodle_exception $e) {
2185              $this->assertEquals('nopermissions', $e->errorcode);
2186          }
2187  
2188          // Unenrol user from the course.
2189          $enrol->unenrol_user($instance, $user->id);
2190  
2191          // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
2192          try {
2193              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2194              $this->fail('Exception expected due to being unenrolled from the course.');
2195          } catch (moodle_exception $e) {
2196              $this->assertEquals('requireloginerror', $e->errorcode);
2197          }
2198      }
2199  
2200      /**
2201       * Test import_course into an empty course
2202       */
2203      public function test_import_course_empty() {
2204          global $USER;
2205  
2206          $this->resetAfterTest(true);
2207  
2208          $course1  = self::getDataGenerator()->create_course();
2209          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2210          $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2211  
2212          $course2  = self::getDataGenerator()->create_course();
2213  
2214          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2215          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2216  
2217          // Verify the state of the courses before we do the import.
2218          $this->assertCount(2, $course1cms);
2219          $this->assertEmpty($course2cms);
2220  
2221          // Setup the user to run the operation (ugly hack because validate_context() will
2222          // fail as the email is not set by $this->setAdminUser()).
2223          $this->setAdminUser();
2224          $USER->email = 'emailtopass@example.com';
2225  
2226          // Import from course1 to course2.
2227          core_course_external::import_course($course1->id, $course2->id, 0);
2228  
2229          // Verify that now we have two modules in both courses.
2230          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2231          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2232          $this->assertCount(2, $course1cms);
2233          $this->assertCount(2, $course2cms);
2234  
2235          // Verify that the names transfered across correctly.
2236          foreach ($course2cms as $cm) {
2237              if ($cm->modname === 'page') {
2238                  $this->assertEquals($cm->name, $page->name);
2239              } else if ($cm->modname === 'forum') {
2240                  $this->assertEquals($cm->name, $forum->name);
2241              } else {
2242                  $this->fail('Unknown CM found.');
2243              }
2244          }
2245      }
2246  
2247      /**
2248       * Test import_course into an filled course
2249       */
2250      public function test_import_course_filled() {
2251          global $USER;
2252  
2253          $this->resetAfterTest(true);
2254  
2255          // Add forum and page to course1.
2256          $course1  = self::getDataGenerator()->create_course();
2257          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2258          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2259  
2260          // Add quiz to course 2.
2261          $course2  = self::getDataGenerator()->create_course();
2262          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2263  
2264          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2265          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2266  
2267          // Verify the state of the courses before we do the import.
2268          $this->assertCount(2, $course1cms);
2269          $this->assertCount(1, $course2cms);
2270  
2271          // Setup the user to run the operation (ugly hack because validate_context() will
2272          // fail as the email is not set by $this->setAdminUser()).
2273          $this->setAdminUser();
2274          $USER->email = 'emailtopass@example.com';
2275  
2276          // Import from course1 to course2 without deleting content.
2277          core_course_external::import_course($course1->id, $course2->id, 0);
2278  
2279          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2280  
2281          // Verify that now we have three modules in course2.
2282          $this->assertCount(3, $course2cms);
2283  
2284          // Verify that the names transfered across correctly.
2285          foreach ($course2cms as $cm) {
2286              if ($cm->modname === 'page') {
2287                  $this->assertEquals($cm->name, $page->name);
2288              } else if ($cm->modname === 'forum') {
2289                  $this->assertEquals($cm->name, $forum->name);
2290              } else if ($cm->modname === 'quiz') {
2291                  $this->assertEquals($cm->name, $quiz->name);
2292              } else {
2293                  $this->fail('Unknown CM found.');
2294              }
2295          }
2296      }
2297  
2298      /**
2299       * Test import_course with only blocks set to backup
2300       */
2301      public function test_import_course_blocksonly() {
2302          global $USER, $DB;
2303  
2304          $this->resetAfterTest(true);
2305  
2306          // Add forum and page to course1.
2307          $course1  = self::getDataGenerator()->create_course();
2308          $course1ctx = context_course::instance($course1->id);
2309          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2310          $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2311  
2312          $course2  = self::getDataGenerator()->create_course();
2313          $course2ctx = context_course::instance($course2->id);
2314          $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2315          $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2316  
2317          // Setup the user to run the operation (ugly hack because validate_context() will
2318          // fail as the email is not set by $this->setAdminUser()).
2319          $this->setAdminUser();
2320          $USER->email = 'emailtopass@example.com';
2321  
2322          // Import from course1 to course2 without deleting content, but excluding
2323          // activities.
2324          $options = array(
2325              array('name' => 'activities', 'value' => 0),
2326              array('name' => 'blocks', 'value' => 1),
2327              array('name' => 'filters', 'value' => 0),
2328          );
2329  
2330          core_course_external::import_course($course1->id, $course2->id, 0, $options);
2331  
2332          $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2333          $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2334          // Check that course modules haven't changed, but that blocks have.
2335          $this->assertEquals($initialcmcount, $newcmcount);
2336          $this->assertEquals(($initialblockcount + 1), $newblockcount);
2337      }
2338  
2339      /**
2340       * Test import_course into an filled course, deleting content.
2341       */
2342      public function test_import_course_deletecontent() {
2343          global $USER;
2344          $this->resetAfterTest(true);
2345  
2346          // Add forum and page to course1.
2347          $course1  = self::getDataGenerator()->create_course();
2348          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2349          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2350  
2351          // Add quiz to course 2.
2352          $course2  = self::getDataGenerator()->create_course();
2353          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2354  
2355          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2356          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2357  
2358          // Verify the state of the courses before we do the import.
2359          $this->assertCount(2, $course1cms);
2360          $this->assertCount(1, $course2cms);
2361  
2362          // Setup the user to run the operation (ugly hack because validate_context() will
2363          // fail as the email is not set by $this->setAdminUser()).
2364          $this->setAdminUser();
2365          $USER->email = 'emailtopass@example.com';
2366  
2367          // Import from course1 to course2,  deleting content.
2368          core_course_external::import_course($course1->id, $course2->id, 1);
2369  
2370          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2371  
2372          // Verify that now we have two modules in course2.
2373          $this->assertCount(2, $course2cms);
2374  
2375          // Verify that the course only contains the imported modules.
2376          foreach ($course2cms as $cm) {
2377              if ($cm->modname === 'page') {
2378                  $this->assertEquals($cm->name, $page->name);
2379              } else if ($cm->modname === 'forum') {
2380                  $this->assertEquals($cm->name, $forum->name);
2381              } else {
2382                  $this->fail('Unknown CM found: '.$cm->name);
2383              }
2384          }
2385      }
2386  
2387      /**
2388       * Ensure import_course handles incorrect deletecontent option correctly.
2389       */
2390      public function test_import_course_invalid_deletecontent_option() {
2391          $this->resetAfterTest(true);
2392  
2393          $course1  = self::getDataGenerator()->create_course();
2394          $course2  = self::getDataGenerator()->create_course();
2395  
2396          $this->expectException('moodle_exception');
2397          $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2398          // Import from course1 to course2, with invalid option
2399          core_course_external::import_course($course1->id, $course2->id, -1);;
2400      }
2401  
2402      /**
2403       * Test view_course function
2404       */
2405      public function test_view_course() {
2406  
2407          $this->resetAfterTest();
2408  
2409          // Course without sections.
2410          $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2411          $this->setAdminUser();
2412  
2413          // Redirect events to the sink, so we can recover them later.
2414          $sink = $this->redirectEvents();
2415  
2416          $result = core_course_external::view_course($course->id, 1);
2417          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2418          $events = $sink->get_events();
2419          $event = reset($events);
2420  
2421          // Check the event details are correct.
2422          $this->assertInstanceOf('\core\event\course_viewed', $event);
2423          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2424          $this->assertEquals(1, $event->other['coursesectionnumber']);
2425  
2426          $result = core_course_external::view_course($course->id);
2427          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2428          $events = $sink->get_events();
2429          $event = array_pop($events);
2430          $sink->close();
2431  
2432          // Check the event details are correct.
2433          $this->assertInstanceOf('\core\event\course_viewed', $event);
2434          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2435          $this->assertEmpty($event->other);
2436  
2437      }
2438  
2439      /**
2440       * Test get_course_module
2441       */
2442      public function test_get_course_module() {
2443          global $DB;
2444  
2445          $this->resetAfterTest(true);
2446  
2447          $this->setAdminUser();
2448          $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]);
2449          $record = array(
2450              'course' => $course->id,
2451              'name' => 'First Assignment'
2452          );
2453          $options = array(
2454              'idnumber' => 'ABC',
2455              'visible' => 0,
2456              'completion' => COMPLETION_TRACKING_AUTOMATIC,
2457              'completiongradeitemnumber' => 0,
2458              'completionpassgrade' => 1,
2459          );
2460          // Hidden activity.
2461          $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2462  
2463          $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2464  
2465          // Insert a custom grade scale to be used by an outcome.
2466          $gradescale = new grade_scale();
2467          $gradescale->name        = 'gettcoursemodulescale';
2468          $gradescale->courseid    = $course->id;
2469          $gradescale->userid      = 0;
2470          $gradescale->scale       = $outcomescale;
2471          $gradescale->description = 'This scale is used to mark standard assignments.';
2472          $gradescale->insert();
2473  
2474          // Insert an outcome.
2475          $data = new stdClass();
2476          $data->courseid = $course->id;
2477          $data->fullname = 'Team work';
2478          $data->shortname = 'Team work';
2479          $data->scaleid = $gradescale->id;
2480          $outcome = new grade_outcome($data, false);
2481          $outcome->insert();
2482  
2483          $outcomegradeitem = new grade_item();
2484          $outcomegradeitem->itemname = $outcome->shortname;
2485          $outcomegradeitem->itemtype = 'mod';
2486          $outcomegradeitem->itemmodule = 'assign';
2487          $outcomegradeitem->iteminstance = $assign->id;
2488          $outcomegradeitem->outcomeid = $outcome->id;
2489          $outcomegradeitem->cmid = 0;
2490          $outcomegradeitem->courseid = $course->id;
2491          $outcomegradeitem->aggregationcoef = 0;
2492          $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2493          $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2494          $outcomegradeitem->scaleid = $outcome->scaleid;
2495          $outcomegradeitem->insert();
2496  
2497          $assignmentgradeitem = grade_item::fetch(
2498              array(
2499                  'itemtype' => 'mod',
2500                  'itemmodule' => 'assign',
2501                  'iteminstance' => $assign->id,
2502                  'itemnumber' => 0,
2503                  'courseid' => $course->id
2504              )
2505          );
2506          $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2507          $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2508  
2509          // Test admin user can see the complete hidden activity.
2510          $result = core_course_external::get_course_module($assign->cmid);
2511          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2512  
2513          $this->assertCount(0, $result['warnings']);
2514          // Test we retrieve all the fields.
2515          $this->assertCount(30, $result['cm']);
2516          $this->assertEquals($record['name'], $result['cm']['name']);
2517          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2518          $this->assertEquals(100, $result['cm']['grade']);
2519          $this->assertEquals(0.0, $result['cm']['gradepass']);
2520          $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2521          $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2522          $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2523          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2524  
2525          $student = $this->getDataGenerator()->create_user();
2526          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2527  
2528          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2529          $this->setUser($student);
2530  
2531          // The user shouldn't be able to see the activity.
2532          try {
2533              core_course_external::get_course_module($assign->cmid);
2534              $this->fail('Exception expected due to invalid permissions.');
2535          } catch (moodle_exception $e) {
2536              $this->assertEquals('requireloginerror', $e->errorcode);
2537          }
2538  
2539          // Make module visible.
2540          set_coursemodule_visible($assign->cmid, 1);
2541  
2542          // Test student user.
2543          $result = core_course_external::get_course_module($assign->cmid);
2544          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2545  
2546          $this->assertCount(0, $result['warnings']);
2547          // Test we retrieve only the few files we can see.
2548          $this->assertCount(12, $result['cm']);
2549          $this->assertEquals($assign->cmid, $result['cm']['id']);
2550          $this->assertEquals($course->id, $result['cm']['course']);
2551          $this->assertEquals('assign', $result['cm']['modname']);
2552          $this->assertEquals($assign->id, $result['cm']['instance']);
2553  
2554      }
2555  
2556      /**
2557       * Test get_course_module_by_instance
2558       */
2559      public function test_get_course_module_by_instance() {
2560          global $DB;
2561  
2562          $this->resetAfterTest(true);
2563  
2564          $this->setAdminUser();
2565          $course = self::getDataGenerator()->create_course();
2566          $record = array(
2567              'course' => $course->id,
2568              'name' => 'First quiz',
2569              'grade' => 90.00
2570          );
2571          $options = array(
2572              'idnumber' => 'ABC',
2573              'visible' => 0
2574          );
2575          // Hidden activity.
2576          $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2577  
2578          // Test admin user can see the complete hidden activity.
2579          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2580          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2581  
2582          $this->assertCount(0, $result['warnings']);
2583          // Test we retrieve all the fields.
2584          $this->assertCount(28, $result['cm']);
2585          $this->assertEquals($record['name'], $result['cm']['name']);
2586          $this->assertEquals($record['grade'], $result['cm']['grade']);
2587          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2588          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2589  
2590          $student = $this->getDataGenerator()->create_user();
2591          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2592  
2593          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2594          $this->setUser($student);
2595  
2596          // The user shouldn't be able to see the activity.
2597          try {
2598              core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2599              $this->fail('Exception expected due to invalid permissions.');
2600          } catch (moodle_exception $e) {
2601              $this->assertEquals('requireloginerror', $e->errorcode);
2602          }
2603  
2604          // Make module visible.
2605          set_coursemodule_visible($quiz->cmid, 1);
2606  
2607          // Test student user.
2608          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2609          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2610  
2611          $this->assertCount(0, $result['warnings']);
2612          // Test we retrieve only the few files we can see.
2613          $this->assertCount(12, $result['cm']);
2614          $this->assertEquals($quiz->cmid, $result['cm']['id']);
2615          $this->assertEquals($course->id, $result['cm']['course']);
2616          $this->assertEquals('quiz', $result['cm']['modname']);
2617          $this->assertEquals($quiz->id, $result['cm']['instance']);
2618  
2619          // Try with an invalid module name.
2620          try {
2621              core_course_external::get_course_module_by_instance('abc', $quiz->id);
2622              $this->fail('Exception expected due to invalid module name.');
2623          } catch (dml_read_exception $e) {
2624              $this->assertEquals('dmlreadexception', $e->errorcode);
2625          }
2626  
2627      }
2628  
2629      /**
2630       * Test get_user_navigation_options
2631       */
2632      public function test_get_user_navigation_options() {
2633          global $USER;
2634  
2635          $this->resetAfterTest();
2636          $course1 = self::getDataGenerator()->create_course();
2637          $course2 = self::getDataGenerator()->create_course();
2638  
2639          // Create a viewer user.
2640          $viewer = self::getDataGenerator()->create_user();
2641          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2642          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2643  
2644          $this->setUser($viewer->id);
2645          $courses = array($course1->id , $course2->id, SITEID);
2646  
2647          $result = core_course_external::get_user_navigation_options($courses);
2648          $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2649  
2650          $this->assertCount(0, $result['warnings']);
2651          $this->assertCount(3, $result['courses']);
2652  
2653          foreach ($result['courses'] as $course) {
2654              $navoptions = new stdClass;
2655              foreach ($course['options'] as $option) {
2656                  $navoptions->{$option['name']} = $option['available'];
2657              }
2658              $this->assertCount(8, $course['options']);
2659              if ($course['id'] == SITEID) {
2660                  $this->assertTrue($navoptions->blogs);
2661                  $this->assertFalse($navoptions->notes);
2662                  $this->assertFalse($navoptions->participants);
2663                  $this->assertTrue($navoptions->badges);
2664                  $this->assertTrue($navoptions->tags);
2665                  $this->assertFalse($navoptions->grades);
2666                  $this->assertFalse($navoptions->search);
2667                  $this->assertTrue($navoptions->competencies);
2668              } else {
2669                  $this->assertTrue($navoptions->blogs);
2670                  $this->assertFalse($navoptions->notes);
2671                  $this->assertTrue($navoptions->participants);
2672                  $this->assertFalse($navoptions->badges);
2673                  $this->assertFalse($navoptions->tags);
2674                  $this->assertTrue($navoptions->grades);
2675                  $this->assertFalse($navoptions->search);
2676                  $this->assertTrue($navoptions->competencies);
2677              }
2678          }
2679      }
2680  
2681      /**
2682       * Test get_user_administration_options
2683       */
2684      public function test_get_user_administration_options() {
2685          global $USER;
2686  
2687          $this->resetAfterTest();
2688          $course1 = self::getDataGenerator()->create_course();
2689          $course2 = self::getDataGenerator()->create_course();
2690  
2691          // Create a viewer user.
2692          $viewer = self::getDataGenerator()->create_user();
2693          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2694          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2695  
2696          $this->setUser($viewer->id);
2697          $courses = array($course1->id , $course2->id, SITEID);
2698  
2699          $result = core_course_external::get_user_administration_options($courses);
2700          $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2701  
2702          $this->assertCount(0, $result['warnings']);
2703          $this->assertCount(3, $result['courses']);
2704  
2705          foreach ($result['courses'] as $course) {
2706              $adminoptions = new stdClass;
2707              foreach ($course['options'] as $option) {
2708                  $adminoptions->{$option['name']} = $option['available'];
2709              }
2710              if ($course['id'] == SITEID) {
2711                  $this->assertCount(17, $course['options']);
2712                  $this->assertFalse($adminoptions->update);
2713                  $this->assertFalse($adminoptions->filters);
2714                  $this->assertFalse($adminoptions->reports);
2715                  $this->assertFalse($adminoptions->backup);
2716                  $this->assertFalse($adminoptions->restore);
2717                  $this->assertFalse($adminoptions->files);
2718                  $this->assertFalse(!isset($adminoptions->tags));
2719                  $this->assertFalse($adminoptions->gradebook);
2720                  $this->assertFalse($adminoptions->outcomes);
2721                  $this->assertFalse($adminoptions->badges);
2722                  $this->assertFalse($adminoptions->import);
2723                  $this->assertFalse($adminoptions->reset);
2724                  $this->assertFalse($adminoptions->roles);
2725                  $this->assertFalse($adminoptions->editcompletion);
2726                  $this->assertFalse($adminoptions->copy);
2727              } else {
2728                  $this->assertCount(15, $course['options']);
2729                  $this->assertFalse($adminoptions->update);
2730                  $this->assertFalse($adminoptions->filters);
2731                  $this->assertFalse($adminoptions->reports);
2732                  $this->assertFalse($adminoptions->backup);
2733                  $this->assertFalse($adminoptions->restore);
2734                  $this->assertFalse($adminoptions->files);
2735                  $this->assertFalse($adminoptions->tags);
2736                  $this->assertFalse($adminoptions->gradebook);
2737                  $this->assertFalse($adminoptions->outcomes);
2738                  $this->assertTrue($adminoptions->badges);
2739                  $this->assertFalse($adminoptions->import);
2740                  $this->assertFalse($adminoptions->reset);
2741                  $this->assertFalse($adminoptions->roles);
2742                  $this->assertFalse($adminoptions->editcompletion);
2743                  $this->assertFalse($adminoptions->copy);
2744              }
2745          }
2746      }
2747  
2748      /**
2749       * Test get_courses_by_fields
2750       */
2751      public function test_get_courses_by_field() {
2752          global $DB, $USER;
2753          $this->resetAfterTest(true);
2754  
2755          $this->setAdminUser();
2756  
2757          $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2758          $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2759          $course1 = self::getDataGenerator()->create_course(
2760              array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2761  
2762          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2763          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2764              'categoryid' => $fieldcategory->get('id')];
2765          $field = self::getDataGenerator()->create_custom_field($customfield);
2766          $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2767          // Create course image.
2768          $draftid = file_get_unused_draft_itemid();
2769          $filerecord = [
2770              'component' => 'user',
2771              'filearea' => 'draft',
2772              'contextid' => context_user::instance($USER->id)->id,
2773              'itemid' => $draftid,
2774              'filename' => 'image.jpg',
2775              'filepath' => '/',
2776          ];
2777          $fs = get_file_storage();
2778          $fs->create_file_from_pathname($filerecord, __DIR__ . '/fixtures/image.jpg');
2779          $course2 = self::getDataGenerator()->create_course([
2780              'visible' => 0,
2781              'category' => $category2->id,
2782              'idnumber' => 'i2',
2783              'customfields' => [$customfieldvalue],
2784              'overviewfiles_filemanager' => $draftid
2785          ]);
2786  
2787          $student1 = self::getDataGenerator()->create_user();
2788          $user1 = self::getDataGenerator()->create_user();
2789          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2790          self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2791          self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2792  
2793          self::setAdminUser();
2794          // As admins, we should be able to retrieve everything.
2795          $result = core_course_external::get_courses_by_field();
2796          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2797          $this->assertCount(3, $result['courses']);
2798          // Expect to receive all the fields.
2799          $this->assertCount(41, $result['courses'][0]);
2800          $this->assertCount(42, $result['courses'][1]);  // One more field because is not the site course.
2801          $this->assertCount(42, $result['courses'][2]);  // One more field because is not the site course.
2802  
2803          $result = core_course_external::get_courses_by_field('id', $course1->id);
2804          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2805          $this->assertCount(1, $result['courses']);
2806          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2807          // Expect to receive all the fields.
2808          $this->assertCount(42, $result['courses'][0]);
2809          // Check default values for course format topics.
2810          $this->assertCount(3, $result['courses'][0]['courseformatoptions']);
2811          foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2812              switch ($option['name']) {
2813                  case 'hiddensections':
2814                      $this->assertEquals(1, $option['value']);
2815                      break;
2816                  case 'coursedisplay':
2817                      $this->assertEquals(0, $option['value']);
2818                      break;
2819                  case 'indentation':
2820                      $this->assertEquals(1, $option['value']);
2821                      break;
2822                  default:
2823              }
2824          }
2825          $this->assertStringContainsString('/course/generated', $result['courses'][0]['courseimage']);
2826  
2827          $result = core_course_external::get_courses_by_field('id', $course2->id);
2828          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2829          $this->assertCount(1, $result['courses']);
2830          $this->assertEquals($course2->id, $result['courses'][0]['id']);
2831          // Check custom fields properly returned.
2832          $this->assertEquals([
2833              'shortname' => $customfield['shortname'],
2834              'name' => $customfield['name'],
2835              'type' => $customfield['type'],
2836              'value' => $customfieldvalue['value'],
2837              'valueraw' => $customfieldvalue['value'],
2838          ], $result['courses'][0]['customfields'][0]);
2839          $this->assertStringContainsString('/course/overviewfiles', $result['courses'][0]['courseimage']);
2840  
2841          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2842          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2843          $this->assertCount(2, $result['courses']);
2844  
2845          // Check default filters.
2846          $this->assertCount(6, $result['courses'][0]['filters']);
2847          $this->assertCount(6, $result['courses'][1]['filters']);
2848  
2849          $result = core_course_external::get_courses_by_field('category', $category1->id);
2850          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2851          $this->assertCount(1, $result['courses']);
2852          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2853          $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2854  
2855          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2856          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2857          $this->assertCount(1, $result['courses']);
2858          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2859  
2860          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2861          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2862          $this->assertCount(1, $result['courses']);
2863          $this->assertEquals($course2->id, $result['courses'][0]['id']);
2864  
2865          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2866          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2867          $this->assertCount(0, $result['courses']);
2868  
2869          // Change filter value.
2870          filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2871  
2872          self::setUser($student1);
2873          // All visible courses  (including front page) for normal student.
2874          $result = core_course_external::get_courses_by_field();
2875          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2876          $this->assertCount(2, $result['courses']);
2877          $this->assertCount(34, $result['courses'][0]);
2878          $this->assertCount(35, $result['courses'][1]);  // One field more (course format options), not present in site course.
2879  
2880          $result = core_course_external::get_courses_by_field('id', $course1->id);
2881          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2882          $this->assertCount(1, $result['courses']);
2883          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2884          // Expect to receive all the files that a student can see.
2885          $this->assertCount(35, $result['courses'][0]);
2886  
2887          // Check default filters.
2888          $filters = $result['courses'][0]['filters'];
2889          $this->assertCount(6, $filters);
2890          $found = false;
2891          foreach ($filters as $filter) {
2892              if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2893                  $found = true;
2894              }
2895          }
2896          $this->assertTrue($found);
2897  
2898          // Course 2 is not visible.
2899          $result = core_course_external::get_courses_by_field('id', $course2->id);
2900          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2901          $this->assertCount(0, $result['courses']);
2902  
2903          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2904          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2905          $this->assertCount(1, $result['courses']);
2906  
2907          $result = core_course_external::get_courses_by_field('category', $category1->id);
2908          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2909          $this->assertCount(1, $result['courses']);
2910          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2911  
2912          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2913          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2914          $this->assertCount(1, $result['courses']);
2915          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2916  
2917          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2918          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2919          $this->assertCount(0, $result['courses']);
2920  
2921          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2922          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2923          $this->assertCount(0, $result['courses']);
2924  
2925          self::setUser($user1);
2926          // All visible courses (including front page) for authenticated user.
2927          $result = core_course_external::get_courses_by_field();
2928          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2929          $this->assertCount(2, $result['courses']);
2930          $this->assertCount(34, $result['courses'][0]);  // Site course.
2931          $this->assertCount(17, $result['courses'][1]);  // Only public information, not enrolled.
2932  
2933          $result = core_course_external::get_courses_by_field('id', $course1->id);
2934          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2935          $this->assertCount(1, $result['courses']);
2936          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2937          // Expect to receive all the files that a authenticated can see.
2938          $this->assertCount(17, $result['courses'][0]);
2939  
2940          // Course 2 is not visible.
2941          $result = core_course_external::get_courses_by_field('id', $course2->id);
2942          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2943          $this->assertCount(0, $result['courses']);
2944  
2945          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2946          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2947          $this->assertCount(1, $result['courses']);
2948  
2949          $result = core_course_external::get_courses_by_field('category', $category1->id);
2950          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2951          $this->assertCount(1, $result['courses']);
2952          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2953  
2954          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2955          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2956          $this->assertCount(1, $result['courses']);
2957          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2958  
2959          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2960          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2961          $this->assertCount(0, $result['courses']);
2962  
2963          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2964          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2965          $this->assertCount(0, $result['courses']);
2966      }
2967  
2968      /**
2969       * Test retrieving courses by field returns custom field data
2970       */
2971      public function test_get_courses_by_field_customfields(): void {
2972          $this->resetAfterTest();
2973          $this->setAdminUser();
2974  
2975          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
2976          $datefield = $this->getDataGenerator()->create_custom_field([
2977              'categoryid' => $fieldcategory->get('id'),
2978              'shortname' => 'mydate',
2979              'name' => 'My date',
2980              'type' => 'date',
2981          ]);
2982  
2983          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
2984              [
2985                  'shortname' => $datefield->get('shortname'),
2986                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
2987              ],
2988          ]]);
2989  
2990          $result = external_api::clean_returnvalue(
2991              core_course_external::get_courses_by_field_returns(),
2992              core_course_external::get_courses_by_field('id', $newcourse->id)
2993          );
2994  
2995          $this->assertCount(1, $result['courses']);
2996          $course = reset($result['courses']);
2997  
2998          $this->assertArrayHasKey('customfields', $course);
2999          $this->assertCount(1, $course['customfields']);
3000  
3001          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
3002          $this->assertEquals([
3003              'name' => $datefield->get('name'),
3004              'shortname' => $datefield->get('shortname'),
3005              'type' => $datefield->get('type'),
3006              'value' => userdate(1580389200),
3007              'valueraw' => 1580389200,
3008          ], reset($course['customfields']));
3009      }
3010  
3011      public function test_get_courses_by_field_invalid_field() {
3012          $this->expectException('invalid_parameter_exception');
3013          $result = core_course_external::get_courses_by_field('zyx', 'x');
3014      }
3015  
3016      public function test_get_courses_by_field_invalid_courses() {
3017          $result = core_course_external::get_courses_by_field('id', '-1');
3018          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3019          $this->assertCount(0, $result['courses']);
3020      }
3021  
3022      /**
3023       * Test get_courses_by_field_invalid_theme_and_lang
3024       */
3025      public function test_get_courses_by_field_invalid_theme_and_lang() {
3026          $this->resetAfterTest(true);
3027          $this->setAdminUser();
3028  
3029          $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
3030          $result = core_course_external::get_courses_by_field('id', $course->id);
3031          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3032          $this->assertEmpty($result['courses']['0']['theme']);
3033          $this->assertEmpty($result['courses']['0']['lang']);
3034      }
3035  
3036  
3037      public function test_check_updates() {
3038          global $DB;
3039          $this->resetAfterTest(true);
3040          $this->setAdminUser();
3041  
3042          // Create different types of activities.
3043          $course  = self::getDataGenerator()->create_course();
3044          $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz',
3045                              'resource', 'scorm', 'survey', 'url', 'wiki');
3046  
3047          $modules = array();
3048          foreach ($tocreate as $modname) {
3049              $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
3050              $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
3051              $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
3052          }
3053  
3054          $student = self::getDataGenerator()->create_user();
3055          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3056          self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
3057          $this->setUser($student);
3058  
3059          $since = time();
3060          $this->waitForSecond();
3061          $params = array();
3062          foreach ($modules as $modname => $data) {
3063              $params[$data['cm']->id] = array(
3064                  'contextlevel' => 'module',
3065                  'id' => $data['cm']->id,
3066                  'since' => $since
3067              );
3068          }
3069  
3070          // Check there is nothing updated because modules are fresh new.
3071          $result = core_course_external::check_updates($course->id, $params);
3072          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3073          $this->assertCount(0, $result['instances']);
3074          $this->assertCount(0, $result['warnings']);
3075  
3076          // Test with get_updates_since the same data.
3077          $result = core_course_external::get_updates_since($course->id, $since);
3078          $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3079          $this->assertCount(0, $result['instances']);
3080          $this->assertCount(0, $result['warnings']);
3081  
3082          // Update a module after a second.
3083          $this->waitForSecond();
3084          set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
3085  
3086          $found = false;
3087          $result = core_course_external::check_updates($course->id, $params);
3088          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3089          $this->assertCount(1, $result['instances']);
3090          $this->assertCount(0, $result['warnings']);
3091          foreach ($result['instances'] as $module) {
3092              foreach ($module['updates'] as $update) {
3093                  if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3094                      $found = true;
3095                  }
3096              }
3097          }
3098          $this->assertTrue($found);
3099  
3100          // Test with get_updates_since the same data.
3101          $result = core_course_external::get_updates_since($course->id, $since);
3102          $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3103          $this->assertCount(1, $result['instances']);
3104          $this->assertCount(0, $result['warnings']);
3105          $found = false;
3106          $this->assertCount(1, $result['instances']);
3107          $this->assertCount(0, $result['warnings']);
3108          foreach ($result['instances'] as $module) {
3109              foreach ($module['updates'] as $update) {
3110                  if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3111                      $found = true;
3112                  }
3113              }
3114          }
3115          $this->assertTrue($found);
3116  
3117          // Do not retrieve the configuration field.
3118          $filter = array('files');
3119          $found = false;
3120          $result = core_course_external::check_updates($course->id, $params, $filter);
3121          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3122          $this->assertCount(0, $result['instances']);
3123          $this->assertCount(0, $result['warnings']);
3124          $this->assertFalse($found);
3125  
3126          // Add invalid cmid.
3127          $params[] = array(
3128              'contextlevel' => 'module',
3129              'id' => -2,
3130              'since' => $since
3131          );
3132          $result = core_course_external::check_updates($course->id, $params);
3133          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3134          $this->assertCount(1, $result['warnings']);
3135          $this->assertEquals(-2, $result['warnings'][0]['itemid']);
3136      }
3137  
3138      /**
3139       * Test cases for the get_enrolled_courses_by_timeline_classification test.
3140       */
3141      public function get_get_enrolled_courses_by_timeline_classification_test_cases():array {
3142          $now = time();
3143          $day = 86400;
3144  
3145          $coursedata = [
3146              [
3147                  'shortname' => 'apast',
3148                  'startdate' => $now - ($day * 2),
3149                  'enddate' => $now - $day
3150              ],
3151              [
3152                  'shortname' => 'bpast',
3153                  'startdate' => $now - ($day * 2),
3154                  'enddate' => $now - $day
3155              ],
3156              [
3157                  'shortname' => 'cpast',
3158                  'startdate' => $now - ($day * 2),
3159                  'enddate' => $now - $day
3160              ],
3161              [
3162                  'shortname' => 'dpast',
3163                  'startdate' => $now - ($day * 2),
3164                  'enddate' => $now - $day
3165              ],
3166              [
3167                  'shortname' => 'epast',
3168                  'startdate' => $now - ($day * 2),
3169                  'enddate' => $now - $day
3170              ],
3171              [
3172                  'shortname' => 'ainprogress',
3173                  'startdate' => $now - $day,
3174                  'enddate' => $now + $day
3175              ],
3176              [
3177                  'shortname' => 'binprogress',
3178                  'startdate' => $now - $day,
3179                  'enddate' => $now + $day
3180              ],
3181              [
3182                  'shortname' => 'cinprogress',
3183                  'startdate' => $now - $day,
3184                  'enddate' => $now + $day
3185              ],
3186              [
3187                  'shortname' => 'dinprogress',
3188                  'startdate' => $now - $day,
3189                  'enddate' => $now + $day
3190              ],
3191              [
3192                  'shortname' => 'einprogress',
3193                  'startdate' => $now - $day,
3194                  'enddate' => $now + $day
3195              ],
3196              [
3197                  'shortname' => 'afuture',
3198                  'startdate' => $now + $day
3199              ],
3200              [
3201                  'shortname' => 'bfuture',
3202                  'startdate' => $now + $day
3203              ],
3204              [
3205                  'shortname' => 'cfuture',
3206                  'startdate' => $now + $day
3207              ],
3208              [
3209                  'shortname' => 'dfuture',
3210                  'startdate' => $now + $day
3211              ],
3212              [
3213                  'shortname' => 'efuture',
3214                  'startdate' => $now + $day
3215              ]
3216          ];
3217  
3218          // Raw enrolled courses result set should be returned in this order:
3219          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
3220          // dfuture, dinprogress, dpast, efuture, einprogress, epast
3221          //
3222          // By classification the offset values for each record should be:
3223          // COURSE_TIMELINE_FUTURE
3224          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
3225          // COURSE_TIMELINE_INPROGRESS
3226          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
3227          // COURSE_TIMELINE_PAST
3228          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3229          //
3230          // NOTE: The offset applies to the unfiltered full set of courses before the classification
3231          // filtering is done.
3232          // E.g. In our example if an offset of 2 is given then it would mean the first
3233          // two courses (afuture, ainprogress) are ignored.
3234          return [
3235              'empty set' => [
3236                  'coursedata' => [],
3237                  'classification' => 'future',
3238                  'limit' => 2,
3239                  'offset' => 0,
3240                  'sort' => 'shortname ASC',
3241                  'expectedcourses' => [],
3242                  'expectednextoffset' => 0,
3243              ],
3244              // COURSE_TIMELINE_FUTURE.
3245              'future not limit no offset' => [
3246                  'coursedata' => $coursedata,
3247                  'classification' => 'future',
3248                  'limit' => 0,
3249                  'offset' => 0,
3250                  'sort' => 'shortname ASC',
3251                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3252                  'expectednextoffset' => 15,
3253              ],
3254              'future no offset' => [
3255                  'coursedata' => $coursedata,
3256                  'classification' => 'future',
3257                  'limit' => 2,
3258                  'offset' => 0,
3259                  'sort' => 'shortname ASC',
3260                  'expectedcourses' => ['afuture', 'bfuture'],
3261                  'expectednextoffset' => 4,
3262              ],
3263              'future offset' => [
3264                  'coursedata' => $coursedata,
3265                  'classification' => 'future',
3266                  'limit' => 2,
3267                  'offset' => 2,
3268                  'sort' => 'shortname ASC',
3269                  'expectedcourses' => ['bfuture', 'cfuture'],
3270                  'expectednextoffset' => 7,
3271              ],
3272              'future exact limit' => [
3273                  'coursedata' => $coursedata,
3274                  'classification' => 'future',
3275                  'limit' => 5,
3276                  'offset' => 0,
3277                  'sort' => 'shortname ASC',
3278                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3279                  'expectednextoffset' => 13,
3280              ],
3281              'future limit less results' => [
3282                  'coursedata' => $coursedata,
3283                  'classification' => 'future',
3284                  'limit' => 10,
3285                  'offset' => 0,
3286                  'sort' => 'shortname ASC',
3287                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3288                  'expectednextoffset' => 15,
3289              ],
3290              'future limit less results with offset' => [
3291                  'coursedata' => $coursedata,
3292                  'classification' => 'future',
3293                  'limit' => 10,
3294                  'offset' => 5,
3295                  'sort' => 'shortname ASC',
3296                  'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3297                  'expectednextoffset' => 15,
3298              ],
3299              'all no limit or offset' => [
3300                  'coursedata' => $coursedata,
3301                  'classification' => 'all',
3302                  'limit' => 0,
3303                  'offset' => 0,
3304                  'sort' => 'shortname ASC',
3305                  'expectedcourses' => [
3306                      'afuture',
3307                      'ainprogress',
3308                      'apast',
3309                      'bfuture',
3310                      'binprogress',
3311                      'bpast',
3312                      'cfuture',
3313                      'cinprogress',
3314                      'cpast',
3315                      'dfuture',
3316                      'dinprogress',
3317                      'dpast',
3318                      'efuture',
3319                      'einprogress',
3320                      'epast'
3321                  ],
3322                  'expectednextoffset' => 15,
3323              ],
3324              'all limit no offset' => [
3325                  'coursedata' => $coursedata,
3326                  'classification' => 'all',
3327                  'limit' => 5,
3328                  'offset' => 0,
3329                  'sort' => 'shortname ASC',
3330                  'expectedcourses' => [
3331                      'afuture',
3332                      'ainprogress',
3333                      'apast',
3334                      'bfuture',
3335                      'binprogress'
3336                  ],
3337                  'expectednextoffset' => 5,
3338              ],
3339              'all limit and offset' => [
3340                  'coursedata' => $coursedata,
3341                  'classification' => 'all',
3342                  'limit' => 5,
3343                  'offset' => 5,
3344                  'sort' => 'shortname ASC',
3345                  'expectedcourses' => [
3346                      'bpast',
3347                      'cfuture',
3348                      'cinprogress',
3349                      'cpast',
3350                      'dfuture'
3351                  ],
3352                  'expectednextoffset' => 10,
3353              ],
3354              'all offset past result set' => [
3355                  'coursedata' => $coursedata,
3356                  'classification' => 'all',
3357                  'limit' => 5,
3358                  'offset' => 50,
3359                  'sort' => 'shortname ASC',
3360                  'expectedcourses' => [],
3361                  'expectednextoffset' => 50,
3362              ],
3363              'all limit and offset with sort ul.timeaccess desc' => [
3364                  'coursedata' => $coursedata,
3365                  'classification' => 'inprogress',
3366                  'limit' => 0,
3367                  'offset' => 0,
3368                  'sort' => 'ul.timeaccess desc',
3369                  'expectedcourses' => [
3370                      'ainprogress',
3371                      'binprogress',
3372                      'cinprogress',
3373                      'dinprogress',
3374                      'einprogress'
3375                  ],
3376                  'expectednextoffset' => 15,
3377              ],
3378              'all limit and offset with sort sql injection for sort or 1==1' => [
3379                  'coursedata' => $coursedata,
3380                  'classification' => 'all',
3381                  'limit' => 5,
3382                  'offset' => 5,
3383                  'sort' => 'ul.timeaccess desc or 1==1',
3384                  'expectedcourses' => [],
3385                  'expectednextoffset' => 0,
3386                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3387              ],
3388              'all limit and offset with sql injection of sort a custom one' => [
3389                  'coursedata' => $coursedata,
3390                  'classification' => 'all',
3391                  'limit' => 5,
3392                  'offset' => 5,
3393                  'sort' => "ul.timeaccess LIMIT 1--",
3394                  'expectedcourses' => [],
3395                  'expectednextoffset' => 0,
3396                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3397              ],
3398              'all limit and offset with wrong sort direction' => [
3399                  'coursedata' => $coursedata,
3400                  'classification' => 'all',
3401                  'limit' => 5,
3402                  'offset' => 5,
3403                  'sort' => "ul.timeaccess abcdasc",
3404                  'expectedcourses' => [],
3405                  'expectednextoffset' => 0,
3406                  'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3407              ],
3408              'all limit and offset with wrong sort direction' => [
3409                  'coursedata' => $coursedata,
3410                  'classification' => 'all',
3411                  'limit' => 5,
3412                  'offset' => 5,
3413                  'sort' => "ul.timeaccess.foo ascd",
3414                  'expectedcourses' => [],
3415                  'expectednextoffset' => 0,
3416                  'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3417              ],
3418              'all limit and offset with wrong sort param' => [
3419                  'coursedata' => $coursedata,
3420                  'classification' => 'all',
3421                  'limit' => 5,
3422                  'offset' => 5,
3423                  'sort' => "foobar",
3424                  'expectedcourses' => [],
3425                  'expectednextoffset' => 0,
3426                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3427              ],
3428              'all limit and offset with wrong field name' => [
3429                  'coursedata' => $coursedata,
3430                  'classification' => 'all',
3431                  'limit' => 5,
3432                  'offset' => 5,
3433                  'sort' => "ul.foobar",
3434                  'expectedcourses' => [],
3435                  'expectednextoffset' => 0,
3436                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3437              ],
3438              'all limit and offset with wrong field separator' => [
3439                  'coursedata' => $coursedata,
3440                  'classification' => 'all',
3441                  'limit' => 5,
3442                  'offset' => 5,
3443                  'sort' => "ul.timeaccess.foo",
3444                  'expectedcourses' => [],
3445                  'expectednextoffset' => 0,
3446                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3447              ],
3448              'all limit and offset with wrong field separator #' => [
3449                  'coursedata' => $coursedata,
3450                  'classification' => 'all',
3451                  'limit' => 5,
3452                  'offset' => 5,
3453                  'sort' => "ul#timeaccess",
3454                  'expectedcourses' => [],
3455                  'expectednextoffset' => 0,
3456                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3457              ],
3458              'all limit and offset with wrong field separator $' => [
3459                  'coursedata' => $coursedata,
3460                  'classification' => 'all',
3461                  'limit' => 5,
3462                  'offset' => 5,
3463                  'sort' => 'ul$timeaccess',
3464                  'expectedcourses' => [],
3465                  'expectednextoffset' => 0,
3466                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3467              ],
3468              'all limit and offset with wrong field name' => [
3469                  'coursedata' => $coursedata,
3470                  'classification' => 'all',
3471                  'limit' => 5,
3472                  'offset' => 5,
3473                  'sort' => 'timeaccess123',
3474                  'expectedcourses' => [],
3475                  'expectednextoffset' => 0,
3476                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3477              ],
3478              'all limit and offset with no sort direction for ul' => [
3479                  'coursedata' => $coursedata,
3480                  'classification' => 'inprogress',
3481                  'limit' => 0,
3482                  'offset' => 0,
3483                  'sort' => "ul.timeaccess",
3484                  'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3485                  'expectednextoffset' => 15,
3486              ],
3487              'all limit and offset with valid field name and no prefix, test for ul' => [
3488                  'coursedata' => $coursedata,
3489                  'classification' => 'inprogress',
3490                  'limit' => 0,
3491                  'offset' => 0,
3492                  'sort' => "timeaccess",
3493                  'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3494                  'expectednextoffset' => 15,
3495              ],
3496              'all limit and offset with valid field name and no prefix' => [
3497                  'coursedata' => $coursedata,
3498                  'classification' => 'all',
3499                  'limit' => 5,
3500                  'offset' => 5,
3501                  'sort' => "fullname",
3502                  'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3503                  'expectednextoffset' => 10,
3504              ],
3505              'all limit and offset with valid field name and no prefix and with sort direction' => [
3506                  'coursedata' => $coursedata,
3507                  'classification' => 'all',
3508                  'limit' => 5,
3509                  'offset' => 5,
3510                  'sort' => "fullname desc",
3511                  'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3512                  'expectednextoffset' => 10,
3513              ],
3514              'Search courses for courses containing bfut' => [
3515                  'coursedata' => $coursedata,
3516                  'classification' => 'search',
3517                  'limit' => 0,
3518                  'offset' => 0,
3519                  'sort' => null,
3520                  'expectedcourses' => ['bfuture'],
3521                  'expectednextoffset' => 1,
3522                  'expectedexception' => null,
3523                  'searchvalue' => 'bfut',
3524              ],
3525              'Search courses for courses containing inp' => [
3526                  'coursedata' => $coursedata,
3527                  'classification' => 'search',
3528                  'limit' => 0,
3529                  'offset' => 0,
3530                  'sort' => null,
3531                  'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3532                  'expectednextoffset' => 5,
3533                  'expectedexception' => null,
3534                  'searchvalue' => 'inp',
3535              ],
3536              'Search courses for courses containing fail' => [
3537                  'coursedata' => $coursedata,
3538                  'classification' => 'search',
3539                  'limit' => 0,
3540                  'offset' => 0,
3541                  'sort' => null,
3542                  'expectedcourses' => [],
3543                  'expectednextoffset' => 0,
3544                  'expectedexception' => null,
3545                  'searchvalue' => 'fail',
3546              ],
3547              'Search courses for courses containing !`~[]C' => [
3548                  'coursedata' => $coursedata,
3549                  'classification' => 'search',
3550                  'limit' => 0,
3551                  'offset' => 0,
3552                  'sort' => null,
3553                  'expectedcourses' => [],
3554                  'expectednextoffset' => 0,
3555                  'expectedexception' => null,
3556                  'searchvalue' => '!`~[]C',
3557              ],
3558          ];
3559      }
3560  
3561      /**
3562       * Test the get_enrolled_courses_by_timeline_classification function.
3563       *
3564       * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
3565       * @param array $coursedata Courses to create
3566       * @param string $classification Timeline classification
3567       * @param int $limit Maximum number of results
3568       * @param int $offset Offset the unfiltered courses result set by this amount
3569       * @param string $sort sort the courses
3570       * @param array $expectedcourses Expected courses in result
3571       * @param int $expectednextoffset Expected next offset value in result
3572       * @param string|null $expectedexception Expected exception string
3573       * @param string|null $searchvalue If we are searching, what do we need to look for?
3574       */
3575      public function test_get_enrolled_courses_by_timeline_classification(
3576          $coursedata,
3577          $classification,
3578          $limit,
3579          $offset,
3580          $sort,
3581          $expectedcourses,
3582          $expectednextoffset,
3583          $expectedexception = null,
3584          $searchvalue = null
3585      ) {
3586          $this->resetAfterTest();
3587          $generator = $this->getDataGenerator();
3588  
3589          $courses = array_map(function($coursedata) use ($generator) {
3590              return $generator->create_course($coursedata);
3591          }, $coursedata);
3592  
3593          $student = $generator->create_user();
3594  
3595          foreach ($courses as $course) {
3596              $generator->enrol_user($student->id, $course->id, 'student');
3597          }
3598  
3599          $this->setUser($student);
3600  
3601          if (isset($expectedexception)) {
3602              $this->expectException('coding_exception');
3603              $this->expectExceptionMessage($expectedexception);
3604          }
3605  
3606          // NOTE: The offset applies to the unfiltered full set of courses before the classification
3607          // filtering is done.
3608          // E.g. In our example if an offset of 2 is given then it would mean the first
3609          // two courses (afuture, ainprogress) are ignored.
3610          $result = core_course_external::get_enrolled_courses_by_timeline_classification(
3611              $classification,
3612              $limit,
3613              $offset,
3614              $sort,
3615              null,
3616              null,
3617              $searchvalue
3618          );
3619          $result = external_api::clean_returnvalue(
3620              core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
3621              $result
3622          );
3623  
3624          $actual = array_map(function($course) {
3625              return $course['shortname'];
3626          }, $result['courses']);
3627  
3628          $this->assertEqualsCanonicalizing($expectedcourses, $actual);
3629          $this->assertEquals($expectednextoffset, $result['nextoffset']);
3630      }
3631  
3632      /**
3633       * Test the get_recent_courses function.
3634       */
3635      public function test_get_recent_courses() {
3636          global $USER, $DB;
3637  
3638          $this->resetAfterTest();
3639          $generator = $this->getDataGenerator();
3640  
3641          set_config('hiddenuserfields', 'lastaccess');
3642  
3643          $courses = array();
3644          for ($i = 1; $i < 12; $i++) {
3645              $courses[]  = $generator->create_course();
3646          };
3647  
3648          $student = $generator->create_user();
3649          $teacher = $generator->create_user();
3650  
3651          foreach ($courses as $course) {
3652              $generator->enrol_user($student->id, $course->id, 'student');
3653          }
3654  
3655          $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
3656  
3657          $this->setUser($student);
3658  
3659          $result = core_course_external::get_recent_courses($USER->id);
3660  
3661          // No course accessed.
3662          $this->assertCount(0, $result);
3663  
3664          foreach ($courses as $course) {
3665              core_course_external::view_course($course->id);
3666          }
3667  
3668          // Every course accessed.
3669          $result = core_course_external::get_recent_courses($USER->id);
3670          $this->assertCount( 11, $result);
3671  
3672          // Every course accessed, result limited to 10 courses.
3673          $result = core_course_external::get_recent_courses($USER->id, 10);
3674          $this->assertCount(10, $result);
3675  
3676          $guestcourse = $generator->create_course(
3677                  (object)array('shortname' => 'guestcourse',
3678                  'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
3679                  'enrol_guest_password_0' => ''));
3680          core_course_external::view_course($guestcourse->id);
3681  
3682          // Every course accessed, even the not enrolled one.
3683          $result = core_course_external::get_recent_courses($USER->id);
3684          $this->assertCount(12, $result);
3685  
3686          // Offset 5, return 7 out of 12.
3687          $result = core_course_external::get_recent_courses($USER->id, 0, 5);
3688          $this->assertCount(7, $result);
3689  
3690          // Offset 5 and limit 3, return 3 out of 12.
3691          $result = core_course_external::get_recent_courses($USER->id, 3, 5);
3692          $this->assertCount(3, $result);
3693  
3694          // Sorted by course id ASC.
3695          $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
3696          $this->assertEquals($courses[0]->id, array_shift($result)->id);
3697  
3698          // Sorted by course id DESC.
3699          $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
3700          $this->assertEquals($guestcourse->id, array_shift($result)->id);
3701  
3702          // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
3703          $this->setUser($teacher);
3704          $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
3705          $usercontext = context_user::instance($student->id);
3706          $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
3707  
3708          // Sorted by course id DESC.
3709          $result = core_course_external::get_recent_courses($student->id);
3710          $this->assertCount(1, $result);
3711          $this->assertEquals($courses[0]->id, array_shift($result)->id);
3712      }
3713  
3714      /**
3715       * Test get enrolled users by cmid function.
3716       */
3717      public function test_get_enrolled_users_by_cmid() {
3718          global $PAGE;
3719          $this->resetAfterTest(true);
3720  
3721          $user1 = self::getDataGenerator()->create_user();
3722          $user2 = self::getDataGenerator()->create_user();
3723          $user3 = self::getDataGenerator()->create_user();
3724  
3725          $user1picture = new user_picture($user1);
3726          $user1picture->size = 1;
3727          $user1->profileimage = $user1picture->get_url($PAGE)->out(false);
3728  
3729          $user2picture = new user_picture($user2);
3730          $user2picture->size = 1;
3731          $user2->profileimage = $user2picture->get_url($PAGE)->out(false);
3732  
3733          $user3picture = new user_picture($user3);
3734          $user3picture->size = 1;
3735          $user3->profileimage = $user3picture->get_url($PAGE)->out(false);
3736  
3737          // Set the first created user to the test user.
3738          self::setUser($user1);
3739  
3740          // Create course to add the module.
3741          $course1 = self::getDataGenerator()->create_course();
3742  
3743          // Forum with tracking off.
3744          $record = new stdClass();
3745          $record->course = $course1->id;
3746          $forum1 = self::getDataGenerator()->create_module('forum', $record);
3747  
3748          // Following lines enrol and assign default role id to the users.
3749          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
3750          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
3751          // Enrol a suspended user in the course.
3752          $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
3753  
3754          // Create what we expect to be returned when querying the course module.
3755          $expectedusers = array(
3756              'users' => array(),
3757              'warnings' => array(),
3758          );
3759  
3760          $expectedusers['users'][0] = [
3761              'id' => $user1->id,
3762              'fullname' => fullname($user1),
3763              'firstname' => $user1->firstname,
3764              'lastname' => $user1->lastname,
3765              'profileimage' => $user1->profileimage,
3766          ];
3767          $expectedusers['users'][1] = [
3768              'id' => $user2->id,
3769              'fullname' => fullname($user2),
3770              'firstname' => $user2->firstname,
3771              'lastname' => $user2->lastname,
3772              'profileimage' => $user2->profileimage,
3773          ];
3774          $expectedusers['users'][2] = [
3775              'id' => $user3->id,
3776              'fullname' => fullname($user3),
3777              'firstname' => $user3->firstname,
3778              'lastname' => $user3->lastname,
3779              'profileimage' => $user3->profileimage,
3780          ];
3781  
3782          // Test getting the users in a given context.
3783          $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
3784          $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3785  
3786          $this->assertEquals(3, count($users['users']));
3787          $this->assertEquals($expectedusers, $users);
3788  
3789          // Test getting only the active users in a given context.
3790          $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true);
3791          $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3792  
3793          $expectedusers['users'] = [
3794              [
3795                  'id' => $user1->id,
3796                  'fullname' => fullname($user1),
3797                  'firstname' => $user1->firstname,
3798                  'lastname' => $user1->lastname,
3799                  'profileimage' => $user1->profileimage,
3800              ],
3801              [
3802                  'id' => $user2->id,
3803                  'fullname' => fullname($user2),
3804                  'firstname' => $user2->firstname,
3805                  'lastname' => $user2->lastname,
3806                  'profileimage' => $user2->profileimage,
3807              ]
3808          ];
3809  
3810          $this->assertEquals(2, count($users['users']));
3811          $this->assertEquals($expectedusers, $users);
3812      }
3813  
3814      /**
3815       * Verify that content items can be added to user favourites.
3816       */
3817      public function test_add_content_item_to_user_favourites() {
3818          $this->resetAfterTest();
3819  
3820          $course = $this->getDataGenerator()->create_course();
3821          $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3822          $this->setUser($user);
3823  
3824          // Using the internal API, confirm that no items are set as favourites for the user.
3825          $contentitemservice = new \core_course\local\service\content_item_service(
3826              new \core_course\local\repository\content_item_readonly_repository()
3827          );
3828          $contentitems = $contentitemservice->get_all_content_items($user);
3829          $favourited = array_filter($contentitems, function($contentitem) {
3830              return $contentitem->favourite == true;
3831          });
3832          $this->assertCount(0, $favourited);
3833  
3834          // Using the external API, favourite a content item for the user.
3835          $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
3836          $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id);
3837          $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(),
3838              $contentitem);
3839  
3840          // Verify the returned item is a favourite.
3841          $this->assertTrue($contentitem['favourite']);
3842  
3843          // Using the internal API, confirm we see a single favourite item.
3844          $contentitems = $contentitemservice->get_all_content_items($user);
3845          $favourited = array_values(array_filter($contentitems, function($contentitem) {
3846              return $contentitem->favourite == true;
3847          }));
3848          $this->assertCount(1, $favourited);
3849          $this->assertEquals('assign', $favourited[0]->name);
3850      }
3851  
3852      /**
3853       * Verify that content items can be removed from user favourites.
3854       */
3855      public function test_remove_content_item_from_user_favourites() {
3856          $this->resetAfterTest();
3857  
3858          $course = $this->getDataGenerator()->create_course();
3859          $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3860          $this->setUser($user);
3861  
3862          // Using the internal API, set a favourite for the user.
3863          $contentitemservice = new \core_course\local\service\content_item_service(
3864              new \core_course\local\repository\content_item_readonly_repository()
3865          );
3866          $contentitems = $contentitemservice->get_all_content_items($user);
3867          $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
3868          $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id);
3869  
3870          $contentitems = $contentitemservice->get_all_content_items($user);
3871          $favourited = array_filter($contentitems, function($contentitem) {
3872              return $contentitem->favourite == true;
3873          });
3874          $this->assertCount(1, $favourited);
3875  
3876          // Now, verify the external API can remove the favourite.
3877          $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id);
3878          $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(),
3879              $contentitem);
3880  
3881          // Verify the returned item is a favourite.
3882          $this->assertFalse($contentitem['favourite']);
3883  
3884          // Using the internal API, confirm we see no favourite items.
3885          $contentitems = $contentitemservice->get_all_content_items($user);
3886          $favourited = array_filter($contentitems, function($contentitem) {
3887              return $contentitem->favourite == true;
3888          });
3889          $this->assertCount(0, $favourited);
3890      }
3891  
3892      /**
3893       * Test the web service returning course content items for inclusion in activity choosers, etc.
3894       */
3895      public function test_get_course_content_items() {
3896          $this->resetAfterTest();
3897  
3898          $course  = self::getDataGenerator()->create_course();
3899          $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
3900  
3901          // Fetch available content items as the editing teacher.
3902          $this->setUser($user);
3903          $result = core_course_external::get_course_content_items($course->id);
3904          $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
3905  
3906          $contentitemservice = new \core_course\local\service\content_item_service(
3907              new \core_course\local\repository\content_item_readonly_repository()
3908          );
3909  
3910          // Check if the webservice returns exactly what the service defines, albeit in array form.
3911          $serviceitemsasarray = array_map(function($item) {
3912              return (array) $item;
3913          }, $contentitemservice->get_content_items_for_user_in_course($user, $course));
3914  
3915          $this->assertEquals($serviceitemsasarray, $result['content_items']);
3916      }
3917  
3918      /**
3919       * Test the web service returning course content items, specifically in case where the user can't manage activities.
3920       */
3921      public function test_get_course_content_items_no_permission_to_manage() {
3922          $this->resetAfterTest();
3923  
3924          $course  = self::getDataGenerator()->create_course();
3925          $user = self::getDataGenerator()->create_and_enrol($course, 'student');
3926  
3927          // Fetch available content items as a student, who won't have the permission to manage activities.
3928          $this->setUser($user);
3929          $result = core_course_external::get_course_content_items($course->id);
3930          $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
3931  
3932          $this->assertEmpty($result['content_items']);
3933      }
3934  
3935      /**
3936       * Test toggling the recommendation of an activity.
3937       */
3938      public function test_toggle_activity_recommendation() {
3939          global $CFG;
3940  
3941          $this->resetAfterTest();
3942  
3943          $context = context_system::instance();
3944          $usercontext = context_user::instance($CFG->siteguest);
3945          $component = 'core_course';
3946          $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
3947  
3948          $areaname = 'test_core';
3949          $areaid = 3;
3950  
3951          // Test we have the favourite.
3952          $this->setAdminUser();
3953          $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
3954          $this->assertTrue($favouritefactory->favourite_exists($component,
3955                  \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context));
3956          $this->assertTrue($result['status']);
3957          // Test that it is now gone.
3958          $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
3959          $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context));
3960          $this->assertFalse($result['status']);
3961      }
3962  
3963  }