bcb1d546649fa50880b60c0713dd0d09125fe811
[moodle.git] / admin / tool / log / store / legacy / classes / log / store.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  * Legacy log reader.
19  * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
20  * @todo  MDL-52805 This is to be removed in Moodle 4.0
21  *
22  * @package    logstore_legacy
23  * @copyright  2013 Petr Skoda {@link http://skodak.org}
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 namespace logstore_legacy\log;
29 defined('MOODLE_INTERNAL') || die();
31 class store implements \tool_log\log\store, \core\log\sql_reader {
32     use \tool_log\helper\store,
33         \tool_log\helper\reader;
35     /**
36      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
37      * @todo  MDL-52805 This is to be removed in Moodle 4.0
38      *
39      * @param \tool_log\log\manager $manager
40      */
41     public function __construct(\tool_log\log\manager $manager) {
42         $this->helper_setup($manager);
43     }
45     /** @var array list of db fields which needs to be replaced for legacy log query */
46     protected static $standardtolegacyfields = array(
47         'timecreated'       => 'time',
48         'courseid'          => 'course',
49         'contextinstanceid' => 'cmid',
50         'origin'            => 'ip',
51         'anonymous'         => 0,
52     );
54     /** @var string Regex to replace the crud params */
55     const CRUD_REGEX = "/(crud).*?(<>|=|!=).*?'(.*?)'/s";
57     /**
58      * This method contains mapping required for Moodle core to make legacy store compatible with other sql_reader based
59      * queries.
60      *
61      * @param string $selectwhere Select statment
62      * @param array $params params for the sql
63      * @param string $sort sort fields
64      *
65      * @return array returns an array containing the sql predicate, an array of params and sorting parameter.
66      */
67     protected static function replace_sql_legacy($selectwhere, array $params, $sort = '') {
68         // Following mapping is done to make can_delete_course() compatible with legacy store.
69         if ($selectwhere == "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since" and
70                 empty($sort)) {
71             $replace = "module = 'course' AND action = 'new' AND userid = :userid AND url = :url AND time > :since";
72             $params += array('url' => "view.php?id={$params['courseid']}");
73             return array($replace, $params, $sort);
74         }
76         // Replace db field names to make it compatible with legacy log.
77         foreach (self::$standardtolegacyfields as $from => $to) {
78             $selectwhere = str_replace($from, $to, $selectwhere);
79             if (!empty($sort)) {
80                 $sort = str_replace($from, $to, $sort);
81             }
82             if (isset($params[$from])) {
83                 $params[$to] = $params[$from];
84                 unset($params[$from]);
85             }
86         }
88         // Replace crud fields.
89         $selectwhere = preg_replace_callback("/(crud).*?(<>|=|!=).*?'(.*?)'/s", 'self::replace_crud', $selectwhere);
91         return array($selectwhere, $params, $sort);
92     }
94     /**
95      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
96      * @todo MDL-52805 This will be removed in Moodle 4.0
97      *
98      * @param  string $selectwhere
99      * @param  array  $params
100      * @param  string $sort
101      * @param  int    $limitfrom
102      * @param  int    $limitnum
103      * @return array
104      */
105     public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
106         global $DB;
108         $sort = self::tweak_sort_by_id($sort);
110         // Replace the query with hardcoded mappings required for core.
111         list($selectwhere, $params, $sort) = self::replace_sql_legacy($selectwhere, $params, $sort);
113         $records = array();
115         try {
116             // A custom report + on the fly SQL rewriting = a possible exception.
117             $records = $DB->get_recordset_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
118         } catch (\moodle_exception $ex) {
119             debugging("error converting legacy event data " . $ex->getMessage() . $ex->debuginfo, DEBUG_DEVELOPER);
120             return array();
121         }
123         $events = array();
125         foreach ($records as $data) {
126             $events[$data->id] = $this->get_log_event($data);
127         }
129         $records->close();
131         return $events;
132     }
134     /**
135      * Fetch records using given criteria returning a Traversable object.
136      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
137      * @todo MDL-52805 This will be removed in Moodle 4.0
138      *
139      * Note that the traversable object contains a moodle_recordset, so
140      * remember that is important that you call close() once you finish
141      * using it.
142      *
143      * @param string $selectwhere
144      * @param array $params
145      * @param string $sort
146      * @param int $limitfrom
147      * @param int $limitnum
148      * @return \Traversable|\core\event\base[]
149      */
150     public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
151         global $DB;
153         $sort = self::tweak_sort_by_id($sort);
155         // Replace the query with hardcoded mappings required for core.
156         list($selectwhere, $params, $sort) = self::replace_sql_legacy($selectwhere, $params, $sort);
158         try {
159             $recordset = $DB->get_recordset_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
160         } catch (\moodle_exception $ex) {
161             debugging("error converting legacy event data " . $ex->getMessage() . $ex->debuginfo, DEBUG_DEVELOPER);
162             return new \EmptyIterator;
163         }
165         return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
166     }
168     /**
169      * Returns an event from the log data.
170      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
171      * @todo MDL-52805 This will be removed in Moodle 4.0
172      *
173      * @param stdClass $data Log data
174      * @return \core\event\base
175      */
176     public function get_log_event($data) {
177         return \logstore_legacy\event\legacy_logged::restore_legacy($data);
178     }
180     /**
181      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
182      * @todo MDL-52805 This will be removed in Moodle 4.0
183      *
184      * @param  string $selectwhere
185      * @param  array  $params
186      * @return int
187      */
188     public function get_events_select_count($selectwhere, array $params) {
189         global $DB;
191         // Replace the query with hardcoded mappings required for core.
192         list($selectwhere, $params) = self::replace_sql_legacy($selectwhere, $params);
194         try {
195             return $DB->count_records_select('log', $selectwhere, $params);
196         } catch (\moodle_exception $ex) {
197             debugging("error converting legacy event data " . $ex->getMessage() . $ex->debuginfo, DEBUG_DEVELOPER);
198             return 0;
199         }
200     }
202     /**
203      * Are the new events appearing in the reader?
204      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
205      * @todo MDL-52805 This will be removed in Moodle 4.0
206      *
207      * @return bool true means new log events are being added, false means no new data will be added
208      */
209     public function is_logging() {
210         return (bool)$this->get_config('loglegacy', true);
211     }
213     /**
214      * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
215      * @todo MDL-52805 This will be removed in Moodle 4.0
216      */
217     public function dispose() {
218     }
220     /**
221      * Legacy add_to_log() code.
222      * @deprecated since Moodle 3.1 MDL-45104 - Please use supported log stores such as "standard" or "external" instead.
223      * @todo MDL-52805 This will be removed in Moodle 3.3
224      *
225      * @param    int $courseid The course id
226      * @param    string $module The module name  e.g. forum, journal, resource, course, user etc
227      * @param    string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify.
228      * @param    string $url The file and parameters used to see the results of the action
229      * @param    string $info Additional description information
230      * @param    int $cm The course_module->id if there is one
231      * @param    int|\stdClass $user If log regards $user other than $USER
232      * @param    string $ip Override the IP, should only be used for restore.
233      * @param    int $time Override the log time, should only be used for restore.
234      */
235     public function legacy_add_to_log($courseid, $module, $action, $url, $info, $cm, $user, $ip = null, $time = null) {
236         // Note that this function intentionally does not follow the normal Moodle DB access idioms.
237         // This is for a good reason: it is the most frequently used DB update function,
238         // so it has been optimised for speed.
239         global $DB, $CFG, $USER;
240         if (!$this->is_logging()) {
241             return;
242         }
244         if ($cm === '' || is_null($cm)) { // Postgres won't translate empty string to its default.
245             $cm = 0;
246         }
248         if ($user) {
249             $userid = $user;
250         } else {
251             if (\core\session\manager::is_loggedinas()) { // Don't log.
252                 return;
253             }
254             $userid = empty($USER->id) ? '0' : $USER->id;
255         }
257         if (isset($CFG->logguests) and !$CFG->logguests) {
258             if (!$userid or isguestuser($userid)) {
259                 return;
260             }
261         }
263         $remoteaddr = (is_null($ip)) ? getremoteaddr() : $ip;
265         $timenow = (is_null($time)) ? time() : $time;
266         if (!empty($url)) { // Could break doing html_entity_decode on an empty var.
267             $url = html_entity_decode($url, ENT_QUOTES, 'UTF-8');
268         } else {
269             $url = '';
270         }
272         // Restrict length of log lines to the space actually available in the
273         // database so that it doesn't cause a DB error. Log a warning so that
274         // developers can avoid doing things which are likely to cause this on a
275         // routine basis.
276         if (\core_text::strlen($action) > 40) {
277             $action = \core_text::substr($action, 0, 37) . '...';
278             debugging('Warning: logged very long action', DEBUG_DEVELOPER);
279         }
281         if (!empty($info) && \core_text::strlen($info) > 255) {
282             $info = \core_text::substr($info, 0, 252) . '...';
283             debugging('Warning: logged very long info', DEBUG_DEVELOPER);
284         }
286         // If the 100 field size is changed, also need to alter print_log in course/lib.php.
287         if (!empty($url) && \core_text::strlen($url) > 100) {
288             $url = \core_text::substr($url, 0, 97) . '...';
289             debugging('Warning: logged very long URL', DEBUG_DEVELOPER);
290         }
292         if (defined('MDL_PERFDB')) {
293             global $PERF;
294             $PERF->logwrites++;
295         };
297         $log = array('time' => $timenow, 'userid' => $userid, 'course' => $courseid, 'ip' => $remoteaddr,
298                      'module' => $module, 'cmid' => $cm, 'action' => $action, 'url' => $url, 'info' => $info);
300         try {
301             $DB->insert_record_raw('log', $log, false);
302         } catch (\dml_exception $e) {
303             debugging('Error: Could not insert a new entry to the Moodle log. ' . $e->errorcode, DEBUG_ALL);
305             // MDL-11893, alert $CFG->supportemail if insert into log failed.
306             if ($CFG->supportemail and empty($CFG->noemailever)) {
307                 // Function email_to_user is not usable because email_to_user tries to write to the logs table,
308                 // and this will get caught in an infinite loop, if disk is full.
309                 $site = get_site();
310                 $subject = 'Insert into log failed at your moodle site ' . $site->fullname;
311                 $message = "Insert into log table failed at " . date('l dS \of F Y h:i:s A') .
312                     ".\n It is possible that your disk is full.\n\n";
313                 $message .= "The failed query parameters are:\n\n" . var_export($log, true);
315                 $lasttime = get_config('admin', 'lastloginserterrormail');
316                 if (empty($lasttime) || time() - $lasttime > 60 * 60 * 24) { // Limit to 1 email per day.
317                     // Using email directly rather than messaging as they may not be able to log in to access a message.
318                     mail($CFG->supportemail, $subject, $message);
319                     set_config('lastloginserterrormail', time(), 'admin');
320                 }
321             }
322         }
323     }
325     /**
326      * Generate a replace string for crud related sql conditions. This function is called as callback to preg_replace_callback()
327      * on the actual sql.
328      *
329      * @param array $match matched string for the passed pattern
330      *
331      * @return string The sql string to use instead of original
332      */
333     protected static function replace_crud($match) {
334         $return = '';
335         unset($match[0]); // The first entry is the whole string.
336         foreach ($match as $m) {
337             // We hard code LIKE here because we are not worried about case sensitivity and want this to be fast.
338             switch ($m) {
339                 case 'crud' :
340                     $replace = 'action';
341                     break;
342                 case 'c' :
343                     switch ($match[2]) {
344                         case '=' :
345                             $replace = " LIKE '%add%'";
346                             break;
347                         case '!=' :
348                         case '<>' :
349                             $replace = " NOT LIKE '%add%'";
350                             break;
351                         default:
352                             $replace = '';
353                     }
354                     break;
355                 case 'r' :
356                     switch ($match[2]) {
357                         case '=' :
358                             $replace = " LIKE '%view%' OR action LIKE '%report%'";
359                             break;
360                         case '!=' :
361                         case '<>' :
362                             $replace = " NOT LIKE '%view%' AND action NOT LIKE '%report%'";
363                             break;
364                         default:
365                             $replace = '';
366                     }
367                     break;
368                 case 'u' :
369                     switch ($match[2]) {
370                         case '=' :
371                             $replace = " LIKE '%update%'";
372                             break;
373                         case '!=' :
374                         case '<>' :
375                             $replace = " NOT LIKE '%update%'";
376                             break;
377                         default:
378                             $replace = '';
379                     }
380                     break;
381                 case 'd' :
382                     switch ($match[2]) {
383                         case '=' :
384                             $replace = " LIKE '%delete%'";
385                             break;
386                         case '!=' :
387                         case '<>' :
388                             $replace = " NOT LIKE '%delete%'";
389                             break;
390                         default:
391                             $replace = '';
392                     }
393                     break;
394                 default :
395                     $replace = '';
396             }
397             $return .= $replace;
398         }
399         return $return;
400     }