Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 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   * Table log for displaying logs.
  19   *
  20   * @package    report_log
  21   * @copyright  2014 Rajesh Taneja <rajesh.taneja@gmail.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die;
  26  
  27  /**
  28   * Table log class for displaying logs.
  29   *
  30   * @package    report_log
  31   * @copyright  2014 Rajesh Taneja <rajesh.taneja@gmail.com>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class report_log_table_log extends table_sql {
  35  
  36      /** @var array list of user fullnames shown in report */
  37      private $userfullnames = array();
  38  
  39      /** @var array list of context name shown in report */
  40      private $contextname = array();
  41  
  42      /** @var stdClass filters parameters */
  43      private $filterparams;
  44  
  45      /**
  46       * Sets up the table_log parameters.
  47       *
  48       * @param string $uniqueid unique id of form.
  49       * @param stdClass $filterparams (optional) filter params.
  50       *     - int courseid: id of course
  51       *     - int userid: user id
  52       *     - int|string modid: Module id or "site_errors" to view site errors
  53       *     - int groupid: Group id
  54       *     - \core\log\sql_reader logreader: reader from which data will be fetched.
  55       *     - int edulevel: educational level.
  56       *     - string action: view action
  57       *     - int date: Date from which logs to be viewed.
  58       */
  59      public function __construct($uniqueid, $filterparams = null) {
  60          parent::__construct($uniqueid);
  61  
  62          $this->set_attribute('class', 'reportlog generaltable generalbox table-sm');
  63          $this->filterparams = $filterparams;
  64          // Add course column if logs are displayed for site.
  65          $cols = array();
  66          $headers = array();
  67          if (empty($filterparams->courseid)) {
  68              $cols = array('course');
  69              $headers = array(get_string('course'));
  70          }
  71  
  72          $this->define_columns(array_merge($cols, array('time', 'fullnameuser', 'relatedfullnameuser', 'context', 'component',
  73                  'eventname', 'description', 'origin', 'ip')));
  74          $this->define_headers(array_merge($headers, array(
  75                  get_string('time'),
  76                  get_string('fullnameuser'),
  77                  get_string('eventrelatedfullnameuser', 'report_log'),
  78                  get_string('eventcontext', 'report_log'),
  79                  get_string('eventcomponent', 'report_log'),
  80                  get_string('eventname'),
  81                  get_string('description'),
  82                  get_string('eventorigin', 'report_log'),
  83                  get_string('ip_address')
  84                  )
  85              ));
  86          $this->collapsible(false);
  87          $this->sortable(false);
  88          $this->pageable(true);
  89      }
  90  
  91      /**
  92       * Generate the course column.
  93       *
  94       * @deprecated since Moodle 2.9 MDL-48595 - please do not use this function any more.
  95       */
  96      public function col_course($event) {
  97          throw new coding_exception('col_course() can not be used any more, there is no such column.');
  98      }
  99  
 100      /**
 101       * Gets the user full name.
 102       *
 103       * This function is useful because, in the unlikely case that the user is
 104       * not already loaded in $this->userfullnames it will fetch it from db.
 105       *
 106       * @since Moodle 2.9
 107       * @param int $userid
 108       * @return string|false
 109       */
 110      protected function get_user_fullname($userid) {
 111          if (empty($userid)) {
 112              return false;
 113          }
 114  
 115          // Check if we already have this users' fullname.
 116          $userfullname = $this->userfullnames[$userid] ?? null;
 117          if (!empty($userfullname)) {
 118              return $userfullname;
 119          }
 120  
 121          // We already looked for the user and it does not exist.
 122          if ($userfullname === false) {
 123              return false;
 124          }
 125  
 126          // If we reach that point new users logs have been generated since the last users db query.
 127          $userfieldsapi = \core_user\fields::for_name();
 128          $fields = $userfieldsapi->get_sql('', false, '', '', false)->selects;
 129          if ($user = \core_user::get_user($userid, $fields)) {
 130              $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
 131          } else {
 132              $this->userfullnames[$userid] = false;
 133          }
 134  
 135          return $this->userfullnames[$userid];
 136      }
 137  
 138      /**
 139       * Generate the time column.
 140       *
 141       * @param stdClass $event event data.
 142       * @return string HTML for the time column
 143       */
 144      public function col_time($event) {
 145  
 146          if (empty($this->download)) {
 147              $dateformat = get_string('strftimedatetimeaccurate', 'core_langconfig');
 148          } else {
 149              $dateformat = get_string('strftimedatetimeshortaccurate', 'core_langconfig');
 150          }
 151          return userdate($event->timecreated, $dateformat);
 152      }
 153  
 154      /**
 155       * Generate the username column.
 156       *
 157       * @param stdClass $event event data.
 158       * @return string HTML for the username column
 159       */
 160      public function col_fullnameuser($event) {
 161          // Get extra event data for origin and realuserid.
 162          $logextra = $event->get_logextra();
 163  
 164          // Add username who did the action.
 165          if (!empty($logextra['realuserid'])) {
 166              $a = new stdClass();
 167              if (!$a->realusername = $this->get_user_fullname($logextra['realuserid'])) {
 168                  $a->realusername = '-';
 169              }
 170              if (!$a->asusername = $this->get_user_fullname($event->userid)) {
 171                  $a->asusername = '-';
 172              }
 173              if (empty($this->download)) {
 174                  $params = array('id' => $logextra['realuserid']);
 175                  if ($event->courseid) {
 176                      $params['course'] = $event->courseid;
 177                  }
 178                  $a->realusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->realusername);
 179                  $params['id'] = $event->userid;
 180                  $a->asusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->asusername);
 181              }
 182              $username = get_string('eventloggedas', 'report_log', $a);
 183  
 184          } else if (!empty($event->userid) && $username = $this->get_user_fullname($event->userid)) {
 185              if (empty($this->download)) {
 186                  $params = array('id' => $event->userid);
 187                  if ($event->courseid) {
 188                      $params['course'] = $event->courseid;
 189                  }
 190                  $username = html_writer::link(new moodle_url('/user/view.php', $params), $username);
 191              }
 192          } else {
 193              $username = '-';
 194          }
 195          return $username;
 196      }
 197  
 198      /**
 199       * Generate the related username column.
 200       *
 201       * @param stdClass $event event data.
 202       * @return string HTML for the related username column
 203       */
 204      public function col_relatedfullnameuser($event) {
 205          // Add affected user.
 206          if (!empty($event->relateduserid) && $username = $this->get_user_fullname($event->relateduserid)) {
 207              if (empty($this->download)) {
 208                  $params = array('id' => $event->relateduserid);
 209                  if ($event->courseid) {
 210                      $params['course'] = $event->courseid;
 211                  }
 212                  $username = html_writer::link(new moodle_url('/user/view.php', $params), $username);
 213              }
 214          } else {
 215              $username = '-';
 216          }
 217          return $username;
 218      }
 219  
 220      /**
 221       * Generate the context column.
 222       *
 223       * @param stdClass $event event data.
 224       * @return string HTML for the context column
 225       */
 226      public function col_context($event) {
 227          // Add context name.
 228          if ($event->contextid) {
 229              // If context name was fetched before then return, else get one.
 230              if (isset($this->contextname[$event->contextid])) {
 231                  return $this->contextname[$event->contextid];
 232              } else {
 233                  $context = context::instance_by_id($event->contextid, IGNORE_MISSING);
 234                  if ($context) {
 235                      $contextname = $context->get_context_name(true);
 236                      if (empty($this->download) && $url = $context->get_url()) {
 237                          $contextname = html_writer::link($url, $contextname);
 238                      }
 239                  } else {
 240                      $contextname = get_string('other');
 241                  }
 242              }
 243          } else {
 244              $contextname = get_string('other');
 245          }
 246  
 247          $this->contextname[$event->contextid] = $contextname;
 248          return $contextname;
 249      }
 250  
 251      /**
 252       * Generate the component column.
 253       *
 254       * @param stdClass $event event data.
 255       * @return string HTML for the component column
 256       */
 257      public function col_component($event) {
 258          // Component.
 259          $componentname = $event->component;
 260          if (($event->component === 'core') || ($event->component === 'legacy')) {
 261              return  get_string('coresystem');
 262          } else if (get_string_manager()->string_exists('pluginname', $event->component)) {
 263              return get_string('pluginname', $event->component);
 264          } else {
 265              return $componentname;
 266          }
 267      }
 268  
 269      /**
 270       * Generate the event name column.
 271       *
 272       * @param stdClass $event event data.
 273       * @return string HTML for the event name column
 274       */
 275      public function col_eventname($event) {
 276          // Event name.
 277          if ($this->filterparams->logreader instanceof logstore_legacy\log\store) {
 278              // Hack for support of logstore_legacy.
 279              $eventname = $event->eventname;
 280          } else {
 281              $eventname = $event->get_name();
 282          }
 283          // Only encode as an action link if we're not downloading.
 284          if (($url = $event->get_url()) && empty($this->download)) {
 285              $eventname = $this->action_link($url, $eventname, 'action');
 286          }
 287          return $eventname;
 288      }
 289  
 290      /**
 291       * Generate the description column.
 292       *
 293       * @param stdClass $event event data.
 294       * @return string HTML for the description column
 295       */
 296      public function col_description($event) {
 297          // Description.
 298          return $event->get_description();
 299      }
 300  
 301      /**
 302       * Generate the origin column.
 303       *
 304       * @param stdClass $event event data.
 305       * @return string HTML for the origin column
 306       */
 307      public function col_origin($event) {
 308          // Get extra event data for origin and realuserid.
 309          $logextra = $event->get_logextra();
 310  
 311          // Add event origin, normally IP/cron.
 312          return $logextra['origin'];
 313      }
 314  
 315      /**
 316       * Generate the ip column.
 317       *
 318       * @param stdClass $event event data.
 319       * @return string HTML for the ip column
 320       */
 321      public function col_ip($event) {
 322          // Get extra event data for origin and realuserid.
 323          $logextra = $event->get_logextra();
 324          $ip = $logextra['ip'];
 325  
 326          if (empty($this->download)) {
 327              $url = new moodle_url("/iplookup/index.php?ip={$ip}&user={$event->userid}");
 328              $ip = $this->action_link($url, $ip, 'ip');
 329          }
 330          return $ip;
 331      }
 332  
 333      /**
 334       * Method to create a link with popup action.
 335       *
 336       * @param moodle_url $url The url to open.
 337       * @param string $text Anchor text for the link.
 338       * @param string $name Name of the popup window.
 339       *
 340       * @return string html to use.
 341       */
 342      protected function action_link(moodle_url $url, $text, $name = 'popup') {
 343          global $OUTPUT;
 344          $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 440, 'width' => 700)));
 345          return $OUTPUT->render($link);
 346      }
 347  
 348      /**
 349       * Helper function to get legacy crud action.
 350       *
 351       * @param string $crud crud action
 352       * @return string legacy action.
 353       */
 354      public function get_legacy_crud_action($crud) {
 355          $legacyactionmap = array('c' => 'add', 'r' => 'view', 'u' => 'update', 'd' => 'delete');
 356          if (array_key_exists($crud, $legacyactionmap)) {
 357              return $legacyactionmap[$crud];
 358          } else {
 359              // From old legacy log.
 360              return '-view';
 361          }
 362      }
 363  
 364      /**
 365       * Helper function which is used by build logs to get action sql and param.
 366       *
 367       * @return array sql and param for action.
 368       */
 369      public function get_action_sql() {
 370          global $DB;
 371  
 372          // In new logs we have a field to pick, and in legacy try get this from action.
 373          if ($this->filterparams->logreader instanceof logstore_legacy\log\store) {
 374              $action = $this->get_legacy_crud_action($this->filterparams->action);
 375              $firstletter = substr($action, 0, 1);
 376              if ($firstletter == '-') {
 377                  $sql = $DB->sql_like('action', ':action', false, true, true);
 378                  $params['action'] = '%'.substr($action, 1).'%';
 379              } else {
 380                  $sql = $DB->sql_like('action', ':action', false);
 381                  $params['action'] = '%'.$action.'%';
 382              }
 383          } else if (!empty($this->filterparams->action)) {
 384               list($sql, $params) = $DB->get_in_or_equal(str_split($this->filterparams->action),
 385                      SQL_PARAMS_NAMED, 'crud');
 386              $sql = "crud " . $sql;
 387          } else {
 388              // Add condition for all possible values of crud (to use db index).
 389              list($sql, $params) = $DB->get_in_or_equal(array('c', 'r', 'u', 'd'),
 390                      SQL_PARAMS_NAMED, 'crud');
 391              $sql = "crud ".$sql;
 392          }
 393          return array($sql, $params);
 394      }
 395  
 396      /**
 397       * Helper function which is used by build logs to get course module sql and param.
 398       *
 399       * @return array sql and param for action.
 400       */
 401      public function get_cm_sql() {
 402          $joins = array();
 403          $params = array();
 404  
 405          if ($this->filterparams->logreader instanceof logstore_legacy\log\store) {
 406              // The legacy store doesn't support context level.
 407              $joins[] = "cmid = :cmid";
 408              $params['cmid'] = $this->filterparams->modid;
 409          } else {
 410              $joins[] = "contextinstanceid = :contextinstanceid";
 411              $joins[] = "contextlevel = :contextmodule";
 412              $params['contextinstanceid'] = $this->filterparams->modid;
 413              $params['contextmodule'] = CONTEXT_MODULE;
 414          }
 415  
 416          $sql = implode(' AND ', $joins);
 417          return array($sql, $params);
 418      }
 419  
 420      /**
 421       * Query the reader. Store results in the object for use by build_table.
 422       *
 423       * @param int $pagesize size of page for paginated displayed table.
 424       * @param bool $useinitialsbar do you want to use the initials bar.
 425       */
 426      public function query_db($pagesize, $useinitialsbar = true) {
 427          global $DB;
 428  
 429          $joins = array();
 430          $params = array();
 431  
 432          // If we filter by userid and module id we also need to filter by crud and edulevel to ensure DB index is engaged.
 433          $useextendeddbindex = !($this->filterparams->logreader instanceof logstore_legacy\log\store)
 434                  && !empty($this->filterparams->userid) && !empty($this->filterparams->modid);
 435  
 436          $groupid = 0;
 437          if (!empty($this->filterparams->courseid) && $this->filterparams->courseid != SITEID) {
 438              if (!empty($this->filterparams->groupid)) {
 439                  $groupid = $this->filterparams->groupid;
 440              }
 441  
 442              $joins[] = "courseid = :courseid";
 443              $params['courseid'] = $this->filterparams->courseid;
 444          }
 445  
 446          if (!empty($this->filterparams->siteerrors)) {
 447              $joins[] = "( action='error' OR action='infected' OR action='failed' )";
 448          }
 449  
 450          if (!empty($this->filterparams->modid)) {
 451              list($actionsql, $actionparams) = $this->get_cm_sql();
 452              $joins[] = $actionsql;
 453              $params = array_merge($params, $actionparams);
 454          }
 455  
 456          if (!empty($this->filterparams->action) || $useextendeddbindex) {
 457              list($actionsql, $actionparams) = $this->get_action_sql();
 458              $joins[] = $actionsql;
 459              $params = array_merge($params, $actionparams);
 460          }
 461  
 462          // Getting all members of a group.
 463          if ($groupid and empty($this->filterparams->userid)) {
 464              if ($gusers = groups_get_members($groupid)) {
 465                  $gusers = array_keys($gusers);
 466                  $joins[] = 'userid IN (' . implode(',', $gusers) . ')';
 467              } else {
 468                  $joins[] = 'userid = 0'; // No users in groups, so we want something that will always be false.
 469              }
 470          } else if (!empty($this->filterparams->userid)) {
 471              $joins[] = "userid = :userid";
 472              $params['userid'] = $this->filterparams->userid;
 473          }
 474  
 475          if (!empty($this->filterparams->date)) {
 476              $joins[] = "timecreated > :date AND timecreated < :enddate";
 477              $params['date'] = $this->filterparams->date;
 478              $params['enddate'] = $this->filterparams->date + DAYSECS; // Show logs only for the selected date.
 479          }
 480  
 481          if (isset($this->filterparams->edulevel) && ($this->filterparams->edulevel >= 0)) {
 482              $joins[] = "edulevel = :edulevel";
 483              $params['edulevel'] = $this->filterparams->edulevel;
 484          } else if ($useextendeddbindex) {
 485              list($edulevelsql, $edulevelparams) = $DB->get_in_or_equal(array(\core\event\base::LEVEL_OTHER,
 486                  \core\event\base::LEVEL_PARTICIPATING, \core\event\base::LEVEL_TEACHING), SQL_PARAMS_NAMED, 'edulevel');
 487              $joins[] = "edulevel ".$edulevelsql;
 488              $params = array_merge($params, $edulevelparams);
 489          }
 490  
 491          // Origin.
 492          if (isset($this->filterparams->origin) && ($this->filterparams->origin != '')) {
 493              if ($this->filterparams->origin !== '---') {
 494                  // Filter by a single origin.
 495                  $joins[] = "origin = :origin";
 496                  $params['origin'] = $this->filterparams->origin;
 497              } else {
 498                  // Filter by everything else.
 499                  list($originsql, $originparams) = $DB->get_in_or_equal(array('cli', 'restore', 'ws', 'web'),
 500                      SQL_PARAMS_NAMED, 'origin', false);
 501                  $joins[] = "origin " . $originsql;
 502                  $params = array_merge($params, $originparams);
 503              }
 504          }
 505  
 506          if (!($this->filterparams->logreader instanceof logstore_legacy\log\store)) {
 507              // Filter out anonymous actions, this is N/A for legacy log because it never stores them.
 508              if ($this->filterparams->modid) {
 509                  $context = context_module::instance($this->filterparams->modid);
 510              } else {
 511                  $context = context_course::instance($this->filterparams->courseid);
 512              }
 513              if (!has_capability('moodle/site:viewanonymousevents', $context)) {
 514                  $joins[] = "anonymous = 0";
 515              }
 516          }
 517  
 518          $selector = implode(' AND ', $joins);
 519  
 520          if (!$this->is_downloading()) {
 521              $total = $this->filterparams->logreader->get_events_select_count($selector, $params);
 522              $this->pagesize($pagesize, $total);
 523          } else {
 524              $this->pageable(false);
 525          }
 526  
 527          // Get the users and course data.
 528          $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
 529              $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
 530  
 531          // Update list of users which will be displayed on log page.
 532          $this->update_users_used();
 533  
 534          // Get the events. Same query than before; even if it is not likely, logs from new users
 535          // may be added since last query so we will need to work around later to prevent problems.
 536          // In almost most of the cases this will be better than having two opened recordsets.
 537          $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
 538              $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
 539  
 540          // Set initial bars.
 541          if ($useinitialsbar && !$this->is_downloading()) {
 542              $this->initialbars($total > $pagesize);
 543          }
 544  
 545      }
 546  
 547      /**
 548       * Helper function to create list of course shortname and user fullname shown in log report.
 549       *
 550       * This will update $this->userfullnames and $this->courseshortnames array with userfullname and courseshortname (with link),
 551       * which will be used to render logs in table.
 552       *
 553       * @deprecated since Moodle 2.9 MDL-48595 - please do not use this function any more.
 554       */
 555      public function update_users_and_courses_used() {
 556          throw new coding_exception('update_users_and_courses_used() can not be used any more, please use update_users_used() instead.');
 557      }
 558  
 559      /**
 560       * Helper function to create list of user fullnames shown in log report.
 561       *
 562       * This will update $this->userfullnames array with userfullname,
 563       * which will be used to render logs in table.
 564       *
 565       * @since   Moodle 2.9
 566       * @return  void
 567       */
 568      protected function update_users_used() {
 569          global $DB;
 570  
 571          $this->userfullnames = array();
 572          $userids = array();
 573  
 574          // For each event cache full username.
 575          // Get list of userids which will be shown in log report.
 576          foreach ($this->rawdata as $event) {
 577              $logextra = $event->get_logextra();
 578              if (!empty($event->userid) && empty($userids[$event->userid])) {
 579                  $userids[$event->userid] = $event->userid;
 580              }
 581              if (!empty($logextra['realuserid']) && empty($userids[$logextra['realuserid']])) {
 582                  $userids[$logextra['realuserid']] = $logextra['realuserid'];
 583              }
 584              if (!empty($event->relateduserid) && empty($userids[$event->relateduserid])) {
 585                  $userids[$event->relateduserid] = $event->relateduserid;
 586              }
 587          }
 588          $this->rawdata->close();
 589  
 590          // Get user fullname and put that in return list.
 591          if (!empty($userids)) {
 592              list($usql, $uparams) = $DB->get_in_or_equal($userids);
 593              $userfieldsapi = \core_user\fields::for_name();
 594              $users = $DB->get_records_sql("SELECT id," . $userfieldsapi->get_sql('', false, '', '', false)->selects .
 595                      " FROM {user} WHERE id " . $usql,
 596                      $uparams);
 597              foreach ($users as $userid => $user) {
 598                  $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
 599                  unset($userids[$userid]);
 600              }
 601  
 602              // We fill the array with false values for the users that don't exist anymore
 603              // in the database so we don't need to query the db again later.
 604              foreach ($userids as $userid) {
 605                  $this->userfullnames[$userid] = false;
 606              }
 607          }
 608      }
 609  }