Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

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

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