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 25defined('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 */ 34class 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('strftimedatetime', 'core_langconfig'); 148 } else { 149 $dateformat = get_string('strftimedatetimeshort', '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} 610