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