Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / lib / eventslib.php
blob6fc8bfb0479612193d337994f5e001a78b317734
1 <?php
2 /**
3 * Library of functions for events manipulation.
4 *
5 * The public API is all at the end of this file.
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
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
20 * INTERNAL - to be used from eventslib only
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] == 'gradeimport') {
45 $defpath = $CFG->dirroot.'/grade/import/'.$compparts[1].'/db/events.php';
47 } else if ($compparts[0] == 'gradeexport') {
48 $defpath = $CFG->dirroot.'/grade/export/'.$compparts[1].'/db/events.php';
50 } else if ($compparts[0] == 'gradereport') {
51 $defpath = $CFG->dirroot.'/grade/report/'.$compparts[1].'/db/events.php';
53 } else {
54 $defpath = $CFG->dirroot.'/'.$component.'/db/events.php';
58 $handlers = array();
60 if (file_exists($defpath)) {
61 require($defpath);
64 return $handlers;
67 /**
68 * Gets the capabilities that have been cached in the database for this
69 * component.
70 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
71 * @return array of events
73 * INTERNAL - to be used from eventslib only
75 function events_get_cached($component) {
76 $cachedhandlers = array();
78 if ($storedhandlers = get_records('events_handlers', 'handlermodule', $component)) {
79 foreach ($storedhandlers as $handler) {
80 $cachedhandlers[$handler->eventname] = array (
81 'id' => $handler->id,
82 'handlerfile' => $handler->handlerfile,
83 'handlerfunction' => $handler->handlerfunction,
84 'schedule' => $handler->schedule);
88 return $cachedhandlers;
91 /**
92 * We can not removed all event handlers in table, then add them again
93 * because event handlers could be referenced by queued items
95 * Note that the absence of the db/events.php event definition file
96 * will cause any queued events for the component to be removed from
97 * the database.
99 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
100 * @return boolean
102 function events_update_definition($component='moodle') {
104 // load event definition from events.php
105 $filehandlers = events_load_def($component);
107 // load event definitions from db tables
108 // if we detect an event being already stored, we discard from this array later
109 // the remaining needs to be removed
110 $cachedhandlers = events_get_cached($component);
112 foreach ($filehandlers as $eventname => $filehandler) {
113 if (!empty($cachedhandlers[$eventname])) {
114 if ($cachedhandlers[$eventname]['handlerfile'] == $filehandler['handlerfile'] &&
115 $cachedhandlers[$eventname]['handlerfunction'] == serialize($filehandler['handlerfunction']) &&
116 $cachedhandlers[$eventname]['schedule'] == $filehandler['schedule']) {
117 // exact same event handler already present in db, ignore this entry
119 unset($cachedhandlers[$eventname]);
120 continue;
122 } else {
123 // same event name matches, this event has been updated, update the datebase
124 $handler = new object();
125 $handler->id = $cachedhandlers[$eventname]['id'];
126 $handler->handlerfile = $filehandler['handlerfile'];
127 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
128 $handler->schedule = $filehandler['schedule'];
130 update_record('events_handlers', $handler);
132 unset($cachedhandlers[$eventname]);
133 continue;
136 } else {
137 // if we are here, this event handler is not present in db (new)
138 // add it
139 $handler = new object();
140 $handler->eventname = $eventname;
141 $handler->handlermodule = $component;
142 $handler->handlerfile = $filehandler['handlerfile'];
143 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
144 $handler->schedule = $filehandler['schedule'];
146 insert_record('events_handlers', $handler);
150 // clean up the left overs, the entries in cachedevents array at this points are deprecated event handlers
151 // and should be removed, delete from db
152 events_cleanup($component, $cachedhandlers);
154 return true;
158 * Remove all event handlers and queued events
159 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
161 function events_uninstall($component) {
162 $cachedhandlers = events_get_cached($component);
163 events_cleanup($component, $cachedhandlers);
167 * Deletes cached events that are no longer needed by the component.
168 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
169 * @param $chachedevents - array of the cached events definitions that will be
170 * @return int - number of deprecated capabilities that have been removed
172 * INTERNAL - to be used from eventslib only
174 function events_cleanup($component, $cachedhandlers) {
175 $deletecount = 0;
176 foreach ($cachedhandlers as $eventname => $cachedhandler) {
177 if ($qhandlers = get_records('events_queue_handlers', 'handlerid', $cachedhandler['id'])) {
178 debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
179 foreach ($qhandlers as $qhandler) {
180 events_dequeue($qhandler);
183 if (delete_records('events_handlers', 'eventname', $eventname, 'handlermodule', $component)) {
184 $deletecount++;
188 // reset static handler cache
189 events_get_handlers('reset');
191 return $deletecount;
194 /****************** End of Events handler Definition code *******************/
197 * puts a handler on queue
198 * @param object handler - event handler object from db
199 * @param object eventdata - event data object
200 * @return id number of new queue handler
202 * INTERNAL - to be used from eventslib only
204 function events_queue_handler($handler, $event, $errormessage) {
206 if ($qhandler = get_record('events_queue_handlers', 'queuedeventid', $event->id, 'handlerid', $handler->id)) {
207 debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
208 return $qhandler->id;
211 // make a new queue handler
212 $qhandler = new object();
213 $qhandler->queuedeventid = $event->id;
214 $qhandler->handlerid = $handler->id;
215 $qhandler->errormessage = addslashes($errormessage);
216 $qhandler->timemodified = time();
217 if ($handler->schedule == 'instant' and $handler->status == 1) {
218 $qhandler->status = 1; //already one failed attempt to dispatch this event
219 } else {
220 $qhandler->status = 0;
223 return insert_record('events_queue_handlers', $qhandler);
227 * trigger a single event with a specified handler
228 * @param handler - hander object from db
229 * @param eventdata - event dataobject
230 * @param errormessage - error message indicating problem
231 * @return bool - success or fail
233 * INTERNAL - to be used from eventslib only
235 function events_dispatch($handler, $eventdata, &$errormessage) {
236 global $CFG;
238 $function = unserialize($handler->handlerfunction);
240 if (is_callable($function)) {
241 // oki, no need for includes
243 } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
244 include_once($CFG->dirroot.$handler->handlerfile);
246 } else {
247 $errormessage = "Handler file of component $handler->handlermodule: $handler->handlerfile can not be found!";
248 return false;
251 // checks for handler validity
252 if (is_callable($function)) {
253 return call_user_func($function, $eventdata);
255 } else {
256 $errormessage = "Handler function of component $handler->handlermodule: $handler->handlerfunction not callable function or class method!";
257 return false;
262 * given a queued handler, call the respective event handler to process the event
263 * @param object qhandler - events_queued_handler object from db
264 * @return boolean meaning success, or NULL on fatal failure
266 * INTERNAL - to be used from eventslib only
268 function events_process_queued_handler($qhandler) {
269 global $CFG;
271 // get handler
272 if (!$handler = get_record('events_handlers', 'id', $qhandler->handlerid)) {
273 debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
274 //irrecoverable error, remove broken queue handler
275 events_dequeue($qhandler);
276 return NULL;
279 // get event object
280 if (!$event = get_record('events_queue', 'id', $qhandler->queuedeventid)) {
281 // can't proceed with no event object - might happen when two crons running at the same time
282 debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
283 //irrecoverable error, remove broken queue handler
284 events_dequeue($qhandler);
285 return NULL;
288 // call the function specified by the handler
289 $errormessage = 'Unknown error';
290 if (events_dispatch($handler, unserialize($event->eventdata), $errormessage)) {
291 //everything ok
292 events_dequeue($qhandler);
293 return true;
295 } else {
296 //dispatching failed
297 $qh = new object();
298 $qh->id = $qhandler->id;
299 $qh->errormessage = addslashes($errormessage);
300 $qh->timemodified = time();
301 $qh->status = $qhandler->status + 1;
302 update_record('events_queue_handlers', $qh);
303 return false;
308 * removes this queued handler from the events_queued_handler table
309 * removes events_queue record from events_queue if no more references to this event object exists
310 * @param object qhandler - events_queued_handler object from db
312 * INTERNAL - to be used from eventslib only
314 function events_dequeue($qhandler) {
315 // first delete the queue handler
316 delete_records('events_queue_handlers', 'id', $qhandler->id);
318 // if no more queued handler is pointing to the same event - delete the event too
319 if (!record_exists('events_queue_handlers', 'queuedeventid', $qhandler->queuedeventid)) {
320 delete_records('events_queue', 'id', $qhandler->queuedeventid);
325 * Returns hanflers for given event. Uses caching for better perf.
326 * @param string $eventanme name of even or 'reset'
327 * @return mixed array of handlers or false otherwise
329 * INTERNAL - to be used from eventslib only
331 function events_get_handlers($eventname) {
332 static $handlers = array();
334 if ($eventname == 'reset') {
335 $handlers = array();
336 return false;
339 if (!array_key_exists($eventname, $handlers)) {
340 $handlers[$eventname] = get_records('events_handlers', 'eventname', $eventname);
343 return $handlers[$eventname];
346 /****** Public events API starts here, do not use functions above in 3rd party code ******/
350 * Events cron will try to empty the events queue by processing all the queued events handlers
351 * @param string eventname - empty means all
352 * @return number of dispatched+removed broken events
354 * PUBLIC
356 function events_cron($eventname='') {
357 global $CFG;
359 $failed = array();
360 $processed = 0;
362 if ($eventname) {
363 $sql = "SELECT qh.* FROM {$CFG->prefix}events_queue_handlers qh, {$CFG->prefix}events_handlers h
364 WHERE qh.handlerid = h.id AND h.eventname='$eventname'
365 ORDER BY qh.id";
366 } else {
367 $sql = "SELECT * FROM {$CFG->prefix}events_queue_handlers
368 ORDER BY id";
371 if ($rs = get_recordset_sql($sql)) {
372 while ($qhandler = rs_fetch_next_record($rs)) {
373 if (in_array($qhandler->handlerid, $failed)) {
374 // do not try to dispatch any later events when one already failed
375 continue;
377 $status = events_process_queued_handler($qhandler);
378 if ($status === false) {
379 $failed[] = $qhandler->handlerid;
380 } else {
381 $processed++;
384 rs_close($rs);
386 return $processed;
391 * Function to call all eventhandlers when triggering an event
392 * @param eventname - name of the event
393 * @param eventdata - event data object (without magic quotes)
394 * @return number of failed events
396 * PUBLIC
398 function events_trigger($eventname, $eventdata) {
399 global $CFG, $USER;
401 $failedcount = 0; // number of failed events.
402 $event = false;
404 // pull out all registered event handlers
405 if ($handlers = events_get_handlers($eventname)) {
406 foreach ($handlers as $handler) {
408 $errormessage = '';
410 if ($handler->schedule == 'instant') {
411 if ($handler->status) {
412 //check if previous pending events processed
413 if (!record_exists('events_queue_handlers', 'handlerid', $handler->id)) {
414 // ok, queue is empty, lets reset the status back to 0 == ok
415 $handler->status = 0;
416 set_field('events_handlers', 'status', 0, 'id', $handler->id);
420 // dispatch the event only if instant schedule and status ok
421 if (!$handler->status) {
422 $errormessage = 'Unknown error';;
423 if (events_dispatch($handler, $eventdata, $errormessage)) {
424 continue;
426 // set error count to 1 == send next instant into cron queue
427 set_field('events_handlers', 'status', 1, 'id', $handler->id);
429 } else {
430 // increment the error status counter
431 $handler->status++;
432 set_field('events_handlers', 'status', $handler->status, 'id', $handler->id);
435 // update the failed counter
436 $failedcount ++;
438 } else if ($handler->schedule == 'cron') {
439 //ok - use queuing of events only
441 } else {
442 // unknown schedule - fallback to cron type
443 debugging("Unknown handler schedule type: $handler->schedule");
446 // if even type is not instant, or dispatch failed, queue it
447 if ($event === false) {
448 $event = new object();
449 $event->userid = $USER->id;
450 $event->eventdata = addslashes(serialize($eventdata));
451 $event->timecreated = time();
452 if (debugging()) {
453 $dump = '';
454 $callers = debug_backtrace();
455 foreach ($callers as $caller) {
456 $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
457 if (isset($caller['function'])) {
458 $dump .= ': call to ';
459 if (isset($caller['class'])) {
460 $dump .= $caller['class'] . $caller['type'];
462 $dump .= $caller['function'] . '()';
464 $dump .= "\n";
466 $event->stackdump = addslashes($dump);
467 } else {
468 $event->stackdump = '';
470 $event->id = insert_record('events_queue', $event);
472 events_queue_handler($handler, $event, $errormessage);
474 } else {
475 //debugging("No handler found for event: $eventname");
478 return $failedcount;
482 * checks if an event is registered for this component
483 * @param string eventname - name of the event
484 * @param string component - component name, can be mod/data or moodle
485 * @return bool
487 * PUBLIC
489 function events_is_registered($eventname, $component) {
490 return record_exists('events_handlers', 'handlermodule', $component, 'eventname', $eventname);
494 * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
495 * @param string eventname - name of the event
496 * @return int number of queued events
498 * PUBLIC
500 function events_pending_count($eventname) {
501 global $CFG;
503 $sql = "SELECT COUNT(*) FROM {$CFG->prefix}events_queue_handlers qh, {$CFG->prefix}events_handlers h
504 WHERE qh.handlerid = h.id AND h.eventname='$eventname'";
505 return count_records_sql($sql);