Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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          $eventname = $event->get_name();
 278          // Only encode as an action link if we're not downloading.
 279          if (($url = $event->get_url()) && empty($this->download)) {
 280              $eventname = $this->action_link($url, $eventname, 'action');
 281          }
 282          return $eventname;
 283      }
 284  
 285      /**
 286       * Generate the description column.
 287       *
 288       * @param stdClass $event event data.
 289       * @return string HTML for the description column
 290       */
 291      public function col_description($event) {
 292          // Description.
 293          return $event->get_description();
 294      }
 295  
 296      /**
 297       * Generate the origin column.
 298       *
 299       * @param stdClass $event event data.
 300       * @return string HTML for the origin column
 301       */
 302      public function col_origin($event) {
 303          // Get extra event data for origin and realuserid.
 304          $logextra = $event->get_logextra();
 305  
 306          // Add event origin, normally IP/cron.
 307          return $logextra['origin'];
 308      }
 309  
 310      /**
 311       * Generate the ip column.
 312       *
 313       * @param stdClass $event event data.
 314       * @return string HTML for the ip column
 315       */
 316      public function col_ip($event) {
 317          // Get extra event data for origin and realuserid.
 318          $logextra = $event->get_logextra();
 319          $ip = $logextra['ip'];
 320  
 321          if (empty($this->download)) {
 322              $url = new moodle_url("/iplookup/index.php?popup=1&ip={$ip}&user={$event->userid}");
 323              $ip = $this->action_link($url, $ip, 'ip');
 324          }
 325          return $ip;
 326      }
 327  
 328      /**
 329       * Method to create a link with popup action.
 330       *
 331       * @param moodle_url $url The url to open.
 332       * @param string $text Anchor text for the link.
 333       * @param string $name Name of the popup window.
 334       *
 335       * @return string html to use.
 336       */
 337      protected function action_link(moodle_url $url, $text, $name = 'popup') {
 338          global $OUTPUT;
 339          $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 440, 'width' => 700)));
 340          return $OUTPUT->render($link);
 341      }
 342  
 343      /**
 344       * Helper function to get legacy crud action.
 345       *
 346       * @param string $crud crud action
 347       * @return string legacy action.
 348       */
 349      public function get_legacy_crud_action($crud) {
 350          $legacyactionmap = array('c' => 'add', 'r' => 'view', 'u' => 'update', 'd' => 'delete');
 351          if (array_key_exists($crud, $legacyactionmap)) {
 352              return $legacyactionmap[$crud];
 353          } else {
 354              // From old legacy log.
 355              return '-view';
 356          }
 357      }
 358  
 359      /**
 360       * Helper function which is used by build logs to get action sql and param.
 361       *
 362       * @return array sql and param for action.
 363       */
 364      public function get_action_sql() {
 365          global $DB;
 366  
 367          // In new logs we have a field to pick, and in legacy try get this from action.
 368          if (!empty($this->filterparams->action)) {
 369               list($sql, $params) = $DB->get_in_or_equal(str_split($this->filterparams->action),
 370                      SQL_PARAMS_NAMED, 'crud');
 371              $sql = "crud " . $sql;
 372          } else {
 373              // Add condition for all possible values of crud (to use db index).
 374              list($sql, $params) = $DB->get_in_or_equal(array('c', 'r', 'u', 'd'),
 375                      SQL_PARAMS_NAMED, 'crud');
 376              $sql = "crud ".$sql;
 377          }
 378          return array($sql, $params);
 379      }
 380  
 381      /**
 382       * Helper function which is used by build logs to get course module sql and param.
 383       *
 384       * @return array sql and param for action.
 385       */
 386      public function get_cm_sql() {
 387          $joins = array();
 388          $params = array();
 389  
 390          $joins[] = "contextinstanceid = :contextinstanceid";
 391          $joins[] = "contextlevel = :contextmodule";
 392          $params['contextinstanceid'] = $this->filterparams->modid;
 393          $params['contextmodule'] = CONTEXT_MODULE;
 394  
 395          $sql = implode(' AND ', $joins);
 396          return array($sql, $params);
 397      }
 398  
 399      /**
 400       * Query the reader. Store results in the object for use by build_table.
 401       *
 402       * @param int $pagesize size of page for paginated displayed table.
 403       * @param bool $useinitialsbar do you want to use the initials bar.
 404       */
 405      public function query_db($pagesize, $useinitialsbar = true) {
 406          global $DB;
 407  
 408          $joins = array();
 409          $params = array();
 410  
 411          // If we filter by userid and module id we also need to filter by crud and edulevel to ensure DB index is engaged.
 412          $useextendeddbindex = !empty($this->filterparams->userid) && !empty($this->filterparams->modid);
 413  
 414          $groupid = 0;
 415          if (!empty($this->filterparams->courseid) && $this->filterparams->courseid != SITEID) {
 416              if (!empty($this->filterparams->groupid)) {
 417                  $groupid = $this->filterparams->groupid;
 418              }
 419  
 420              $joins[] = "courseid = :courseid";
 421              $params['courseid'] = $this->filterparams->courseid;
 422          }
 423  
 424          if (!empty($this->filterparams->siteerrors)) {
 425              $joins[] = "( action='error' OR action='infected' OR action='failed' )";
 426          }
 427  
 428          if (!empty($this->filterparams->modid)) {
 429              list($actionsql, $actionparams) = $this->get_cm_sql();
 430              $joins[] = $actionsql;
 431              $params = array_merge($params, $actionparams);
 432          }
 433  
 434          if (!empty($this->filterparams->action) || $useextendeddbindex) {
 435              list($actionsql, $actionparams) = $this->get_action_sql();
 436              $joins[] = $actionsql;
 437              $params = array_merge($params, $actionparams);
 438          }
 439  
 440          // Getting all members of a group.
 441          if ($groupid and empty($this->filterparams->userid)) {
 442              if ($gusers = groups_get_members($groupid)) {
 443                  $gusers = array_keys($gusers);
 444                  $joins[] = 'userid IN (' . implode(',', $gusers) . ')';
 445              } else {
 446                  $joins[] = 'userid = 0'; // No users in groups, so we want something that will always be false.
 447              }
 448          } else if (!empty($this->filterparams->userid)) {
 449              $joins[] = "userid = :userid";
 450              $params['userid'] = $this->filterparams->userid;
 451          }
 452  
 453          if (!empty($this->filterparams->date)) {
 454              $joins[] = "timecreated > :date AND timecreated < :enddate";
 455              $params['date'] = $this->filterparams->date;
 456              $params['enddate'] = $this->filterparams->date + DAYSECS; // Show logs only for the selected date.
 457          }
 458  
 459          if (isset($this->filterparams->edulevel) && ($this->filterparams->edulevel >= 0)) {
 460              $joins[] = "edulevel = :edulevel";
 461              $params['edulevel'] = $this->filterparams->edulevel;
 462          } else if ($useextendeddbindex) {
 463              list($edulevelsql, $edulevelparams) = $DB->get_in_or_equal(array(\core\event\base::LEVEL_OTHER,
 464                  \core\event\base::LEVEL_PARTICIPATING, \core\event\base::LEVEL_TEACHING), SQL_PARAMS_NAMED, 'edulevel');
 465              $joins[] = "edulevel ".$edulevelsql;
 466              $params = array_merge($params, $edulevelparams);
 467          }
 468  
 469          // Origin.
 470          if (isset($this->filterparams->origin) && ($this->filterparams->origin != '')) {
 471              if ($this->filterparams->origin !== '---') {
 472                  // Filter by a single origin.
 473                  $joins[] = "origin = :origin";
 474                  $params['origin'] = $this->filterparams->origin;
 475              } else {
 476                  // Filter by everything else.
 477                  list($originsql, $originparams) = $DB->get_in_or_equal(array('cli', 'restore', 'ws', 'web'),
 478                      SQL_PARAMS_NAMED, 'origin', false);
 479                  $joins[] = "origin " . $originsql;
 480                  $params = array_merge($params, $originparams);
 481              }
 482          }
 483  
 484          // Filter out anonymous actions, this is N/A for legacy log because it never stores them.
 485          if ($this->filterparams->modid) {
 486              $context = context_module::instance($this->filterparams->modid);
 487          } else {
 488              $context = context_course::instance($this->filterparams->courseid);
 489          }
 490          if (!has_capability('moodle/site:viewanonymousevents', $context)) {
 491              $joins[] = "anonymous = 0";
 492          }
 493  
 494          $selector = implode(' AND ', $joins);
 495  
 496          if (!$this->is_downloading()) {
 497              $total = $this->filterparams->logreader->get_events_select_count($selector, $params);
 498              $this->pagesize($pagesize, $total);
 499          } else {
 500              $this->pageable(false);
 501          }
 502  
 503          // Get the users and course data.
 504          $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
 505              $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
 506  
 507          // Update list of users which will be displayed on log page.
 508          $this->update_users_used();
 509  
 510          // Get the events. Same query than before; even if it is not likely, logs from new users
 511          // may be added since last query so we will need to work around later to prevent problems.
 512          // In almost most of the cases this will be better than having two opened recordsets.
 513          $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
 514              $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
 515  
 516          // Set initial bars.
 517          if ($useinitialsbar && !$this->is_downloading()) {
 518              $this->initialbars($total > $pagesize);
 519          }
 520  
 521      }
 522  
 523      /**
 524       * Helper function to create list of course shortname and user fullname shown in log report.
 525       *
 526       * This will update $this->userfullnames and $this->courseshortnames array with userfullname and courseshortname (with link),
 527       * which will be used to render logs in table.
 528       *
 529       * @deprecated since Moodle 2.9 MDL-48595 - please do not use this function any more.
 530       */
 531      public function update_users_and_courses_used() {
 532          throw new coding_exception('update_users_and_courses_used() can not be used any more, please use update_users_used() instead.');
 533      }
 534  
 535      /**
 536       * Helper function to create list of user fullnames shown in log report.
 537       *
 538       * This will update $this->userfullnames array with userfullname,
 539       * which will be used to render logs in table.
 540       *
 541       * @since   Moodle 2.9
 542       * @return  void
 543       */
 544      protected function update_users_used() {
 545          global $DB;
 546  
 547          $this->userfullnames = array();
 548          $userids = array();
 549  
 550          // For each event cache full username.
 551          // Get list of userids which will be shown in log report.
 552          foreach ($this->rawdata as $event) {
 553              $logextra = $event->get_logextra();
 554              if (!empty($event->userid) && empty($userids[$event->userid])) {
 555                  $userids[$event->userid] = $event->userid;
 556              }
 557              if (!empty($logextra['realuserid']) && empty($userids[$logextra['realuserid']])) {
 558                  $userids[$logextra['realuserid']] = $logextra['realuserid'];
 559              }
 560              if (!empty($event->relateduserid) && empty($userids[$event->relateduserid])) {
 561                  $userids[$event->relateduserid] = $event->relateduserid;
 562              }
 563          }
 564          $this->rawdata->close();
 565  
 566          // Get user fullname and put that in return list.
 567          if (!empty($userids)) {
 568              list($usql, $uparams) = $DB->get_in_or_equal($userids);
 569              $userfieldsapi = \core_user\fields::for_name();
 570              $users = $DB->get_records_sql("SELECT id," . $userfieldsapi->get_sql('', false, '', '', false)->selects .
 571                      " FROM {user} WHERE id " . $usql,
 572                      $uparams);
 573              foreach ($users as $userid => $user) {
 574                  $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
 575                  unset($userids[$userid]);
 576              }
 577  
 578              // We fill the array with false values for the users that don't exist anymore
 579              // in the database so we don't need to query the db again later.
 580              foreach ($userids as $userid) {
 581                  $this->userfullnames[$userid] = false;
 582              }
 583          }
 584      }
 585  }