Merge branch 'MDL-41933' of https://github.com/merrill-oakland/moodle
[moodle.git] / lib / eventslib.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  * Library of functions for events manipulation.
19  *
20  * The public API is all at the end of this file.
21  *
22  * @package core
23  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Loads the events definitions for the component (from file). If no
31  * events are defined for the component, we simply return an empty array.
32  *
33  * @access protected To be used from eventslib only
34  *
35  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
36  * @return array Array of capabilities or empty array if not exists
37  */
38 function events_load_def($component) {
39     global $CFG;
40     if ($component === 'unittest') {
41         $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
42     } else {
43         $defpath = core_component::get_component_directory($component).'/db/events.php';
44     }
46     $handlers = array();
48     if (file_exists($defpath)) {
49         require($defpath);
50     }
52     // make sure the definitions are valid and complete; tell devs what is wrong
53     foreach ($handlers as $eventname => $handler) {
54         if ($eventname === 'reset') {
55             debugging("'reset' can not be used as event name.");
56             unset($handlers['reset']);
57             continue;
58         }
59         if (!is_array($handler)) {
60             debugging("Handler of '$eventname' must be specified as array'");
61             unset($handlers[$eventname]);
62             continue;
63         }
64         if (!isset($handler['handlerfile'])) {
65             debugging("Handler of '$eventname' must include 'handlerfile' key'");
66             unset($handlers[$eventname]);
67             continue;
68         }
69         if (!isset($handler['handlerfunction'])) {
70             debugging("Handler of '$eventname' must include 'handlerfunction' key'");
71             unset($handlers[$eventname]);
72             continue;
73         }
74         if (!isset($handler['schedule'])) {
75             $handler['schedule'] = 'instant';
76         }
77         if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
78             debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
79             unset($handlers[$eventname]);
80             continue;
81         }
82         if (!isset($handler['internal'])) {
83             $handler['internal'] = 1;
84         }
85         $handlers[$eventname] = $handler;
86     }
88     return $handlers;
89 }
91 /**
92  * Gets the capabilities that have been cached in the database for this
93  * component.
94  *
95  * @access protected To be used from eventslib only
96  *
97  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
98  * @return array of events
99  */
100 function events_get_cached($component) {
101     global $DB;
103     $cachedhandlers = array();
105     if ($storedhandlers = $DB->get_records('events_handlers', array('component'=>$component))) {
106         foreach ($storedhandlers as $handler) {
107             $cachedhandlers[$handler->eventname] = array (
108                 'id'              => $handler->id,
109                 'handlerfile'     => $handler->handlerfile,
110                 'handlerfunction' => $handler->handlerfunction,
111                 'schedule'        => $handler->schedule,
112                 'internal'        => $handler->internal);
113         }
114     }
116     return $cachedhandlers;
119 /**
120  * Updates all of the event definitions within the database.
121  *
122  * Unfortunately this isn't as simple as removing them all and then readding
123  * the updated event definitions. Chances are queued items are referencing the
124  * existing definitions.
125  *
126  * Note that the absence of the db/events.php event definition file
127  * will cause any queued events for the component to be removed from
128  * the database.
129  *
130  * @category event
131  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
132  * @return boolean always returns true
133  */
134 function events_update_definition($component='moodle') {
135     global $DB;
137     // load event definition from events.php
138     $filehandlers = events_load_def($component);
140     // load event definitions from db tables
141     // if we detect an event being already stored, we discard from this array later
142     // the remaining needs to be removed
143     $cachedhandlers = events_get_cached($component);
145     foreach ($filehandlers as $eventname => $filehandler) {
146         if (!empty($cachedhandlers[$eventname])) {
147             if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
148                 $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
149                 $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
150                 $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
151                 // exact same event handler already present in db, ignore this entry
153                 unset($cachedhandlers[$eventname]);
154                 continue;
156             } else {
157                 // same event name matches, this event has been updated, update the datebase
158                 $handler = new stdClass();
159                 $handler->id              = $cachedhandlers[$eventname]['id'];
160                 $handler->handlerfile     = $filehandler['handlerfile'];
161                 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
162                 $handler->schedule        = $filehandler['schedule'];
163                 $handler->internal        = $filehandler['internal'];
165                 $DB->update_record('events_handlers', $handler);
167                 unset($cachedhandlers[$eventname]);
168                 continue;
169             }
171         } else {
172             // if we are here, this event handler is not present in db (new)
173             // add it
174             $handler = new stdClass();
175             $handler->eventname       = $eventname;
176             $handler->component       = $component;
177             $handler->handlerfile     = $filehandler['handlerfile'];
178             $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
179             $handler->schedule        = $filehandler['schedule'];
180             $handler->status          = 0;
181             $handler->internal        = $filehandler['internal'];
183             $DB->insert_record('events_handlers', $handler);
184         }
185     }
187     // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
188     // and should be removed, delete from db
189     events_cleanup($component, $cachedhandlers);
191     events_get_handlers('reset');
193     return true;
196 /**
197  * Remove all event handlers and queued events
198  *
199  * @category event
200  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
201  */
202 function events_uninstall($component) {
203     $cachedhandlers = events_get_cached($component);
204     events_cleanup($component, $cachedhandlers);
206     events_get_handlers('reset');
209 /**
210  * Deletes cached events that are no longer needed by the component.
211  *
212  * @access protected To be used from eventslib only
213  *
214  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
215  * @param array $cachedhandlers array of the cached events definitions that will be
216  * @return int number of unused handlers that have been removed
217  */
218 function events_cleanup($component, $cachedhandlers) {
219     global $DB;
221     $deletecount = 0;
222     foreach ($cachedhandlers as $eventname => $cachedhandler) {
223         if ($qhandlers = $DB->get_records('events_queue_handlers', array('handlerid'=>$cachedhandler['id']))) {
224             //debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
225             foreach ($qhandlers as $qhandler) {
226                 events_dequeue($qhandler);
227             }
228         }
229         $DB->delete_records('events_handlers', array('eventname'=>$eventname, 'component'=>$component));
230         $deletecount++;
231     }
233     return $deletecount;
236 /****************** End of Events handler Definition code *******************/
238 /**
239  * Puts a handler on queue
240  *
241  * @access protected To be used from eventslib only
242  *
243  * @param stdClass $handler event handler object from db
244  * @param stdClass $event event data object
245  * @param string $errormessage The error message indicating the problem
246  * @return int id number of new queue handler
247  */
248 function events_queue_handler($handler, $event, $errormessage) {
249     global $DB;
251     if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
252         debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
253         return $qhandler->id;
254     }
256     // make a new queue handler
257     $qhandler = new stdClass();
258     $qhandler->queuedeventid  = $event->id;
259     $qhandler->handlerid      = $handler->id;
260     $qhandler->errormessage   = $errormessage;
261     $qhandler->timemodified   = time();
262     if ($handler->schedule === 'instant' and $handler->status == 1) {
263         $qhandler->status     = 1; //already one failed attempt to dispatch this event
264     } else {
265         $qhandler->status     = 0;
266     }
268     return $DB->insert_record('events_queue_handlers', $qhandler);
271 /**
272  * trigger a single event with a specified handler
273  *
274  * @access protected To be used from eventslib only
275  *
276  * @param stdClass $handler This shoudl be a row from the events_handlers table.
277  * @param stdClass $eventdata An object containing information about the event
278  * @param string $errormessage error message indicating problem
279  * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
280  */
281 function events_dispatch($handler, $eventdata, &$errormessage) {
282     global $CFG;
284     $function = unserialize($handler->handlerfunction);
286     if (is_callable($function)) {
287         // oki, no need for includes
289     } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
290         include_once($CFG->dirroot.$handler->handlerfile);
292     } else {
293         $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
294         return null;
295     }
297     // checks for handler validity
298     if (is_callable($function)) {
299         $result = call_user_func($function, $eventdata);
300         if ($result === false) {
301             $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
302             return false;
303         }
304         return true;
306     } else {
307         $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
308         return null;
309     }
312 /**
313  * given a queued handler, call the respective event handler to process the event
314  *
315  * @access protected To be used from eventslib only
316  *
317  * @param stdClass $qhandler events_queued_handler row from db
318  * @return boolean true means event processed, false means retry later, NULL means fatal failure
319  */
320 function events_process_queued_handler($qhandler) {
321     global $DB;
323     // get handler
324     if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
325         debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
326         //irrecoverable error, remove broken queue handler
327         events_dequeue($qhandler);
328         return NULL;
329     }
331     // get event object
332     if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
333         // can't proceed with no event object - might happen when two crons running at the same time
334         debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
335         //irrecoverable error, remove broken queue handler
336         events_dequeue($qhandler);
337         return NULL;
338     }
340     // call the function specified by the handler
341     try {
342         $errormessage = 'Unknown error';
343         if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
344             //everything ok
345             events_dequeue($qhandler);
346             return true;
347         }
348     } catch (Exception $e) {
349         // the problem here is that we do not want one broken handler to stop all others,
350         // cron handlers are very tricky because the needed data might have been deleted before the cron execution
351         $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
352                 $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
353         if (!empty($e->debuginfo)) {
354             $errormessage .= $e->debuginfo;
355         }
356     }
358     //dispatching failed
359     $qh = new stdClass();
360     $qh->id           = $qhandler->id;
361     $qh->errormessage = $errormessage;
362     $qh->timemodified = time();
363     $qh->status       = $qhandler->status + 1;
364     $DB->update_record('events_queue_handlers', $qh);
366     return false;
369 /**
370  * Removes this queued handler from the events_queued_handler table
371  *
372  * Removes events_queue record from events_queue if no more references to this event object exists
373  *
374  * @access protected To be used from eventslib only
375  *
376  * @param stdClass $qhandler A row from the events_queued_handler table
377  */
378 function events_dequeue($qhandler) {
379     global $DB;
381     // first delete the queue handler
382     $DB->delete_records('events_queue_handlers', array('id'=>$qhandler->id));
384     // if no more queued handler is pointing to the same event - delete the event too
385     if (!$DB->record_exists('events_queue_handlers', array('queuedeventid'=>$qhandler->queuedeventid))) {
386         $DB->delete_records('events_queue', array('id'=>$qhandler->queuedeventid));
387     }
390 /**
391  * Returns handlers for given event. Uses caching for better perf.
392  *
393  * @access protected To be used from eventslib only
394  *
395  * @staticvar array $handlers
396  * @param string $eventname name of event or 'reset'
397  * @return array|false array of handlers or false otherwise
398  */
399 function events_get_handlers($eventname) {
400     global $DB;
401     static $handlers = array();
403     if ($eventname === 'reset') {
404         $handlers = array();
405         return false;
406     }
408     if (!array_key_exists($eventname, $handlers)) {
409         $handlers[$eventname] = $DB->get_records('events_handlers', array('eventname'=>$eventname));
410     }
412     return $handlers[$eventname];
415 /**
416  * Events cron will try to empty the events queue by processing all the queued events handlers
417  *
418  * @access public Part of the public API
419  * @category event
420  * @param string $eventname empty means all
421  * @return int number of dispatched events
422  */
423 function events_cron($eventname='') {
424     global $DB;
426     $failed = array();
427     $processed = 0;
429     if ($eventname) {
430         $sql = "SELECT qh.*
431                   FROM {events_queue_handlers} qh, {events_handlers} h
432                  WHERE qh.handlerid = h.id AND h.eventname=?
433               ORDER BY qh.id";
434         $params = array($eventname);
435     } else {
436         $sql = "SELECT *
437                   FROM {events_queue_handlers}
438               ORDER BY id";
439         $params = array();
440     }
442     $rs = $DB->get_recordset_sql($sql, $params);
443     foreach ($rs as $qhandler) {
444         if (isset($failed[$qhandler->handlerid])) {
445             // do not try to dispatch any later events when one already asked for retry or ended with exception
446             continue;
447         }
448         $status = events_process_queued_handler($qhandler);
449         if ($status === false) {
450             // handler is asking for retry, do not send other events to this handler now
451             $failed[$qhandler->handlerid] = $qhandler->handlerid;
452         } else if ($status === NULL) {
453             // means completely broken handler, event data was purged
454             $failed[$qhandler->handlerid] = $qhandler->handlerid;
455         } else {
456             $processed++;
457         }
458     }
459     $rs->close();
461     // remove events that do not have any handlers waiting
462     $sql = "SELECT eq.id
463               FROM {events_queue} eq
464               LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
465              WHERE qh.id IS NULL";
466     $rs = $DB->get_recordset_sql($sql);
467     foreach ($rs as $event) {
468         //debugging('Purging stale event '.$event->id);
469         $DB->delete_records('events_queue', array('id'=>$event->id));
470     }
471     $rs->close();
473     return $processed;
476 /**
477  * Do not call directly, this is intended to be used from new event base only.
478  *
479  * @private
480  * @param string $eventname name of the event
481  * @param mixed $eventdata event data object
482  * @return int number of failed events
483  */
484 function events_trigger_legacy($eventname, $eventdata) {
485     global $CFG, $USER, $DB;
487     $failedcount = 0; // number of failed events.
489     // pull out all registered event handlers
490     if ($handlers = events_get_handlers($eventname)) {
491         foreach ($handlers as $handler) {
492             $errormessage = '';
494             if ($handler->schedule === 'instant') {
495                 if ($handler->status) {
496                     //check if previous pending events processed
497                     if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
498                         // ok, queue is empty, lets reset the status back to 0 == ok
499                         $handler->status = 0;
500                         $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
501                         // reset static handler cache
502                         events_get_handlers('reset');
503                     }
504                 }
506                 // dispatch the event only if instant schedule and status ok
507                 if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
508                     // increment the error status counter
509                     $handler->status++;
510                     $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
511                     // reset static handler cache
512                     events_get_handlers('reset');
514                 } else {
515                     $errormessage = 'Unknown error';
516                     $result = events_dispatch($handler, $eventdata, $errormessage);
517                     if ($result === true) {
518                         // everything is fine - event dispatched
519                         continue;
520                     } else if ($result === false) {
521                         // retry later - set error count to 1 == send next instant into cron queue
522                         $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
523                         // reset static handler cache
524                         events_get_handlers('reset');
525                     } else {
526                         // internal problem - ignore the event completely
527                         $failedcount ++;
528                         continue;
529                     }
530                 }
532                 // update the failed counter
533                 $failedcount ++;
535             } else if ($handler->schedule === 'cron') {
536                 //ok - use queueing of events only
538             } else {
539                 // unknown schedule - ignore event completely
540                 debugging("Unknown handler schedule type: $handler->schedule");
541                 $failedcount ++;
542                 continue;
543             }
545             // if even type is not instant, or dispatch asked for retry, queue it
546             $event = new stdClass();
547             $event->userid      = $USER->id;
548             $event->eventdata   = base64_encode(serialize($eventdata));
549             $event->timecreated = time();
550             if (debugging()) {
551                 $dump = '';
552                 $callers = debug_backtrace();
553                 foreach ($callers as $caller) {
554                     if (!isset($caller['line'])) {
555                         $caller['line'] = '?';
556                     }
557                     if (!isset($caller['file'])) {
558                         $caller['file'] = '?';
559                     }
560                     $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
561                     if (isset($caller['function'])) {
562                         $dump .= ': call to ';
563                         if (isset($caller['class'])) {
564                             $dump .= $caller['class'] . $caller['type'];
565                         }
566                         $dump .= $caller['function'] . '()';
567                     }
568                     $dump .= "\n";
569                 }
570                 $event->stackdump = $dump;
571             } else {
572                 $event->stackdump = '';
573             }
574             $event->id = $DB->insert_record('events_queue', $event);
575             events_queue_handler($handler, $event, $errormessage);
576         }
577     } else {
578         // No handler found for this event name - this is ok!
579     }
581     return $failedcount;
584 /**
585  * checks if an event is registered for this component
586  *
587  * @access public Part of the public API
588  *
589  * @param string $eventname name of the event
590  * @param string $component component name, can be mod/data or moodle
591  * @return bool
592  */
593 function events_is_registered($eventname, $component) {
594     global $DB;
595     return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
598 /**
599  * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
600  *
601  * @access public Part of the public API
602  *
603  * @param string $eventname name of the event
604  * @return int number of queued events
605  */
606 function events_pending_count($eventname) {
607     global $DB;
609     $sql = "SELECT COUNT('x')
610               FROM {events_queue_handlers} qh
611               JOIN {events_handlers} h ON h.id = qh.handlerid
612              WHERE h.eventname = ?";
614     return $DB->count_records_sql($sql, array($eventname));