87ec286c251c2e2969cecc4311dbb8dc8077f4a3
[moodle.git] / lib / eventslib.php
1 <?php
2 /**
3  * Library of functions for events manipulation.
4  * 
5  * The public API is all at the end of this file.
6  *
7  * @author Martin Dougiamas and many others
8  * @version $Id$
9  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
10  * @package moodlecore
11  */
14 /**
15  * Loads the events definitions for the component (from file). If no
16  * events are defined for the component, we simply return an empty array.
17  * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
18  * @return array of capabilities or empty array if not exists
19  *
20  * INTERNAL - to be used from eventslib only
21  */
22 function events_load_def($component) {
23     global $CFG;
25     if ($component == 'moodle') {
26         $defpath = $CFG->libdir.'/db/events.php';
28     } else if ($component == 'unittest') {
29         $defpath = $CFG->libdir.'/simpletest/fixtures/events.php';
31     } else {
32         $compparts = explode('/', $component);
34         if ($compparts[0] == 'block') {
35             // Blocks are an exception. Blocks directory is 'blocks', and not
36             // 'block'. So we need to jump through hoops.
37             $defpath = $CFG->dirroot.'/blocks/'.$compparts[1].'/db/events.php';
39         } else if ($compparts[0] == 'format') {
40             // Similar to the above, course formats are 'format' while they
41             // are stored in 'course/format'.
42             $defpath = $CFG->dirroot.'/course/format/'.$compparts[1].'/db/events.php';
44         } else if ($compparts[0] == 'editor') {
45             $defpath = $CFG->dirroot.'/lib/editor/'.$compparts[1].'/db/events.php';
47         } else if ($compparts[0] == 'gradeimport') {
48             $defpath = $CFG->dirroot.'/grade/import/'.$compparts[1].'/db/events.php';  
49         
50         } else if ($compparts[0] == 'gradeexport') {
51             $defpath = $CFG->dirroot.'/grade/export/'.$compparts[1].'/db/events.php'; 
52         
53         } else if ($compparts[0] == 'gradereport') {
54             $defpath = $CFG->dirroot.'/grade/report/'.$compparts[1].'/db/events.php'; 
55         } else if ($compparts[0] == 'portfolio'){
56             $defpath = $CFG->dirroot.'/portfolio/type/'.$compparts[1].'/db/events.php';
57         } else {
58             $defpath = $CFG->dirroot.'/'.$component.'/db/events.php';
59         }
60     }
62     $handlers = array();
64     if (file_exists($defpath)) {
65         require($defpath);
66     }
68     return $handlers;
69 }
71 /**
72  * Gets the capabilities that have been cached in the database for this
73  * component.
74  * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
75  * @return array of events
76  *
77  * INTERNAL - to be used from eventslib only
78  */
79 function events_get_cached($component) {
80     global $DB;
82     $cachedhandlers = array();
84     if ($storedhandlers = $DB->get_records('events_handlers', array('handlermodule'=>$component))) {
85         foreach ($storedhandlers as $handler) {
86             $cachedhandlers[$handler->eventname] = array (
87                 'id'              => $handler->id,
88                 'handlerfile'     => $handler->handlerfile,
89                 'handlerfunction' => $handler->handlerfunction,
90                 'schedule'        => $handler->schedule);
91         }
92     }
94     return $cachedhandlers;
95 }
97 /**
98  * We can not removed all event handlers in table, then add them again
99  * because event handlers could be referenced by queued items
100  *
101  * Note that the absence of the db/events.php event definition file
102  * will cause any queued events for the component to be removed from
103  * the database.
104  *
105  * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
106  * @return boolean
107  */
108 function events_update_definition($component='moodle') {
109     global $DB;
111     // load event definition from events.php
112     $filehandlers = events_load_def($component);
114     // load event definitions from db tables
115     // if we detect an event being already stored, we discard from this array later
116     // the remaining needs to be removed
117     $cachedhandlers = events_get_cached($component);
119     foreach ($filehandlers as $eventname => $filehandler) {
120         if (!empty($cachedhandlers[$eventname])) {
121             if ($cachedhandlers[$eventname]['handlerfile'] == $filehandler['handlerfile'] &&
122                 $cachedhandlers[$eventname]['handlerfunction'] == serialize($filehandler['handlerfunction']) &&
123                 $cachedhandlers[$eventname]['schedule'] == $filehandler['schedule']) {
124                 // exact same event handler already present in db, ignore this entry
126                 unset($cachedhandlers[$eventname]);
127                 continue;
129             } else {
130                 // same event name matches, this event has been updated, update the datebase
131                 $handler = new object();
132                 $handler->id              = $cachedhandlers[$eventname]['id'];
133                 $handler->handlerfile     = $filehandler['handlerfile'];
134                 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
135                 $handler->schedule        = $filehandler['schedule'];
137                 $DB->update_record('events_handlers', $handler);
139                 unset($cachedhandlers[$eventname]);
140                 continue;
141             }
143         } else {
144             // if we are here, this event handler is not present in db (new)
145             // add it
146             $handler = new object();
147             $handler->eventname       = $eventname;
148             $handler->handlermodule   = $component;
149             $handler->handlerfile     = $filehandler['handlerfile'];
150             $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
151             $handler->schedule        = $filehandler['schedule'];
153             $DB->insert_record('events_handlers', $handler);
154         }
155     }
157     // clean up the left overs, the entries in cachedevents array at this points are deprecated event handlers
158     // and should be removed, delete from db
159     events_cleanup($component, $cachedhandlers);
161     return true;
164 /**
165  * Remove all event handlers and queued events
166  * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
167  */
168 function events_uninstall($component) {
169     $cachedhandlers = events_get_cached($component);
170     events_cleanup($component, $cachedhandlers);
173 /**
174  * Deletes cached events that are no longer needed by the component.
175  * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
176  * @param $chachedevents - array of the cached events definitions that will be
177  * @return int - number of deprecated capabilities that have been removed
178  *
179  * INTERNAL - to be used from eventslib only
180  */
181 function events_cleanup($component, $cachedhandlers) {
182     global $DB;
184     $deletecount = 0;
185     foreach ($cachedhandlers as $eventname => $cachedhandler) {
186         if ($qhandlers = $DB->get_records('events_queue_handlers', array('handlerid'=>$cachedhandler['id']))) {
187             debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
188             foreach ($qhandlers as $qhandler) {
189                 events_dequeue($qhandler);
190             }
191         }
192         if ($DB->delete_records('events_handlers', array('eventname'=>$eventname, 'handlermodule'=>$component))) {
193             $deletecount++;
194         }
195     }
196     return $deletecount;
199 /****************** End of Events handler Definition code *******************/
201 /**
202  * puts a handler on queue
203  * @param object handler - event handler object from db
204  * @param object eventdata - event data object
205  * @return id number of new queue handler
206  *
207  * INTERNAL - to be used from eventslib only
208  */
209 function events_queue_handler($handler, $event, $errormessage) {
210     global $DB;
212     if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
213         debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
214         return $qhandler->id;
215     }
217     // make a new queue handler
218     $qhandler = new object();
219     $qhandler->queuedeventid  = $event->id;
220     $qhandler->handlerid      = $handler->id;
221     $qhandler->errormessage   = $errormessage;
222     $qhandler->timemodified   = time();
223     if ($handler->schedule == 'instant' and $handler->status == 1) {
224         $qhandler->status     = 1; //already one failed attempt to dispatch this event
225     } else {
226         $qhandler->status     = 0;
227     }
229     return $DB->insert_record('events_queue_handlers', $qhandler);
232 /**
233  * trigger a single event with a specified handler
234  * @param handler - hander object from db
235  * @param eventdata - event dataobject
236  * @param errormessage - error message indicating problem
237  * @return bool - success or fail
238  *
239  * INTERNAL - to be used from eventslib only
240  */
241 function events_dispatch($handler, $eventdata, &$errormessage) {
242     global $CFG;
244     $function = unserialize($handler->handlerfunction);
246     if (is_callable($function)) {
247         // oki, no need for includes
249     } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
250         include_once($CFG->dirroot.$handler->handlerfile);
252     } else {
253         $errormessage = "Handler file of component $handler->handlermodule: $handler->handlerfile can not be found!";
254         return false;
255     }
257     // checks for handler validity
258     if (is_callable($function)) {
259         return call_user_func($function, $eventdata);
261     } else {
262         $errormessage = "Handler function of component $handler->handlermodule: $handler->handlerfunction not callable function or class method!";
263         return false;
264     }
267 /**
268  * given a queued handler, call the respective event handler to process the event
269  * @param object qhandler - events_queued_handler object from db
270  * @return boolean meaning success, or NULL on fatal failure
271  *
272  * INTERNAL - to be used from eventslib only
273  */
274 function events_process_queued_handler($qhandler) {
275     global $CFG, $DB;
277     // get handler
278     if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
279         debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
280         //irrecoverable error, remove broken queue handler
281         events_dequeue($qhandler);
282         return NULL;
283     }
285     // get event object
286     if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
287         // can't proceed with no event object - might happen when two crons running at the same time
288         debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
289         //irrecoverable error, remove broken queue handler
290         events_dequeue($qhandler);
291         return NULL;
292     }
294     // call the function specified by the handler
295     $errormessage = 'Unknown error';
296     if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
297         //everything ok
298         events_dequeue($qhandler);
299         return true;
301     } else {
302         //dispatching failed
303         $qh = new object();
304         $qh->id           = $qhandler->id;
305         $qh->errormessage = $errormessage;
306         $qh->timemodified = time();
307         $qh->status       = $qhandler->status + 1;
308         $DB->update_record('events_queue_handlers', $qh);
309         return false;
310     }
313 /**
314  * removes this queued handler from the events_queued_handler table
315  * removes events_queue record from events_queue if no more references to this event object exists
316  * @param object qhandler - events_queued_handler object from db
317  *
318  * INTERNAL - to be used from eventslib only
319  */
320 function events_dequeue($qhandler) {
321     global $DB;
323     // first delete the queue handler
324     $DB->delete_records('events_queue_handlers', array('id'=>$qhandler->id));
326     // if no more queued handler is pointing to the same event - delete the event too
327     if (!$DB->record_exists('events_queue_handlers', array('queuedeventid'=>$qhandler->queuedeventid))) {
328         $DB->delete_records('events_queue', array('id'=>$qhandler->queuedeventid));
329     }
332 /**
333  * Returns hanflers for given event. Uses caching for better perf.
334  * @param string $eventanme name of even or 'reset'
335  * @return mixed array of handlers or false otherwise
336  *
337  * INTERNAL - to be used from eventslib only
338  */
339 function events_get_handlers($eventname) {
340     global $DB;
341     static $handlers = array();
343     if ($eventname == 'reset') {
344         $handlers = array();
345         return false;
346     }
348     if (!array_key_exists($eventname, $handlers)) {
349         $handlers[$eventname] = $DB->get_records('events_handlers', array('eventname'=>$eventname));
350     }
352     return $handlers[$eventname];
355 /****** Public events API starts here, do not use functions above in 3rd party code ******/
358 /**
359  * Events cron will try to empty the events queue by processing all the queued events handlers
360  * @param string eventname - empty means all
361  * @return number of dispatched+removed broken events
362  *
363  * PUBLIC
364  */
365 function events_cron($eventname='') {
366     global $DB;
368     $failed = array();
369     $processed = 0;
371     if ($eventname) {
372         $sql = "SELECT qh.*
373                   FROM {events_queue_handlers} qh, {events_handlers} h
374                  WHERE qh.handlerid = h.id AND h.eventname=?
375               ORDER BY qh.id";
376         $params = array($eventname);
377     } else {
378         $sql = "SELECT *
379                   FROM {events_queue_handlers}
380               ORDER BY id";
381         $params = array();
382     }
384     if ($rs = $DB->get_recordset_sql($sql, $params)) {
385         foreach ($rs as $qhandler) {
386             if (in_array($qhandler->handlerid, $failed)) {
387                 // do not try to dispatch any later events when one already failed
388                 continue;
389             }
390             $status = events_process_queued_handler($qhandler);
391             if ($status === false) {
392                 $failed[] = $qhandler->handlerid;
393             } else {
394                 $processed++;
395             }
396         }
397         $rs->close();
398     }
399     return $processed;
403 /**
404  * Function to call all eventhandlers when triggering an event
405  * @param eventname - name of the event
406  * @param eventdata - event data object
407  * @return number of failed events
408  *
409  * PUBLIC
410  */
411 function events_trigger($eventname, $eventdata) {
412     global $CFG, $USER, $DB;
414     $failedcount = 0; // number of failed events.
415     $event = false;
417     // pull out all registered event handlers
418     if ($handlers = events_get_handlers($eventname)) {
419         foreach ($handlers as $handler) {
421            $errormessage = '';
423            if ($handler->schedule == 'instant') {
424                 if ($handler->status) {
425                     //check if previous pending events processed
426                     if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
427                         // ok, queue is empty, lets reset the status back to 0 == ok
428                         $handler->status = 0;
429                         $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
430                         // reset static handler cache
431                         events_get_handlers('reset');
432                     }
433                 }
435                 // dispatch the event only if instant schedule and status ok
436                 if (!$handler->status) {
437                     $errormessage = 'Unknown error';;
438                     if (events_dispatch($handler, $eventdata, $errormessage)) {
439                         continue;
440                     }
441                     // set error count to 1 == send next instant into cron queue
442                     $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
443                     // reset static handler cache
444                     events_get_handlers('reset');
446                 } else {
447                     // increment the error status counter
448                     $handler->status++;
449                     $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
450                     // reset static handler cache
451                     events_get_handlers('reset');
452                 }
454                 // update the failed counter
455                 $failedcount ++;
457             } else if ($handler->schedule == 'cron') {
458                 //ok - use queuing of events only
460             } else {
461                 // unknown schedule - fallback to cron type
462                 debugging("Unknown handler schedule type: $handler->schedule");
463             }
465             // if even type is not instant, or dispatch failed, queue it
466             if ($event === false) {
467                 $event = new object();
468                 $event->userid      = $USER->id;
469                 $event->eventdata   = base64_encode(serialize($eventdata));
470                 $event->timecreated = time();
471                 if (debugging()) {
472                     $dump = '';
473                     $callers = debug_backtrace();
474                     foreach ($callers as $caller) {
475                         $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
476                         if (isset($caller['function'])) {
477                             $dump .= ': call to ';
478                             if (isset($caller['class'])) {
479                                 $dump .= $caller['class'] . $caller['type'];
480                             }
481                             $dump .= $caller['function'] . '()';
482                         }
483                         $dump .= "\n";
484                     }
485                     $event->stackdump = $dump;
486                } else {
487                     $event->stackdump = '';
488                 }
489                 $event->id = $DB->insert_record('events_queue', $event);
490             }
491             events_queue_handler($handler, $event, $errormessage);
492         }
493     } else {
494         //debugging("No handler found for event: $eventname");
495     }
497     return $failedcount;
500 /**
501  * checks if an event is registered for this component
502  * @param string eventname - name of the event
503  * @param string component - component name, can be mod/data or moodle
504  * @return bool
505  *
506  * PUBLIC
507  */
508 function events_is_registered($eventname, $component) {
509     global $DB;
510     return $DB->record_exists('events_handlers', array('handlermodule'=>$component, 'eventname'=>$eventname));
513 /**
514  * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
515  * @param string eventname - name of the event
516  * @return int number of queued events
517  *
518  * PUBLIC
519  */
520 function events_pending_count($eventname) {
521     global $CFG, $DB;
523     $sql = "SELECT COUNT('x')
524               FROM {events_queue_handlers} qh, {events_handlers} h
525              WHERE qh.handlerid = h.id AND h.eventname=?";
526     return $DB->count_records_sql($sql, array($eventname));
528 ?>