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 global $DB; 112 113 if (empty($userid)) { 114 return false; 115 } 116 117 if (!empty($this->userfullnames[$userid])) { 118 return $this->userfullnames[$userid]; 119 } 120 121 // We already looked for the user and it does not exist. 122 if ($this->userfullnames[$userid] === 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 list($usql, $uparams) = $DB->get_in_or_equal($userid); 128 $sql = "SELECT id," . get_all_user_name_fields(true) . " FROM {user} WHERE id " . $usql; 129 if (!$user = $DB->get_records_sql($sql, $uparams)) { 130 return false; 131 } 132 133 $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())); 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} 607