MDL-55367 report_log: declare log array indexes to avoid errors
[moodle.git] / report / log / locallib.php
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/>.
17 /**
18  * This file contains functions used by the log reports
19  *
20  * This files lists the functions that are used during the log report generation.
21  *
22  * @package    report_log
23  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die;
29 if (!defined('REPORT_LOG_MAX_DISPLAY')) {
30     define('REPORT_LOG_MAX_DISPLAY', 150); // days
31 }
33 require_once(__DIR__.'/lib.php');
35 /**
36  * This function is used to generate and display the log activity graph
37  *
38  * @global stdClass $CFG
39  * @param  stdClass $course course instance
40  * @param  int|stdClass    $user id/object of the user whose logs are needed
41  * @param  string $typeormode type of logs graph needed (usercourse.png/userday.png) or the mode (today, all).
42  * @param  int $date timestamp in GMT (seconds since epoch)
43  * @param  string $logreader Log reader.
44  * @return void
45  */
46 function report_log_print_graph($course, $user, $typeormode, $date=0, $logreader='') {
47     global $CFG, $OUTPUT;
49     if (!is_object($user)) {
50         $user = core_user::get_user($user);
51     }
53     $logmanager = get_log_manager();
54     $readers = $logmanager->get_readers();
56     if (empty($logreader)) {
57         $reader = reset($readers);
58     } else {
59         $reader = $readers[$logreader];
60     }
61     // If reader is not a sql_internal_table_reader and not legacy store then don't show graph.
62     if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
63         return array();
64     }
65     $coursecontext = context_course::instance($course->id);
67     $a = new stdClass();
68     $a->coursename = format_string($course->shortname, true, array('context' => $coursecontext));
69     $a->username = fullname($user, true);
71     if ($typeormode == 'today' || $typeormode == 'userday.png') {
72         $logs = report_log_usertoday_data($course, $user, $date, $logreader);
73         $title = get_string("hitsoncoursetoday", "", $a);
74     } else if ($typeormode == 'all' || $typeormode == 'usercourse.png') {
75         $logs = report_log_userall_data($course, $user, $logreader);
76         $title = get_string("hitsoncourse", "", $a);
77     }
79     if (!empty($CFG->preferlinegraphs)) {
80         $chart = new \core\chart_line();
81     } else {
82         $chart = new \core\chart_bar();
83     }
85     $series = new \core\chart_series(get_string("hits"), $logs['series']);
86     $chart->add_series($series);
87     $chart->set_title($title);
88     $chart->set_labels($logs['labels']);
89     $yaxis = $chart->get_yaxis(0, true);
90     $yaxis->set_label(get_string("hits"));
91     $yaxis->set_stepsize(max(1, round(max($logs['series']) / 10)));
93     echo $OUTPUT->render($chart);
94 }
96 /**
97  * Select all log records for a given course and user
98  *
99  * @param int $userid The id of the user as found in the 'user' table.
100  * @param int $courseid The id of the course as found in the 'course' table.
101  * @param string $coursestart unix timestamp representing course start date and time.
102  * @param string $logreader log reader to use.
103  * @return array
104  */
105 function report_log_usercourse($userid, $courseid, $coursestart, $logreader = '') {
106     global $DB;
108     $logmanager = get_log_manager();
109     $readers = $logmanager->get_readers();
110     if (empty($logreader)) {
111         $reader = reset($readers);
112     } else {
113         $reader = $readers[$logreader];
114     }
116     // If reader is not a sql_internal_table_reader and not legacy store then return.
117     if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
118         return array();
119     }
121     $coursestart = (int)$coursestart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY.
122     if ($reader instanceof logstore_legacy\log\store) {
123         $logtable = 'log';
124         $timefield = 'time';
125         $coursefield = 'course';
126         // Anonymous actions are never logged in legacy log.
127         $nonanonymous = '';
128     } else {
129         $logtable = $reader->get_internal_log_table_name();
130         $timefield = 'timecreated';
131         $coursefield = 'courseid';
132         $nonanonymous = 'AND anonymous = 0';
133     }
135     $params = array();
136     $courseselect = '';
137     if ($courseid) {
138         $courseselect = "AND $coursefield = :courseid";
139         $params['courseid'] = $courseid;
140     }
141     $params['userid'] = $userid;
142     return $DB->get_records_sql("SELECT FLOOR(($timefield - $coursestart)/" . DAYSECS . ") AS day, COUNT(*) AS num
143                                    FROM {" . $logtable . "}
144                                   WHERE userid = :userid
145                                         AND $timefield > $coursestart $courseselect $nonanonymous
146                                GROUP BY FLOOR(($timefield - $coursestart)/" . DAYSECS .")", $params);
149 /**
150  * Select all log records for a given course, user, and day
151  *
152  * @param int $userid The id of the user as found in the 'user' table.
153  * @param int $courseid The id of the course as found in the 'course' table.
154  * @param string $daystart unix timestamp of the start of the day for which the logs needs to be retrived
155  * @param string $logreader log reader to use.
156  * @return array
157  */
158 function report_log_userday($userid, $courseid, $daystart, $logreader = '') {
159     global $DB;
160     $logmanager = get_log_manager();
161     $readers = $logmanager->get_readers();
162     if (empty($logreader)) {
163         $reader = reset($readers);
164     } else {
165         $reader = $readers[$logreader];
166     }
168     // If reader is not a sql_internal_table_reader and not legacy store then return.
169     if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
170         return array();
171     }
173     $daystart = (int)$daystart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY.
175     if ($reader instanceof logstore_legacy\log\store) {
176         $logtable = 'log';
177         $timefield = 'time';
178         $coursefield = 'course';
179         // Anonymous actions are never logged in legacy log.
180         $nonanonymous = '';
181     } else {
182         $logtable = $reader->get_internal_log_table_name();
183         $timefield = 'timecreated';
184         $coursefield = 'courseid';
185         $nonanonymous = 'AND anonymous = 0';
186     }
187     $params = array('userid' => $userid);
189     $courseselect = '';
190     if ($courseid) {
191         $courseselect = "AND $coursefield = :courseid";
192         $params['courseid'] = $courseid;
193     }
194     return $DB->get_records_sql("SELECT FLOOR(($timefield - $daystart)/" . HOURSECS . ") AS hour, COUNT(*) AS num
195                                    FROM {" . $logtable . "}
196                                   WHERE userid = :userid
197                                         AND $timefield > $daystart $courseselect $nonanonymous
198                                GROUP BY FLOOR(($timefield - $daystart)/" . HOURSECS . ") ", $params);
201 /**
202  * This function is used to generate and display Mnet selector form
203  *
204  * @global stdClass $USER
205  * @global stdClass $CFG
206  * @global stdClass $SITE
207  * @global moodle_database $DB
208  * @global core_renderer $OUTPUT
209  * @global stdClass $SESSION
210  * @uses CONTEXT_SYSTEM
211  * @uses COURSE_MAX_COURSES_PER_DROPDOWN
212  * @uses CONTEXT_COURSE
213  * @uses SEPARATEGROUPS
214  * @param  int      $hostid host id
215  * @param  stdClass $course course instance
216  * @param  int      $selecteduser id of the selected user
217  * @param  string   $selecteddate Date selected
218  * @param  string   $modname course_module->id
219  * @param  string   $modid number or 'site_errors'
220  * @param  string   $modaction an action as recorded in the logs
221  * @param  int      $selectedgroup Group to display
222  * @param  int      $showcourses whether to show courses if we're over our limit.
223  * @param  int      $showusers whether to show users if we're over our limit.
224  * @param  string   $logformat Format of the logs (downloadascsv, showashtml, downloadasods, downloadasexcel)
225  * @return void
226  */
227 function report_log_print_mnet_selector_form($hostid, $course, $selecteduser=0, $selecteddate='today',
228                                  $modname="", $modid=0, $modaction='', $selectedgroup=-1, $showcourses=0, $showusers=0, $logformat='showashtml') {
230     global $USER, $CFG, $SITE, $DB, $OUTPUT, $SESSION;
231     require_once $CFG->dirroot.'/mnet/peer.php';
233     $mnet_peer = new mnet_peer();
234     $mnet_peer->set_id($hostid);
236     $sql = "SELECT DISTINCT course, hostid, coursename FROM {mnet_log}";
237     $courses = $DB->get_records_sql($sql);
238     $remotecoursecount = count($courses);
240     // first check to see if we can override showcourses and showusers
241     $numcourses = $remotecoursecount + $DB->count_records('course');
242     if ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN && !$showcourses) {
243         $showcourses = 1;
244     }
246     $sitecontext = context_system::instance();
248     // Context for remote data is always SITE
249     // Groups for remote data are always OFF
250     if ($hostid == $CFG->mnet_localhost_id) {
251         $context = context_course::instance($course->id);
253         /// Setup for group handling.
254         if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
255             $selectedgroup = -1;
256             $showgroups = false;
257         } else if ($course->groupmode) {
258             $showgroups = true;
259         } else {
260             $selectedgroup = 0;
261             $showgroups = false;
262         }
264         if ($selectedgroup === -1) {
265             if (isset($SESSION->currentgroup[$course->id])) {
266                 $selectedgroup =  $SESSION->currentgroup[$course->id];
267             } else {
268                 $selectedgroup = groups_get_all_groups($course->id, $USER->id);
269                 if (is_array($selectedgroup)) {
270                     $selectedgroup = array_shift(array_keys($selectedgroup));
271                     $SESSION->currentgroup[$course->id] = $selectedgroup;
272                 } else {
273                     $selectedgroup = 0;
274                 }
275             }
276         }
278     } else {
279         $context = $sitecontext;
280     }
282     // Get all the possible users
283     $users = array();
285     // Define limitfrom and limitnum for queries below
286     // If $showusers is enabled... don't apply limitfrom and limitnum
287     $limitfrom = empty($showusers) ? 0 : '';
288     $limitnum  = empty($showusers) ? COURSE_MAX_USERS_PER_DROPDOWN + 1 : '';
290     // If looking at a different host, we're interested in all our site users
291     if ($hostid == $CFG->mnet_localhost_id && $course->id != SITEID) {
292         $courseusers = get_enrolled_users($context, '', $selectedgroup, 'u.id, ' . get_all_user_name_fields(true, 'u'),
293                 null, $limitfrom, $limitnum);
294     } else {
295         // this may be a lot of users :-(
296         $courseusers = $DB->get_records('user', array('deleted'=>0), 'lastaccess DESC', 'id, ' . get_all_user_name_fields(true),
297                 $limitfrom, $limitnum);
298     }
300     if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$showusers) {
301         $showusers = 1;
302     }
304     if ($showusers) {
305         if ($courseusers) {
306             foreach ($courseusers as $courseuser) {
307                 $users[$courseuser->id] = fullname($courseuser, has_capability('moodle/site:viewfullnames', $context));
308             }
309         }
310         $users[$CFG->siteguest] = get_string('guestuser');
311     }
313     // Get all the hosts that have log records
314     $sql = "select distinct
315                 h.id,
316                 h.name
317             from
318                 {mnet_host} h,
319                 {mnet_log} l
320             where
321                 h.id = l.hostid
322             order by
323                 h.name";
325     if ($hosts = $DB->get_records_sql($sql)) {
326         foreach($hosts as $host) {
327             $hostarray[$host->id] = $host->name;
328         }
329     }
331     $hostarray[$CFG->mnet_localhost_id] = $SITE->fullname;
332     asort($hostarray);
334     $dropdown = array();
336     foreach($hostarray as $hostid => $name) {
337         $courses = array();
338         $sites = array();
339         if ($CFG->mnet_localhost_id == $hostid) {
340             if (has_capability('report/log:view', $sitecontext) && $showcourses) {
341                 if ($ccc = $DB->get_records("course", null, "fullname","id,shortname,fullname,category")) {
342                     foreach ($ccc as $cc) {
343                         if ($cc->id == SITEID) {
344                             $sites["$hostid/$cc->id"]   = format_string($cc->fullname).' ('.get_string('site').')';
345                         } else {
346                             $courses["$hostid/$cc->id"] = format_string(get_course_display_name_for_list($cc));
347                         }
348                     }
349                 }
350             }
351         } else {
352             if (has_capability('report/log:view', $sitecontext) && $showcourses) {
353                 $sql = "SELECT DISTINCT course, coursename FROM {mnet_log} where hostid = ?";
354                 if ($ccc = $DB->get_records_sql($sql, array($hostid))) {
355                     foreach ($ccc as $cc) {
356                         if (1 == $cc->course) { // TODO: this might be wrong - site course may have another id
357                             $sites["$hostid/$cc->course"]   = $cc->coursename.' ('.get_string('site').')';
358                         } else {
359                             $courses["$hostid/$cc->course"] = $cc->coursename;
360                         }
361                     }
362                 }
363             }
364         }
366         asort($courses);
367         $dropdown[] = array($name=>($sites + $courses));
368     }
371     $activities = array();
372     $selectedactivity = "";
374     $modinfo = get_fast_modinfo($course);
375     if (!empty($modinfo->cms)) {
376         $section = 0;
377         $thissection = array();
378         foreach ($modinfo->cms as $cm) {
379             if (!$cm->uservisible || !$cm->has_view()) {
380                 continue;
381             }
382             if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) {
383                 $activities[] = $thissection;
384                 $thissection = array();
385             }
386             $section = $cm->sectionnum;
387             $modname = strip_tags($cm->get_formatted_name());
388             if (core_text::strlen($modname) > 55) {
389                 $modname = core_text::substr($modname, 0, 50)."...";
390             }
391             if (!$cm->visible) {
392                 $modname = "(".$modname.")";
393             }
394             $key = get_section_name($course, $cm->sectionnum);
395             if (!isset($thissection[$key])) {
396                 $thissection[$key] = array();
397             }
398             $thissection[$key][$cm->id] = $modname;
400             if ($cm->id == $modid) {
401                 $selectedactivity = "$cm->id";
402             }
403         }
404         if (!empty($thissection)) {
405             $activities[] = $thissection;
406         }
407     }
409     if (has_capability('report/log:view', $sitecontext) && !$course->category) {
410         $activities["site_errors"] = get_string("siteerrors");
411         if ($modid === "site_errors") {
412             $selectedactivity = "site_errors";
413         }
414     }
416     $strftimedate = get_string("strftimedate");
417     $strftimedaydate = get_string("strftimedaydate");
419     asort($users);
421     // Prepare the list of action options.
422     $actions = array(
423         'view' => get_string('view'),
424         'add' => get_string('add'),
425         'update' => get_string('update'),
426         'delete' => get_string('delete'),
427         '-view' => get_string('allchanges')
428     );
430     // Get all the possible dates
431     // Note that we are keeping track of real (GMT) time and user time
432     // User time is only used in displays - all calcs and passing is GMT
434     $timenow = time(); // GMT
436     // What day is it now for the user, and when is midnight that day (in GMT).
437     $timemidnight = $today = usergetmidnight($timenow);
439     // Put today up the top of the list
440     $dates = array(
441         "0" => get_string('alldays'),
442         "$timemidnight" => get_string("today").", ".userdate($timenow, $strftimedate)
443     );
445     if (!$course->startdate or ($course->startdate > $timenow)) {
446         $course->startdate = $course->timecreated;
447     }
449     $numdates = 1;
450     while ($timemidnight > $course->startdate and $numdates < 365) {
451         $timemidnight = $timemidnight - 86400;
452         $timenow = $timenow - 86400;
453         $dates["$timemidnight"] = userdate($timenow, $strftimedaydate);
454         $numdates++;
455     }
457     if ($selecteddate === "today") {
458         $selecteddate = $today;
459     }
461     echo "<form class=\"logselectform\" action=\"$CFG->wwwroot/report/log/index.php\" method=\"get\">\n";
462     echo "<div>\n";//invisible fieldset here breaks wrapping
463     echo "<input type=\"hidden\" name=\"chooselog\" value=\"1\" />\n";
464     echo "<input type=\"hidden\" name=\"showusers\" value=\"$showusers\" />\n";
465     echo "<input type=\"hidden\" name=\"showcourses\" value=\"$showcourses\" />\n";
466     if (has_capability('report/log:view', $sitecontext) && $showcourses) {
467         $cid = empty($course->id)? '1' : $course->id;
468         echo html_writer::label(get_string('selectacoursesite'), 'menuhost_course', false, array('class' => 'accesshide'));
469         echo html_writer::select($dropdown, "host_course", $hostid.'/'.$cid);
470     } else {
471         $courses = array();
472         $courses[$course->id] = get_course_display_name_for_list($course) . ((empty($course->category)) ? ' ('.get_string('site').') ' : '');
473         echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide'));
474         echo html_writer::select($courses,"id",$course->id, false);
475         if (has_capability('report/log:view', $sitecontext)) {
476             $a = new stdClass();
477             $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser"
478                 ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showcourses=1&showusers=$showusers";
479             print_string('logtoomanycourses','moodle',$a);
480         }
481     }
483     if ($showgroups) {
484         if ($cgroups = groups_get_all_groups($course->id)) {
485             foreach ($cgroups as $cgroup) {
486                 $groups[$cgroup->id] = $cgroup->name;
487             }
488         }
489         else {
490             $groups = array();
491         }
492         echo html_writer::label(get_string('selectagroup'), 'menugroup', false, array('class' => 'accesshide'));
493         echo html_writer::select($groups, "group", $selectedgroup, get_string("allgroups"));
494     }
496     if ($showusers) {
497         echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide'));
498         echo html_writer::select($users, "user", $selecteduser, get_string("allparticipants"));
499     }
500     else {
501         $users = array();
502         if (!empty($selecteduser)) {
503             $user = $DB->get_record('user', array('id'=>$selecteduser));
504             $users[$selecteduser] = fullname($user);
505         }
506         else {
507             $users[0] = get_string('allparticipants');
508         }
509         echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide'));
510         echo html_writer::select($users, "user", $selecteduser, false);
511         $a = new stdClass();
512         $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser"
513             ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showusers=1&showcourses=$showcourses";
514         print_string('logtoomanyusers','moodle',$a);
515     }
517     echo html_writer::label(get_string('date'), 'menudate', false, array('class' => 'accesshide'));
518     echo html_writer::select($dates, "date", $selecteddate, false);
519     echo html_writer::label(get_string('showreports'), 'menumodid', false, array('class' => 'accesshide'));
520     echo html_writer::select($activities, "modid", $selectedactivity, get_string("allactivities"));
521     echo html_writer::label(get_string('actions'), 'menumodaction', false, array('class' => 'accesshide'));
522     echo html_writer::select($actions, 'modaction', $modaction, get_string("allactions"));
524     $logformats = array('showashtml' => get_string('displayonpage'),
525                         'downloadascsv' => get_string('downloadtext'),
526                         'downloadasods' => get_string('downloadods'),
527                         'downloadasexcel' => get_string('downloadexcel'));
528     echo html_writer::label(get_string('logsformat', 'report_log'), 'menulogformat', false, array('class' => 'accesshide'));
529     echo html_writer::select($logformats, 'logformat', $logformat, false);
530     echo '<input type="submit" value="'.get_string('gettheselogs').'" />';
531     echo '</div>';
532     echo '</form>';
535 /**
536  * Fetch logs since the start of the courses and structure in series and labels to be sent to Chart API.
537  *
538  * @param stdClass $course the course object
539  * @param stdClass $user user object
540  * @param string $logreader the log reader where the logs are.
541  * @return array structured array to be sent to chart API, split in two indexes (series and labels).
542  */
543 function report_log_userall_data($course, $user, $logreader) {
544     global $CFG;
545     $site = get_site();
546     $timenow = time();
547     $logs = [];
548     if ($course->id == $site->id) {
549         $courseselect = 0;
550     } else {
551         $courseselect = $course->id;
552     }
554     $maxseconds = REPORT_LOG_MAX_DISPLAY * 3600 * 24;  // Seconds.
555     if ($timenow - $course->startdate > $maxseconds) {
556         $course->startdate = $timenow - $maxseconds;
557     }
559     if (!empty($CFG->loglifetime)) {
560         $maxseconds = $CFG->loglifetime * 3600 * 24;  // Seconds.
561         if ($timenow - $course->startdate > $maxseconds) {
562             $course->startdate = $timenow - $maxseconds;
563         }
564     }
566     $timestart = $coursestart = usergetmidnight($course->startdate);
568     $i = 0;
569     $logs['series'][$i] = 0;
570     $logs['labels'][$i] = 0;
571     while ($timestart < $timenow) {
572         $timefinish = $timestart + 86400;
573         $logs['labels'][$i] = userdate($timestart, "%a %d %b");
574         $logs['series'][$i] = 0;
575         $i++;
576         $timestart = $timefinish;
577     }
578     $rawlogs = report_log_usercourse($user->id, $courseselect, $coursestart, $logreader);
580     foreach ($rawlogs as $rawlog) {
581         if (isset($logs['labels'][$rawlog->day])) {
582             $logs['series'][$rawlog->day] = $rawlog->num;
583         }
584     }
586     return $logs;
589 /**
590  * Fetch logs of the current day and structure in series and labels to be sent to Chart API.
591  *
592  * @param stdClass $course the course object
593  * @param stdClass $user user object
594  * @param int $date A time of a day (in GMT).
595  * @param string $logreader the log reader where the logs are.
596  * @return array $logs structured array to be sent to chart API, split in two indexes (series and labels).
597  */
598 function report_log_usertoday_data($course, $user, $date, $logreader) {
599     $site = get_site();
600     $logs = [];
602     if ($course->id == $site->id) {
603         $courseselect = 0;
604     } else {
605         $courseselect = $course->id;
606     }
608     if ($date) {
609         $daystart = usergetmidnight($date);
610     } else {
611         $daystart = usergetmidnight(time());
612     }
614     for ($i = 0; $i <= 23; $i++) {
615         $hour = $daystart + $i * 3600;
616         $logs['series'][$i] = 0;
617         $logs['labels'][$i] = userdate($hour, "%H:00");
618     }
620     $rawlogs = report_log_userday($user->id, $courseselect, $daystart, $logreader);
622     foreach ($rawlogs as $rawlog) {
623         if (isset($logs['labels'][$rawlog->hour])) {
624             $logs['series'][$rawlog->hour] = $rawlog->num;
625         }
626     }
628     return $logs;