Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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