MDL-9137 Fixing errors in the overview report
[moodle-pu.git] / lib / eventslib.php
blob178b9f61f4391f2ed55af393d7b5ac66364db643
1 <?php
2 /**
3 * Library of functions for events manipulation.
5 * @author Martin Dougiamas and many others
6 * @version $Id$
7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package moodlecore
9 */
12 /**
13 * Loads the events definitions for the component (from file). If no
14 * events are defined for the component, we simply return an empty array.
15 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
16 * @return array of capabilities or empty array if not exists
18 * INTERNAL - to be used from eventslib only
20 function events_load_def($component) {
21 global $CFG;
23 if ($component == 'moodle') {
24 $defpath = $CFG->libdir.'/db/events.php';
26 } else if ($component == 'unittest') {
27 $defpath = $CFG->libdir.'/simpletest/fixtures/events.php';
29 } else {
30 $compparts = explode('/', $component);
32 if ($compparts[0] == 'block') {
33 // Blocks are an exception. Blocks directory is 'blocks', and not
34 // 'block'. So we need to jump through hoops.
35 $defpath = $CFG->dirroot.'/blocks/'.$compparts[1].'/db/events.php';
37 } else if ($compparts[0] == 'format') {
38 // Similar to the above, course formats are 'format' while they
39 // are stored in 'course/format'.
40 $defpath = $CFG->dirroot.'/course/format/'.$compparts[1].'/db/events.php';
42 } else if ($compparts[0] == 'gradeimport') {
43 $defpath = $CFG->dirroot.'/grade/import/'.$compparts[1].'/db/events.php';
45 } else if ($compparts[0] == 'gradeexport') {
46 $defpath = $CFG->dirroot.'/grade/export/'.$compparts[1].'/db/events.php';
48 } else if ($compparts[0] == 'gradereport') {
49 $defpath = $CFG->dirroot.'/grade/report/'.$compparts[1].'/db/events.php';
51 } else {
52 $defpath = $CFG->dirroot.'/'.$component.'/db/events.php';
56 $handlers = array();
58 if (file_exists($defpath)) {
59 require($defpath);
62 return $handlers;
65 /**
66 * Gets the capabilities that have been cached in the database for this
67 * component.
68 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
69 * @return array of events
71 * INTERNAL - to be used from eventslib only
73 function events_get_cached($component) {
74 $cachedhandlers = array();
76 if ($storedhandlers = get_records('events_handlers', 'handlermodule', $component)) {
77 foreach ($storedhandlers as $handler) {
78 $cachedhandlers[$handler->eventname] = array (
79 'id' => $handler->id,
80 'handlerfile' => $handler->handlerfile,
81 'handlerfunction' => $handler->handlerfunction,
82 'schedule' => $handler->schedule);
86 return $cachedhandlers;
89 /**
90 * We can not removed all event handlers in table, then add them again
91 * because event handlers could be referenced by queued items
93 * Note that the absence of the db/events.php event definition file
94 * will cause any queued events for the component to be removed from
95 * the database.
97 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
98 * @return boolean
100 function events_update_definition($component='moodle') {
102 // load event definition from events.php
103 $filehandlers = events_load_def($component);
105 // load event definitions from db tables
106 // if we detect an event being already stored, we discard from this array later
107 // the remaining needs to be removed
108 $cachedhandlers = events_get_cached($component);
110 foreach ($filehandlers as $eventname => $filehandler) {
111 if (!empty($cachedhandlers[$eventname])) {
112 if ($cachedhandlers[$eventname]['handlerfile'] == $filehandler['handlerfile'] &&
113 $cachedhandlers[$eventname]['handlerfunction'] == serialize($filehandler['handlerfunction']) &&
114 $cachedhandlers[$eventname]['schedule'] == $filehandler['schedule']) {
115 // exact same event handler already present in db, ignore this entry
117 unset($cachedhandlers[$eventname]);
118 continue;
120 } else {
121 // same event name matches, this event has been updated, update the datebase
122 $handler = new object();
123 $handler->id = $cachedhandlers[$eventname]['id'];
124 $handler->handlerfile = $filehandler['handlerfile'];
125 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
126 $handler->schedule = $filehandler['schedule'];
128 update_record('events_handlers', $handler);
130 unset($cachedhandlers[$eventname]);
131 continue;
134 } else {
135 // if we are here, this event handler is not present in db (new)
136 // add it
137 $handler = new object();
138 $handler->eventname = $eventname;
139 $handler->handlermodule = $component;
140 $handler->handlerfile = $filehandler['handlerfile'];
141 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
142 $handler->schedule = $filehandler['schedule'];
144 insert_record('events_handlers', $handler);
148 // clean up the left overs, the entries in cachedevents array at this points are deprecated event handlers
149 // and should be removed, delete from db
150 events_cleanup($component, $cachedhandlers);
152 return true;
156 * Remove all event handlers and queued events
157 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
159 function events_uninstall($component) {
160 $cachedhandlers = events_get_cached($component);
161 events_cleanup($component, $cachedhandlers);
165 * Deletes cached events that are no longer needed by the component.
166 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
167 * @param $chachedevents - array of the cached events definitions that will be
168 * @return int - number of deprecated capabilities that have been removed
170 * INTERNAL - to be used from eventslib only
172 function events_cleanup($component, $cachedhandlers) {
173 $deletecount = 0;
174 foreach ($cachedhandlers as $eventname => $cachedhandler) {
175 if ($qhandlers = get_records('events_queue_handlers', 'handlerid', $cachedhandler['id'])) {
176 debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
177 foreach ($qhandlers as $qhandler) {
178 events_dequeue($qhandler);
181 if (delete_records('events_handlers', 'eventname', $eventname, 'handlermodule', $component)) {
182 $deletecount++;
185 return $deletecount;
188 /****************** End of Events handler Definition code *******************/
191 * puts a handler on queue
192 * @param object handler - event handler object from db
193 * @param object eventdata - event data object
194 * @return id number of new queue handler
196 * INTERNAL - to be used from eventslib only
198 function events_queue_handler($handler, $event, $errormessage) {
200 if ($qhandler = get_record('events_queue_handlers', 'queuedeventid', $event->id, 'handlerid', $handler->id)) {
201 debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
202 return $qhandler->id;
205 // make a new queue handler
206 $qhandler = new object();
207 $qhandler->queuedeventid = $event->id;
208 $qhandler->handlerid = $handler->id;
209 $qhandler->errormessage = addslashes($errormessage);
210 $qhandler->timemodified = time();
211 if ($handler->schedule == 'instant' and $handler->status == 1) {
212 $qhandler->status = 1; //already one failed attempt to dispatch this event
213 } else {
214 $qhandler->status = 0;
217 return insert_record('events_queue_handlers', $qhandler);
221 * trigger a single event with a specified handler
222 * @param handler - hander object from db
223 * @param eventdata - event dataobject
224 * @param errormessage - error message indicating problem
225 * @return bool - success or fail
227 * INTERNAL - to be used from eventslib only
229 function events_dispatch($handler, $eventdata, &$errormessage) {
230 global $CFG;
232 $function = unserialize($handler->handlerfunction);
234 if (is_callable($function)) {
235 // oki, no need for includes
237 } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
238 include_once($CFG->dirroot.$handler->handlerfile);
240 } else {
241 $errormessage = "Handler file of component $handler->handlermodule: $handler->handlerfile can not be found!";
242 return false;
245 // checks for handler validity
246 if (is_callable($function)) {
247 return call_user_func($function, $eventdata);
249 } else {
250 $errormessage = "Handler function of component $handler->handlermodule: $handler->handlerfunction not callable function or class method!";
251 return false;
256 * given a queued handler, call the respective event handler to process the event
257 * @param object qhandler - events_queued_handler object from db
258 * @return boolean meaning success, or NULL on fatal failure
260 * INTERNAL - to be used from eventslib only
262 function events_process_queued_handler($qhandler) {
263 global $CFG;
265 // get handler
266 if (!$handler = get_record('events_handlers', 'id', $qhandler->handlerid)) {
267 debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
268 //irrecoverable error, remove broken queue handler
269 events_dequeue($qhandler);
270 return NULL;
273 // get event object
274 if (!$event = get_record('events_queue', 'id', $qhandler->queuedeventid)) {
275 // can't proceed with no event object - might happen when two crons running at the same time
276 debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
277 //irrecoverable error, remove broken queue handler
278 events_dequeue($qhandler);
279 return NULL;
282 // call the function specified by the handler
283 $errormessage = 'Unknown error';
284 if (events_dispatch($handler, unserialize($event->eventdata), $errormessage)) {
285 //everything ok
286 events_dequeue($qhandler);
287 return true;
289 } else {
290 //dispatching failed
291 $qh = new object();
292 $qh->id = $qhandler->id;
293 $qh->errormessage = addslashes($errormessage);
294 $qh->timemodified = time();
295 $qh->status = $qhandler->status + 1;
296 update_record('events_queue_handlers', $qh);
297 return false;
302 * removes this queued handler from the events_queued_handler table
303 * removes events_queue record from events_queue if no more references to this event object exists
304 * @param object qhandler - events_queued_handler object from db
306 * INTERNAL - to be used from eventslib only
308 function events_dequeue($qhandler) {
309 // first delete the queue handler
310 delete_records('events_queue_handlers', 'id', $qhandler->id);
312 // if no more queued handler is pointing to the same event - delete the event too
313 if (!record_exists('events_queue_handlers', 'queuedeventid', $qhandler->queuedeventid)) {
314 delete_records('events_queue', 'id', $qhandler->queuedeventid);
320 /****** Public events API starts here, do not use functions above in 3rd party code ******/
324 * Events cron will try to empty the events queue by processing all the queued events handlers
325 * @param string eventname - empty means all
326 * @return number of dispatched+removed broken events
328 * PUBLIC
330 function events_cron($eventname='') {
331 global $CFG;
333 $failed = array();
334 $processed = 0;
336 if ($eventname) {
337 $sql = "SELECT qh.* FROM {$CFG->prefix}events_queue_handlers qh, {$CFG->prefix}events_handlers h
338 WHERE qh.handlerid = h.id AND h.eventname='$eventname'
339 ORDER BY qh.id";
340 } else {
341 $sql = "SELECT * FROM {$CFG->prefix}events_queue_handlers
342 ORDER BY id";
345 if ($rs = get_recordset_sql($sql)) {
346 if ($rs->RecordCount() > 0) {
347 while ($qhandler = rs_fetch_next_record($rs)) {
348 if (in_array($qhandler->handlerid, $failed)) {
349 // do not try to dispatch any later events when one already failed
350 continue;
352 $status = events_process_queued_handler($qhandler);
353 if ($status === false) {
354 $failed[] = $qhandler->handlerid;
355 } else {
356 $processed++;
360 rs_close($rs);
362 return $processed;
367 * Function to call all eventhandlers when triggering an event
368 * @param eventname - name of the event
369 * @param eventdata - event data object
370 * @return number of failed events
372 * PUBLIC
374 function events_trigger($eventname, $eventdata) {
375 global $CFG, $USER;
377 $failedcount = 0; // number of failed events.
378 $event = false;
380 // pull out all registered event handlers
381 if ($handlers = get_records('events_handlers', 'eventname', $eventname)) {
382 foreach ($handlers as $handler) {
384 $errormessage = '';
386 if ($handler->schedule == 'instant') {
387 if ($handler->status) {
388 //check if previous pending events processed
389 if (!record_exists('events_queue_handlers', 'handlerid', $handler->id)) {
390 // ok, queue is empty, lets reset the status back to 0 == ok
391 $handler->status = 0;
392 set_field('events_handlers', 'status', 0, 'id', $handler->id);
396 // dispatch the event only if instant schedule and status ok
397 if (!$handler->status) {
398 $errormessage = 'Unknown error';;
399 if (events_dispatch($handler, $eventdata, $errormessage)) {
400 continue;
402 // set error count to 1 == send next instant into cron queue
403 set_field('events_handlers', 'status', 1, 'id', $handler->id);
405 } else {
406 // increment the error status counter
407 $handler->status++;
408 set_field('events_handlers', 'status', $handler->status, 'id', $handler->id);
411 // update the failed counter
412 $failedcount ++;
414 } else if ($handler->schedule == 'cron') {
415 //ok - use queuing of events only
417 } else {
418 // unknown schedule - fallback to cron type
419 debugging("Unknown handler schedule type: $handler->schedule");
422 // if even type is not instant, or dispatch failed, queue it
423 if ($event === false) {
424 $event = new object();
425 $event->userid = $USER->id;
426 $event->eventdata = serialize($eventdata);
427 $event->timecreated = time();
428 if (debugging()) {
429 $dump = '';
430 $callers = debug_backtrace();
431 foreach ($callers as $caller) {
432 $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
433 if (isset($caller['function'])) {
434 $dump .= ': call to ';
435 if (isset($caller['class'])) {
436 $dump .= $caller['class'] . $caller['type'];
438 $dump .= $caller['function'] . '()';
440 $dump .= "\n";
442 $event->stackdump = addslashes($dump);
443 } else {
444 $event->stackdump = '';
446 $event->id = insert_record('events_queue', $event);
448 events_queue_handler($handler, $event, $errormessage);
450 } else {
451 //debugging("No handler found for event: $eventname");
454 return $failedcount;
458 * checks if an event is registered for this component
459 * @param string eventname - name of the event
460 * @param string component - component name, can be mod/data or moodle
461 * @return bool
463 * PUBLIC
465 function events_is_registered($eventname, $component) {
466 return record_exists('events_handlers', 'handlermodule', $component, 'eventname', $eventname);
470 * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
471 * @param string eventname - name of the event
472 * @return int number of queued events
474 * PUBLIC
476 function events_pending_count($eventname) {
477 global $CFG;
479 $sql = "SELECT COUNT(*) FROM {$CFG->prefix}events_queue_handlers qh, {$CFG->prefix}events_handlers h
480 WHERE qh.handlerid = h.id AND h.eventname='$eventname'";
481 return count_records_sql($sql);