Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Forum search unit tests.
  19   *
  20   * @package     mod_forum
  21   * @category    test
  22   * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_forum\search;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
  32  require_once($CFG->dirroot . '/mod/forum/tests/generator/lib.php');
  33  require_once($CFG->dirroot . '/mod/forum/lib.php');
  34  
  35  /**
  36   * Provides the unit tests for forum search.
  37   *
  38   * @package     mod_forum
  39   * @category    test
  40   * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
  41   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class search_test extends \advanced_testcase {
  44  
  45      /**
  46       * @var string Area id
  47       */
  48      protected $forumpostareaid = null;
  49  
  50      public function setUp(): void {
  51          $this->resetAfterTest(true);
  52          set_config('enableglobalsearch', true);
  53  
  54          $this->forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
  55  
  56          // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
  57          $search = \testable_core_search::instance();
  58      }
  59  
  60      /**
  61       * Availability.
  62       *
  63       * @return void
  64       */
  65      public function test_search_enabled() {
  66  
  67          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
  68          list($componentname, $varname) = $searcharea->get_config_var_name();
  69  
  70          // Enabled by default once global search is enabled.
  71          $this->assertTrue($searcharea->is_enabled());
  72  
  73          set_config($varname . '_enabled', 0, $componentname);
  74          $this->assertFalse($searcharea->is_enabled());
  75  
  76          set_config($varname . '_enabled', 1, $componentname);
  77          $this->assertTrue($searcharea->is_enabled());
  78      }
  79  
  80      /**
  81       * Indexing mod forum contents.
  82       *
  83       * @return void
  84       */
  85      public function test_posts_indexing() {
  86          global $DB;
  87  
  88          // Returns the instance as long as the area is supported.
  89          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
  90          $this->assertInstanceOf('\mod_forum\search\post', $searcharea);
  91  
  92          $user1 = self::getDataGenerator()->create_user();
  93          $user2 = self::getDataGenerator()->create_user();
  94  
  95          $course1 = self::getDataGenerator()->create_course();
  96          $course2 = self::getDataGenerator()->create_course();
  97  
  98          $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
  99          $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
 100  
 101          $record = new \stdClass();
 102          $record->course = $course1->id;
 103  
 104          // Available for both student and teacher.
 105          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 106  
 107          // Create discussion1.
 108          $record = new \stdClass();
 109          $record->course = $course1->id;
 110          $record->userid = $user1->id;
 111          $record->forum = $forum1->id;
 112          $record->message = 'discussion';
 113          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 114  
 115          // Create post1 in discussion1.
 116          $record = new \stdClass();
 117          $record->discussion = $discussion1->id;
 118          $record->parent = $discussion1->firstpost;
 119          $record->userid = $user2->id;
 120          $record->message = 'post2';
 121          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 122  
 123          // All records.
 124          $recordset = $searcharea->get_recordset_by_timestamp(0);
 125          $this->assertTrue($recordset->valid());
 126          $nrecords = 0;
 127          foreach ($recordset as $record) {
 128              $this->assertInstanceOf('stdClass', $record);
 129              $doc = $searcharea->get_document($record);
 130              $this->assertInstanceOf('\core_search\document', $doc);
 131  
 132              // Static caches are working.
 133              $dbreads = $DB->perf_get_reads();
 134              $doc = $searcharea->get_document($record);
 135              $this->assertEquals($dbreads, $DB->perf_get_reads());
 136              $this->assertInstanceOf('\core_search\document', $doc);
 137              $nrecords++;
 138          }
 139          // If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
 140          $recordset->close();
 141          $this->assertEquals(2, $nrecords);
 142  
 143          // The +2 is to prevent race conditions.
 144          $recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
 145  
 146          // No new records.
 147          $this->assertFalse($recordset->valid());
 148          $recordset->close();
 149  
 150          // Context test: create another forum with 1 post.
 151          $forum2 = self::getDataGenerator()->create_module('forum', ['course' => $course1->id]);
 152          $record = new \stdClass();
 153          $record->course = $course1->id;
 154          $record->userid = $user1->id;
 155          $record->forum = $forum2->id;
 156          $record->message = 'discussion';
 157          self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 158  
 159          // Test indexing with each forum then combined course context.
 160          $rs = $searcharea->get_document_recordset(0, \context_module::instance($forum1->cmid));
 161          $this->assertEquals(2, iterator_count($rs));
 162          $rs->close();
 163          $rs = $searcharea->get_document_recordset(0, \context_module::instance($forum2->cmid));
 164          $this->assertEquals(1, iterator_count($rs));
 165          $rs->close();
 166          $rs = $searcharea->get_document_recordset(0, \context_course::instance($course1->id));
 167          $this->assertEquals(3, iterator_count($rs));
 168          $rs->close();
 169      }
 170  
 171      /**
 172       * Document contents.
 173       *
 174       * @return void
 175       */
 176      public function test_posts_document() {
 177          global $DB;
 178  
 179          // Returns the instance as long as the area is supported.
 180          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 181          $this->assertInstanceOf('\mod_forum\search\post', $searcharea);
 182  
 183          $user = self::getDataGenerator()->create_user();
 184          $course1 = self::getDataGenerator()->create_course();
 185          $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'teacher');
 186  
 187          $record = new \stdClass();
 188          $record->course = $course1->id;
 189          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 190  
 191          // Teacher only.
 192          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 193          set_coursemodule_visible($forum2->cmid, 0);
 194  
 195          // Create discussion1.
 196          $record = new \stdClass();
 197          $record->course = $course1->id;
 198          $record->userid = $user->id;
 199          $record->forum = $forum1->id;
 200          $record->message = 'discussion';
 201          $record->groupid = 0;
 202          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 203  
 204          // Create post1 in discussion1.
 205          $record = new \stdClass();
 206          $record->discussion = $discussion1->id;
 207          $record->parent = $discussion1->firstpost;
 208          $record->userid = $user->id;
 209          $record->subject = 'subject1';
 210          $record->message = 'post1';
 211          $record->groupid = -1;
 212          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 213  
 214          $post1 = $DB->get_record('forum_posts', array('id' => $discussion1reply1->id));
 215          $post1->forumid = $forum1->id;
 216          $post1->courseid = $forum1->course;
 217          $post1->groupid = -1;
 218  
 219          $doc = $searcharea->get_document($post1);
 220          $this->assertInstanceOf('\core_search\document', $doc);
 221          $this->assertEquals($discussion1reply1->id, $doc->get('itemid'));
 222          $this->assertEquals($this->forumpostareaid . '-' . $discussion1reply1->id, $doc->get('id'));
 223          $this->assertEquals($course1->id, $doc->get('courseid'));
 224          $this->assertEquals($user->id, $doc->get('userid'));
 225          $this->assertEquals($discussion1reply1->subject, $doc->get('title'));
 226          $this->assertEquals($discussion1reply1->message, $doc->get('content'));
 227      }
 228  
 229      /**
 230       * Group support for forum posts.
 231       */
 232      public function test_posts_group_support() {
 233          // Get the search area and test generators.
 234          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 235          $generator = $this->getDataGenerator();
 236          $forumgenerator = $generator->get_plugin_generator('mod_forum');
 237  
 238          // Create a course, a user, and two groups.
 239          $course = $generator->create_course();
 240          $user = $generator->create_user();
 241          $generator->enrol_user($user->id, $course->id, 'teacher');
 242          $group1 = $generator->create_group(['courseid' => $course->id]);
 243          $group2 = $generator->create_group(['courseid' => $course->id]);
 244  
 245          // Separate groups forum.
 246          $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id,
 247                  'groupmode' => SEPARATEGROUPS]);
 248  
 249          // Create discussion with each group and one for all groups. One has a post in.
 250          $discussion1 = $forumgenerator->create_discussion(['course' => $course->id,
 251                  'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd1',
 252                  'groupid' => $group1->id]);
 253          $forumgenerator->create_discussion(['course' => $course->id,
 254                  'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd2',
 255                  'groupid' => $group2->id]);
 256          $forumgenerator->create_discussion(['course' => $course->id,
 257                  'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd3']);
 258  
 259          // Create a reply in discussion1.
 260          $forumgenerator->create_post(['discussion' => $discussion1->id, 'parent' => $discussion1->firstpost,
 261                  'userid' => $user->id, 'message' => 'p1']);
 262  
 263          // Do the indexing of all 4 posts.
 264          $rs = $searcharea->get_recordset_by_timestamp(0);
 265          $results = [];
 266          foreach ($rs as $rec) {
 267              $results[$rec->message] = $rec;
 268          }
 269          $rs->close();
 270          $this->assertCount(4, $results);
 271  
 272          // Check each document has the correct groupid.
 273          $doc = $searcharea->get_document($results['d1']);
 274          $this->assertTrue($doc->is_set('groupid'));
 275          $this->assertEquals($group1->id, $doc->get('groupid'));
 276          $doc = $searcharea->get_document($results['d2']);
 277          $this->assertTrue($doc->is_set('groupid'));
 278          $this->assertEquals($group2->id, $doc->get('groupid'));
 279          $doc = $searcharea->get_document($results['d3']);
 280          $this->assertFalse($doc->is_set('groupid'));
 281          $doc = $searcharea->get_document($results['p1']);
 282          $this->assertTrue($doc->is_set('groupid'));
 283          $this->assertEquals($group1->id, $doc->get('groupid'));
 284  
 285          // While we're here, also test that the search area requests restriction by group.
 286          $modinfo = get_fast_modinfo($course);
 287          $this->assertTrue($searcharea->restrict_cm_access_by_group($modinfo->get_cm($forum->cmid)));
 288  
 289          // In visible groups mode, it won't request restriction by group.
 290          set_coursemodule_groupmode($forum->cmid, VISIBLEGROUPS);
 291          $modinfo = get_fast_modinfo($course);
 292          $this->assertFalse($searcharea->restrict_cm_access_by_group($modinfo->get_cm($forum->cmid)));
 293      }
 294  
 295      /**
 296       * Document accesses.
 297       *
 298       * @return void
 299       */
 300      public function test_posts_access() {
 301          global $DB;
 302  
 303          // Returns the instance as long as the area is supported.
 304          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 305  
 306          $user1 = self::getDataGenerator()->create_user();
 307          $user2 = self::getDataGenerator()->create_user();
 308  
 309          $course1 = self::getDataGenerator()->create_course();
 310          $course2 = self::getDataGenerator()->create_course();
 311  
 312          $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
 313          $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
 314  
 315          $record = new \stdClass();
 316          $record->course = $course1->id;
 317  
 318          // Available for both student and teacher.
 319          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 320  
 321          // Teacher only.
 322          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 323          set_coursemodule_visible($forum2->cmid, 0);
 324  
 325          // Create discussion1.
 326          $record = new \stdClass();
 327          $record->course = $course1->id;
 328          $record->userid = $user1->id;
 329          $record->forum = $forum1->id;
 330          $record->message = 'discussion';
 331          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 332  
 333          // Create post1 in discussion1.
 334          $record = new \stdClass();
 335          $record->discussion = $discussion1->id;
 336          $record->parent = $discussion1->firstpost;
 337          $record->userid = $user2->id;
 338          $record->message = 'post1';
 339          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 340  
 341          // Create discussion2 only visible to teacher.
 342          $record = new \stdClass();
 343          $record->course = $course1->id;
 344          $record->userid = $user1->id;
 345          $record->forum = $forum2->id;
 346          $record->message = 'discussion';
 347          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 348  
 349          // Create post2 in discussion2.
 350          $record = new \stdClass();
 351          $record->discussion = $discussion2->id;
 352          $record->parent = $discussion2->firstpost;
 353          $record->userid = $user1->id;
 354          $record->message = 'post2';
 355          $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 356  
 357          $this->setUser($user2);
 358          $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($discussion1reply1->id));
 359          $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($discussion2reply1->id));
 360      }
 361  
 362      /**
 363       * Test for post attachments.
 364       *
 365       * @return void
 366       */
 367      public function test_attach_files() {
 368          global $DB;
 369  
 370          $fs = get_file_storage();
 371  
 372          // Returns the instance as long as the area is supported.
 373          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 374          $this->assertInstanceOf('\mod_forum\search\post', $searcharea);
 375  
 376          $user1 = self::getDataGenerator()->create_user();
 377          $user2 = self::getDataGenerator()->create_user();
 378  
 379          $course1 = self::getDataGenerator()->create_course();
 380  
 381          $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
 382          $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
 383  
 384          $record = new \stdClass();
 385          $record->course = $course1->id;
 386  
 387          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 388  
 389          // Create discussion1.
 390          $record = new \stdClass();
 391          $record->course = $course1->id;
 392          $record->userid = $user1->id;
 393          $record->forum = $forum1->id;
 394          $record->message = 'discussion';
 395          $record->attachemt = 1;
 396          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 397  
 398          // Attach 2 file to the discussion post.
 399          $post = $DB->get_record('forum_posts', array('discussion' => $discussion1->id));
 400          $filerecord = array(
 401              'contextid' => \context_module::instance($forum1->cmid)->id,
 402              'component' => 'mod_forum',
 403              'filearea'  => 'attachment',
 404              'itemid'    => $post->id,
 405              'filepath'  => '/',
 406              'filename'  => 'myfile1'
 407          );
 408          $file1 = $fs->create_file_from_string($filerecord, 'Some contents 1');
 409          $filerecord['filename'] = 'myfile2';
 410          $file2 = $fs->create_file_from_string($filerecord, 'Some contents 2');
 411  
 412          // Create post1 in discussion1.
 413          $record = new \stdClass();
 414          $record->discussion = $discussion1->id;
 415          $record->parent = $discussion1->firstpost;
 416          $record->userid = $user2->id;
 417          $record->message = 'post2';
 418          $record->attachemt = 1;
 419          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 420  
 421          $filerecord['itemid'] = $discussion1reply1->id;
 422          $filerecord['filename'] = 'myfile3';
 423          $file3 = $fs->create_file_from_string($filerecord, 'Some contents 3');
 424  
 425          // Create post2 in discussion1.
 426          $record = new \stdClass();
 427          $record->discussion = $discussion1->id;
 428          $record->parent = $discussion1->firstpost;
 429          $record->userid = $user2->id;
 430          $record->message = 'post3';
 431          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 432  
 433          // Now get all the posts and see if they have the right files attached.
 434          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 435          $recordset = $searcharea->get_recordset_by_timestamp(0);
 436          $nrecords = 0;
 437          foreach ($recordset as $record) {
 438              $doc = $searcharea->get_document($record);
 439              $searcharea->attach_files($doc);
 440              $files = $doc->get_files();
 441              // Now check that each doc has the right files on it.
 442              switch ($doc->get('itemid')) {
 443                  case ($post->id):
 444                      $this->assertCount(2, $files);
 445                      $this->assertEquals($file1->get_id(), $files[$file1->get_id()]->get_id());
 446                      $this->assertEquals($file2->get_id(), $files[$file2->get_id()]->get_id());
 447                      break;
 448                  case ($discussion1reply1->id):
 449                      $this->assertCount(1, $files);
 450                      $this->assertEquals($file3->get_id(), $files[$file3->get_id()]->get_id());
 451                      break;
 452                  case ($discussion1reply2->id):
 453                      $this->assertCount(0, $files);
 454                      break;
 455                  default:
 456                      $this->fail('Unexpected post returned');
 457                      break;
 458              }
 459              $nrecords++;
 460          }
 461          $recordset->close();
 462          $this->assertEquals(3, $nrecords);
 463      }
 464  
 465      /**
 466       * Tests that reindexing works in order starting from the forum with most recent discussion.
 467       */
 468      public function test_posts_get_contexts_to_reindex() {
 469          global $DB;
 470  
 471          $generator = $this->getDataGenerator();
 472          $adminuser = get_admin();
 473  
 474          $course1 = $generator->create_course();
 475          $course2 = $generator->create_course();
 476  
 477          $time = time() - 1000;
 478  
 479          // Create 3 forums (two in course 1, one in course 2 - doesn't make a difference).
 480          $forum1 = $generator->create_module('forum', ['course' => $course1->id]);
 481          $forum2 = $generator->create_module('forum', ['course' => $course1->id]);
 482          $forum3 = $generator->create_module('forum', ['course' => $course2->id]);
 483          $forum4 = $generator->create_module('forum', ['course' => $course2->id]);
 484  
 485          // Hack added time for the course_modules entries. These should not be used (they would
 486          // be used by the base class implementation). We are setting this so that the order would
 487          // be 4, 3, 2, 1 if this ordering were used (newest first).
 488          $DB->set_field('course_modules', 'added', $time + 100, ['id' => $forum1->cmid]);
 489          $DB->set_field('course_modules', 'added', $time + 110, ['id' => $forum2->cmid]);
 490          $DB->set_field('course_modules', 'added', $time + 120, ['id' => $forum3->cmid]);
 491          $DB->set_field('course_modules', 'added', $time + 130, ['id' => $forum4->cmid]);
 492  
 493          $forumgenerator = $generator->get_plugin_generator('mod_forum');
 494  
 495          // Create one discussion in forums 1 and 3, three in forum 2, and none in forum 4.
 496          $forumgenerator->create_discussion(['course' => $course1->id,
 497                  'forum' => $forum1->id, 'userid' => $adminuser->id, 'timemodified' => $time + 20]);
 498  
 499          $forumgenerator->create_discussion(['course' => $course1->id,
 500                  'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 10]);
 501          $forumgenerator->create_discussion(['course' => $course1->id,
 502                  'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 30]);
 503          $forumgenerator->create_discussion(['course' => $course1->id,
 504                  'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 11]);
 505  
 506          $forumgenerator->create_discussion(['course' => $course2->id,
 507                  'forum' => $forum3->id, 'userid' => $adminuser->id, 'timemodified' => $time + 25]);
 508  
 509          // Get the contexts in reindex order.
 510          $area = \core_search\manager::get_search_area($this->forumpostareaid);
 511          $contexts = iterator_to_array($area->get_contexts_to_reindex(), false);
 512  
 513          // We expect them in order of newest discussion. Forum 4 is not included at all (which is
 514          // correct because it has no content).
 515          $expected = [
 516              \context_module::instance($forum2->cmid),
 517              \context_module::instance($forum3->cmid),
 518              \context_module::instance($forum1->cmid)
 519          ];
 520          $this->assertEquals($expected, $contexts);
 521      }
 522  }