Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 39 and 310]

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