Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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  namespace logstore_standard;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  require_once (__DIR__ . '/fixtures/event.php');
  22  require_once (__DIR__ . '/fixtures/restore_hack.php');
  23  
  24  /**
  25   * Standard log store tests.
  26   *
  27   * @package    logstore_standard
  28   * @copyright  2014 Petr Skoda {@link http://skodak.org/}
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class store_test extends \advanced_testcase {
  32      /**
  33       * @var bool Determine if we disabled the GC, so it can be re-enabled in tearDown.
  34       */
  35      private $wedisabledgc = false;
  36  
  37      /**
  38       * Tests log writing.
  39       *
  40       * @param bool $jsonformat True to test with JSON format
  41       * @dataProvider log_writing_provider
  42       */
  43      public function test_log_writing(bool $jsonformat) {
  44          global $DB;
  45          $this->resetAfterTest();
  46          $this->preventResetByRollback(); // Logging waits till the transaction gets committed.
  47  
  48          // Apply JSON format system setting.
  49          set_config('jsonformat', $jsonformat ? 1 : 0, 'logstore_standard');
  50  
  51          $this->setAdminUser();
  52          $user1 = $this->getDataGenerator()->create_user();
  53          $user2 = $this->getDataGenerator()->create_user();
  54          $course1 = $this->getDataGenerator()->create_course();
  55          $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
  56          $course2 = $this->getDataGenerator()->create_course();
  57          $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
  58  
  59          // Test all plugins are disabled by this command.
  60          set_config('enabled_stores', '', 'tool_log');
  61          $manager = get_log_manager(true);
  62          $stores = $manager->get_readers();
  63          $this->assertCount(0, $stores);
  64  
  65          // Enable logging plugin.
  66          set_config('enabled_stores', 'logstore_standard', 'tool_log');
  67          set_config('buffersize', 0, 'logstore_standard');
  68          set_config('logguests', 1, 'logstore_standard');
  69          $manager = get_log_manager(true);
  70  
  71          $stores = $manager->get_readers();
  72          $this->assertCount(1, $stores);
  73          $this->assertEquals(array('logstore_standard'), array_keys($stores));
  74          /** @var \logstore_standard\log\store $store */
  75          $store = $stores['logstore_standard'];
  76          $this->assertInstanceOf('logstore_standard\log\store', $store);
  77          $this->assertInstanceOf('tool_log\log\writer', $store);
  78          $this->assertTrue($store->is_logging());
  79  
  80          $logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
  81          $this->assertCount(0, $logs);
  82  
  83          $this->setCurrentTimeStart();
  84  
  85          $this->setUser(0);
  86          $event1 = \logstore_standard\event\unittest_executed::create(
  87              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)));
  88          $event1->trigger();
  89  
  90          $logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
  91          $this->assertCount(1, $logs);
  92  
  93          $log1 = reset($logs);
  94          unset($log1->id);
  95          if ($jsonformat) {
  96              $log1->other = json_decode($log1->other, true);
  97          } else {
  98              $log1->other = unserialize($log1->other);
  99          }
 100          $log1 = (array)$log1;
 101          $data = $event1->get_data();
 102          $data['origin'] = 'cli';
 103          $data['ip'] = null;
 104          $data['realuserid'] = null;
 105          $this->assertEquals($data, $log1);
 106  
 107          $this->setAdminUser();
 108          \core\session\manager::loginas($user1->id, \context_system::instance());
 109          $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
 110  
 111          \logstore_standard_restore::hack_executing(1);
 112          $event2 = \logstore_standard\event\unittest_executed::create(
 113              array('context' => \context_module::instance($module2->cmid), 'other' => array('sample' => 6, 'xx' => 9)));
 114          $event2->trigger();
 115          \logstore_standard_restore::hack_executing(0);
 116  
 117          \core\session\manager::init_empty_session();
 118          $this->assertFalse(\core\session\manager::is_loggedinas());
 119  
 120          $logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
 121          $this->assertCount(3, $logs);
 122          array_shift($logs);
 123          $log2 = array_shift($logs);
 124          $this->assertSame('\core\event\user_loggedinas', $log2->eventname);
 125          $this->assertSame('cli', $log2->origin);
 126  
 127          $log3 = array_shift($logs);
 128          unset($log3->id);
 129          if ($jsonformat) {
 130              $log3->other = json_decode($log3->other, true);
 131          } else {
 132              $log3->other = unserialize($log3->other);
 133          }
 134          $log3 = (array)$log3;
 135          $data = $event2->get_data();
 136          $data['origin'] = 'restore';
 137          $data['ip'] = null;
 138          $data['realuserid'] = 2;
 139          $this->assertEquals($data, $log3);
 140  
 141          // Test table exists.
 142          $tablename = $store->get_internal_log_table_name();
 143          $this->assertTrue($DB->get_manager()->table_exists($tablename));
 144  
 145          // Test reading.
 146          $this->assertSame(3, $store->get_events_select_count('', array()));
 147          $events = $store->get_events_select('', array(), 'timecreated ASC', 0, 0); // Is actually sorted by "timecreated ASC, id ASC".
 148          $this->assertCount(3, $events);
 149          $resev1 = array_shift($events);
 150          array_shift($events);
 151          $resev2 = array_shift($events);
 152          $this->assertEquals($event1->get_data(), $resev1->get_data());
 153          $this->assertEquals($event2->get_data(), $resev2->get_data());
 154  
 155          // Test buffering.
 156          set_config('buffersize', 3, 'logstore_standard');
 157          $manager = get_log_manager(true);
 158          $stores = $manager->get_readers();
 159          /** @var \logstore_standard\log\store $store */
 160          $store = $stores['logstore_standard'];
 161          $DB->delete_records('logstore_standard_log');
 162  
 163          \logstore_standard\event\unittest_executed::create(
 164              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 165          $this->assertEquals(0, $DB->count_records('logstore_standard_log'));
 166          \logstore_standard\event\unittest_executed::create(
 167              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 168          $this->assertEquals(0, $DB->count_records('logstore_standard_log'));
 169          $store->flush();
 170          $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
 171          \logstore_standard\event\unittest_executed::create(
 172              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 173          $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
 174          \logstore_standard\event\unittest_executed::create(
 175              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 176          $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
 177          \logstore_standard\event\unittest_executed::create(
 178              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 179          $this->assertEquals(5, $DB->count_records('logstore_standard_log'));
 180          \logstore_standard\event\unittest_executed::create(
 181              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 182          $this->assertEquals(5, $DB->count_records('logstore_standard_log'));
 183          \logstore_standard\event\unittest_executed::create(
 184              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 185          $this->assertEquals(5, $DB->count_records('logstore_standard_log'));
 186          \logstore_standard\event\unittest_executed::create(
 187              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 188          $this->assertEquals(8, $DB->count_records('logstore_standard_log'));
 189  
 190          // Test guest logging setting.
 191          set_config('logguests', 0, 'logstore_standard');
 192          set_config('buffersize', 0, 'logstore_standard');
 193          get_log_manager(true);
 194          $DB->delete_records('logstore_standard_log');
 195          get_log_manager(true);
 196  
 197          $this->setUser(null);
 198          \logstore_standard\event\unittest_executed::create(
 199              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 200          $this->assertEquals(0, $DB->count_records('logstore_standard_log'));
 201  
 202          $this->setGuestUser();
 203          \logstore_standard\event\unittest_executed::create(
 204              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 205          $this->assertEquals(0, $DB->count_records('logstore_standard_log'));
 206  
 207          $this->setUser($user1);
 208          \logstore_standard\event\unittest_executed::create(
 209              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 210          $this->assertEquals(1, $DB->count_records('logstore_standard_log'));
 211  
 212          $this->setUser($user2);
 213          \logstore_standard\event\unittest_executed::create(
 214              array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
 215          $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
 216  
 217          set_config('enabled_stores', '', 'tool_log');
 218          get_log_manager(true);
 219      }
 220  
 221      /**
 222       * Returns different JSON format settings so the test can be run with JSON format either on or
 223       * off.
 224       *
 225       * @return bool[] Array of true/false
 226       */
 227      public static function log_writing_provider(): array {
 228          return [
 229              [false],
 230              [true]
 231          ];
 232      }
 233  
 234      /**
 235       * Test logmanager::get_supported_reports returns all reports that require this store.
 236       */
 237      public function test_get_supported_reports() {
 238          $logmanager = get_log_manager();
 239          $allreports = \core_component::get_plugin_list('report');
 240  
 241          // Make sure all supported reports are installed.
 242          $expectedreports = array_intersect_key([
 243              'log' => 'report_log',
 244              'loglive' => 'report_loglive',
 245              'outline' => 'report_outline',
 246              'participation' => 'report_participation',
 247              'stats' => 'report_stats'
 248          ], $allreports);
 249  
 250          $supportedreports = $logmanager->get_supported_reports('logstore_standard');
 251  
 252          foreach ($expectedreports as $expectedreport) {
 253              $this->assertArrayHasKey($expectedreport, $supportedreports);
 254          }
 255      }
 256  
 257      /**
 258       * Verify that gc disabling works
 259       */
 260      public function test_gc_enabled_as_expected() {
 261          if (!gc_enabled()) {
 262              $this->markTestSkipped('Garbage collector (gc) is globally disabled.');
 263          }
 264  
 265          $this->disable_gc();
 266          $this->assertTrue($this->wedisabledgc);
 267          $this->assertFalse(gc_enabled());
 268      }
 269  
 270      /**
 271       * Test sql_reader::get_events_select_iterator.
 272       * @return void
 273       */
 274      public function test_events_traversable() {
 275          global $DB;
 276  
 277          $this->disable_gc();
 278  
 279          $this->resetAfterTest();
 280          $this->preventResetByRollback();
 281          $this->setAdminUser();
 282  
 283          set_config('enabled_stores', 'logstore_standard', 'tool_log');
 284  
 285          $manager = get_log_manager(true);
 286          $stores = $manager->get_readers();
 287          $store = $stores['logstore_standard'];
 288  
 289          $events = $store->get_events_select_iterator('', array(), '', 0, 0);
 290          $this->assertFalse($events->valid());
 291  
 292          // Here it should be already closed, but we should be allowed to
 293          // over-close it without exception.
 294          $events->close();
 295  
 296          $user = $this->getDataGenerator()->create_user();
 297          for ($i = 0; $i < 1000; $i++) {
 298              \core\event\user_created::create_from_userid($user->id)->trigger();
 299          }
 300          $store->flush();
 301  
 302          // Check some various sizes get the right number of elements.
 303          $this->assertEquals(1, iterator_count($store->get_events_select_iterator('', array(), '', 0, 1)));
 304          $this->assertEquals(2, iterator_count($store->get_events_select_iterator('', array(), '', 0, 2)));
 305  
 306          $iterator = $store->get_events_select_iterator('', array(), '', 0, 500);
 307          $this->assertInstanceOf('\core\event\base', $iterator->current());
 308          $this->assertEquals(500, iterator_count($iterator));
 309          $iterator->close();
 310  
 311          // Look for non-linear memory usage for the iterator version.
 312          $mem = memory_get_usage();
 313          $events = $store->get_events_select('', array(), '', 0, 0);
 314          $arraymemusage = memory_get_usage() - $mem;
 315  
 316          $mem = memory_get_usage();
 317          $eventsit = $store->get_events_select_iterator('', array(), '', 0, 0);
 318          $eventsit->close();
 319          $itmemusage = memory_get_usage() - $mem;
 320  
 321          $this->assertInstanceOf('\Traversable', $eventsit);
 322  
 323          $this->assertLessThan($arraymemusage / 10, $itmemusage);
 324          set_config('enabled_stores', '', 'tool_log');
 325          get_log_manager(true);
 326      }
 327  
 328      /**
 329       * Test that the standard log cleanup works correctly.
 330       */
 331      public function test_cleanup_task() {
 332          global $DB;
 333  
 334          $this->resetAfterTest();
 335  
 336          // Create some records spread over various days; test multiple iterations in cleanup.
 337          $ctx = \context_course::instance(1);
 338          $record = (object) array(
 339              'edulevel' => 0,
 340              'contextid' => $ctx->id,
 341              'contextlevel' => $ctx->contextlevel,
 342              'contextinstanceid' => $ctx->instanceid,
 343              'userid' => 1,
 344              'timecreated' => time(),
 345          );
 346          $DB->insert_record('logstore_standard_log', $record);
 347          $record->timecreated -= 3600 * 24 * 30;
 348          $DB->insert_record('logstore_standard_log', $record);
 349          $record->timecreated -= 3600 * 24 * 30;
 350          $DB->insert_record('logstore_standard_log', $record);
 351          $record->timecreated -= 3600 * 24 * 30;
 352          $DB->insert_record('logstore_standard_log', $record);
 353          $this->assertEquals(4, $DB->count_records('logstore_standard_log'));
 354  
 355          // Remove all logs before "today".
 356          set_config('loglifetime', 1, 'logstore_standard');
 357  
 358          $this->expectOutputString(" Deleted old log records from standard store.\n");
 359          $clean = new \logstore_standard\task\cleanup_task();
 360          $clean->execute();
 361  
 362          $this->assertEquals(1, $DB->count_records('logstore_standard_log'));
 363      }
 364  
 365      /**
 366       * Tests the decode_other function can cope with both JSON and PHP serialized format.
 367       *
 368       * @param mixed $value Value to encode and decode
 369       * @dataProvider decode_other_provider
 370       */
 371      public function test_decode_other($value) {
 372          $this->assertEquals($value, \logstore_standard\log\store::decode_other(serialize($value)));
 373          $this->assertEquals($value, \logstore_standard\log\store::decode_other(json_encode($value)));
 374      }
 375  
 376      public function test_decode_other_with_wrongly_encoded_contents() {
 377          $this->assertSame(null, \logstore_standard\log\store::decode_other(null));
 378      }
 379  
 380      /**
 381       * List of possible values for 'other' field.
 382       *
 383       * I took these types from our logs based on the different first character of PHP serialized
 384       * data - my query found only these types. The normal case is an array.
 385       *
 386       * @return array Array of parameters
 387       */
 388      public function decode_other_provider(): array {
 389          return [
 390              [['info' => 'd2819896', 'logurl' => 'discuss.php?d=2819896']],
 391              [null],
 392              ['just a string'],
 393              [32768]
 394          ];
 395      }
 396  
 397      /**
 398       * Checks that backup and restore of log data works correctly.
 399       *
 400       * @param bool $jsonformat True to test with JSON format
 401       * @dataProvider log_writing_provider
 402       */
 403      public function test_backup_restore(bool $jsonformat) {
 404          global $DB;
 405          $this->resetAfterTest();
 406          $this->preventResetByRollback();
 407  
 408          // Enable logging plugin.
 409          set_config('enabled_stores', 'logstore_standard', 'tool_log');
 410          set_config('buffersize', 0, 'logstore_standard');
 411          $manager = get_log_manager(true);
 412  
 413          // User must be enrolled in course.
 414          $generator = $this->getDataGenerator();
 415          $course = $generator->create_course();
 416          $user = $generator->create_user();
 417          $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
 418          $this->setUser($user);
 419  
 420          // Apply JSON format system setting.
 421          set_config('jsonformat', $jsonformat ? 1 : 0, 'logstore_standard');
 422  
 423          // Create some log data in a course - one with other data, one without.
 424          \logstore_standard\event\unittest_executed::create([
 425                  'context' => \context_course::instance($course->id),
 426                  'other' => ['sample' => 5, 'xx' => 10]])->trigger();
 427          $this->waitForSecond();
 428          \logstore_standard\event\unittest_executed::create([
 429                  'context' => \context_course::instance($course->id)])->trigger();
 430  
 431          $records = array_values($DB->get_records('logstore_standard_log',
 432                  ['courseid' => $course->id, 'target' => 'unittest'], 'timecreated'));
 433          $this->assertCount(2, $records);
 434  
 435          // Work out expected 'other' values based on JSON format.
 436          $expected0 = [
 437              false => 'a:2:{s:6:"sample";i:5;s:2:"xx";i:10;}',
 438              true => '{"sample":5,"xx":10}'
 439          ];
 440          $expected1 = [
 441              false => 'N;',
 442              true => 'null'
 443          ];
 444  
 445          // Backup the course twice, including log data.
 446          $this->setAdminUser();
 447          $backupid1 = $this->backup($course);
 448          $backupid2 = $this->backup($course);
 449  
 450          // Restore it with same jsonformat.
 451          $newcourseid = $this->restore($backupid1, $course, '_A');
 452  
 453          // Check entries are correctly encoded.
 454          $records = array_values($DB->get_records('logstore_standard_log',
 455                  ['courseid' => $newcourseid, 'target' => 'unittest'], 'timecreated'));
 456          $this->assertCount(2, $records);
 457          $this->assertEquals($expected0[$jsonformat], $records[0]->other);
 458          $this->assertEquals($expected1[$jsonformat], $records[1]->other);
 459  
 460          // Change JSON format to opposite value and restore again.
 461          set_config('jsonformat', $jsonformat ? 0 : 1, 'logstore_standard');
 462          $newcourseid = $this->restore($backupid2, $course, '_B');
 463  
 464          // Check entries are correctly encoded in other format.
 465          $records = array_values($DB->get_records('logstore_standard_log',
 466                  ['courseid' => $newcourseid, 'target' => 'unittest'], 'timecreated'));
 467          $this->assertEquals($expected0[!$jsonformat], $records[0]->other);
 468          $this->assertEquals($expected1[!$jsonformat], $records[1]->other);
 469      }
 470  
 471      /**
 472       * Backs a course up to temp directory.
 473       *
 474       * @param \stdClass $course Course object to backup
 475       * @return string ID of backup
 476       */
 477      protected function backup($course): string {
 478          global $USER, $CFG;
 479          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 480  
 481          // Turn off file logging, otherwise it can't delete the file (Windows).
 482          $CFG->backup_file_logger_level = \backup::LOG_NONE;
 483  
 484          // Do backup with default settings. MODE_IMPORT means it will just
 485          // create the directory and not zip it.
 486          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
 487                  \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
 488                  $USER->id);
 489          $bc->get_plan()->get_setting('users')->set_status(\backup_setting::NOT_LOCKED);
 490          $bc->get_plan()->get_setting('users')->set_value(true);
 491          $bc->get_plan()->get_setting('logs')->set_value(true);
 492          $backupid = $bc->get_backupid();
 493  
 494          $bc->execute_plan();
 495          $bc->destroy();
 496          return $backupid;
 497      }
 498  
 499      /**
 500       * Restores a course from temp directory.
 501       *
 502       * @param string $backupid Backup id
 503       * @param \stdClass $course Original course object
 504       * @param string $suffix Suffix to add after original course shortname and fullname
 505       * @return int New course id
 506       * @throws \restore_controller_exception
 507       */
 508      protected function restore(string $backupid, $course, string $suffix): int {
 509          global $USER, $CFG;
 510          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
 511  
 512          // Do restore to new course with default settings.
 513          $newcourseid = \restore_dbops::create_new_course(
 514                  $course->fullname . $suffix, $course->shortname . $suffix, $course->category);
 515          $rc = new \restore_controller($backupid, $newcourseid,
 516                  \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
 517                  \backup::TARGET_NEW_COURSE);
 518          $rc->get_plan()->get_setting('logs')->set_value(true);
 519          $rc->get_plan()->get_setting('users')->set_value(true);
 520  
 521          $this->assertTrue($rc->execute_precheck());
 522          $rc->execute_plan();
 523          $rc->destroy();
 524  
 525          return $newcourseid;
 526      }
 527  
 528      /**
 529       * Disable the garbage collector if it's enabled to ensure we don't adjust memory statistics.
 530       */
 531      private function disable_gc() {
 532          if (gc_enabled()) {
 533              $this->wedisabledgc = true;
 534              gc_disable();
 535          }
 536      }
 537  
 538      /**
 539       * Reset any garbage collector changes to the previous state at the end of the test.
 540       */
 541      public function tearDown(): void {
 542          if ($this->wedisabledgc) {
 543              gc_enable();
 544          }
 545          $this->wedisabledgc = false;
 546      }
 547  }