$registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOODLEORGHUBURL, 'confirmed' => 1));
// Check if there are any cache warnings.
$cachewarnings = cache_helper::warnings();
+// Check if there are events 1 API handlers.
+$eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
admin_externalpage_setup('adminnotifications');
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
- $registered, $cachewarnings);
+ $registered, $cachewarnings, $eventshandlers);
* @param array|null $availableupdates array of \core\update\info objects or null
* @param int|null $availableupdatesfetch timestamp of the most recent updates fetch or null (unknown)
* @param string[] $cachewarnings An array containing warnings from the Cache API.
+ * @param array $eventshandlers Events 1 API handlers.
*
* @return string HTML to output.
*/
public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
- $buggyiconvnomb, $registered, array $cachewarnings = array()) {
+ $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0) {
global $CFG;
$output = '';
$output .= $this->db_problems($dbproblems);
$output .= $this->maintenance_mode_warning($maintenancemode);
$output .= $this->cache_warnings($cachewarnings);
+ $output .= $this->events_handlers($eventshandlers);
$output .= $this->registration_warning($registered);
//////////////////////////////////////////////////////////////////////////////////////////////////
return join("\n", array_map(array($this, 'warning'), $cachewarnings));
}
+ /**
+ * Renders events 1 API handlers warning.
+ *
+ * @param array $eventshandlers
+ * @return string
+ */
+ public function events_handlers($eventshandlers) {
+ if ($eventshandlers) {
+ $components = '';
+ foreach ($eventshandlers as $eventhandler) {
+ $components .= $eventhandler->component . ', ';
+ }
+ $components = rtrim($components, ', ');
+ return $this->warning(get_string('eventshandlersinuse', 'admin', $components));
+ }
+ }
+
/**
* Render an appropriate message if the site in in maintenance mode.
* @param bool $maintenancemode
$string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
$string['errorsetting'] = 'Could not save setting:';
$string['errorwithsettings'] = 'Some settings were not changed due to an error.';
+$string['eventshandlersinuse'] = 'The following plugins in your system are using Events 1 API deprecated handlers: \'{$a}\'. Please, update them to use Events 2 API. See https://docs.moodle.org/dev/Event_2#Event_dispatching_and_observers.';
$string['everyonewhocan'] = 'Everyone who can \'{$a}\'';
$string['exceptions'] = 'exceptions';
$string['execpathnotallowed'] = 'Setting executable and local paths disabled in config.php';
return 0;
}
}
+
+/**
+ * Loads the events definitions for the component (from file). If no
+ * events are defined for the component, we simply return an empty array.
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
+ * @return array Array of capabilities or empty array if not exists
+ */
+function events_load_def($component) {
+ global $CFG;
+ if ($component === 'unittest') {
+ $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
+ } else {
+ $defpath = core_component::get_component_directory($component).'/db/events.php';
+ }
+
+ $handlers = array();
+
+ if (file_exists($defpath)) {
+ require($defpath);
+ }
+
+ // make sure the definitions are valid and complete; tell devs what is wrong
+ foreach ($handlers as $eventname => $handler) {
+ if ($eventname === 'reset') {
+ debugging("'reset' can not be used as event name.");
+ unset($handlers['reset']);
+ continue;
+ }
+ if (!is_array($handler)) {
+ debugging("Handler of '$eventname' must be specified as array'");
+ unset($handlers[$eventname]);
+ continue;
+ }
+ if (!isset($handler['handlerfile'])) {
+ debugging("Handler of '$eventname' must include 'handlerfile' key'");
+ unset($handlers[$eventname]);
+ continue;
+ }
+ if (!isset($handler['handlerfunction'])) {
+ debugging("Handler of '$eventname' must include 'handlerfunction' key'");
+ unset($handlers[$eventname]);
+ continue;
+ }
+ if (!isset($handler['schedule'])) {
+ $handler['schedule'] = 'instant';
+ }
+ if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
+ debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
+ unset($handlers[$eventname]);
+ continue;
+ }
+ if (!isset($handler['internal'])) {
+ $handler['internal'] = 1;
+ }
+ $handlers[$eventname] = $handler;
+ }
+
+ return $handlers;
+}
+
+/**
+ * Puts a handler on queue
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $handler event handler object from db
+ * @param stdClass $event event data object
+ * @param string $errormessage The error message indicating the problem
+ * @return int id number of new queue handler
+ */
+function events_queue_handler($handler, $event, $errormessage) {
+ global $DB;
+
+ if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
+ debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
+ return $qhandler->id;
+ }
+
+ // make a new queue handler
+ $qhandler = new stdClass();
+ $qhandler->queuedeventid = $event->id;
+ $qhandler->handlerid = $handler->id;
+ $qhandler->errormessage = $errormessage;
+ $qhandler->timemodified = time();
+ if ($handler->schedule === 'instant' and $handler->status == 1) {
+ $qhandler->status = 1; //already one failed attempt to dispatch this event
+ } else {
+ $qhandler->status = 0;
+ }
+
+ return $DB->insert_record('events_queue_handlers', $qhandler);
+}
+
+/**
+ * trigger a single event with a specified handler
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $handler This shoudl be a row from the events_handlers table.
+ * @param stdClass $eventdata An object containing information about the event
+ * @param string $errormessage error message indicating problem
+ * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
+ */
+function events_dispatch($handler, $eventdata, &$errormessage) {
+ global $CFG;
+
+ debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+
+ $function = unserialize($handler->handlerfunction);
+
+ if (is_callable($function)) {
+ // oki, no need for includes
+
+ } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
+ include_once($CFG->dirroot.$handler->handlerfile);
+
+ } else {
+ $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
+ return null;
+ }
+
+ // checks for handler validity
+ if (is_callable($function)) {
+ $result = call_user_func($function, $eventdata);
+ if ($result === false) {
+ $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
+ return false;
+ }
+ return true;
+
+ } else {
+ $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
+ return null;
+ }
+}
+
+/**
+ * given a queued handler, call the respective event handler to process the event
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $qhandler events_queued_handler row from db
+ * @return boolean true means event processed, false means retry later, NULL means fatal failure
+ */
+function events_process_queued_handler($qhandler) {
+ global $DB;
+
+ // get handler
+ if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
+ debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
+ //irrecoverable error, remove broken queue handler
+ events_dequeue($qhandler);
+ return NULL;
+ }
+
+ // get event object
+ if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
+ // can't proceed with no event object - might happen when two crons running at the same time
+ debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
+ //irrecoverable error, remove broken queue handler
+ events_dequeue($qhandler);
+ return NULL;
+ }
+
+ // call the function specified by the handler
+ try {
+ $errormessage = 'Unknown error';
+ if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
+ //everything ok
+ events_dequeue($qhandler);
+ return true;
+ }
+ } catch (Exception $e) {
+ // the problem here is that we do not want one broken handler to stop all others,
+ // cron handlers are very tricky because the needed data might have been deleted before the cron execution
+ $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
+ $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
+ if (!empty($e->debuginfo)) {
+ $errormessage .= $e->debuginfo;
+ }
+ }
+
+ //dispatching failed
+ $qh = new stdClass();
+ $qh->id = $qhandler->id;
+ $qh->errormessage = $errormessage;
+ $qh->timemodified = time();
+ $qh->status = $qhandler->status + 1;
+ $DB->update_record('events_queue_handlers', $qh);
+
+ debugging($errormessage);
+
+ return false;
+}
+
+/**
+ * Updates all of the event definitions within the database.
+ *
+ * Unfortunately this isn't as simple as removing them all and then readding
+ * the updated event definitions. Chances are queued items are referencing the
+ * existing definitions.
+ *
+ * Note that the absence of the db/events.php event definition file
+ * will cause any queued events for the component to be removed from
+ * the database.
+ *
+ * @category event
+ * @deprecated since Moodle 3.1
+ * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
+ * @return boolean always returns true
+ */
+function events_update_definition($component='moodle') {
+ global $DB;
+
+ // load event definition from events.php
+ $filehandlers = events_load_def($component);
+
+ if ($filehandlers) {
+ debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+ }
+
+ // load event definitions from db tables
+ // if we detect an event being already stored, we discard from this array later
+ // the remaining needs to be removed
+ $cachedhandlers = events_get_cached($component);
+
+ foreach ($filehandlers as $eventname => $filehandler) {
+ if (!empty($cachedhandlers[$eventname])) {
+ if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
+ $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
+ $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
+ $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
+ // exact same event handler already present in db, ignore this entry
+
+ unset($cachedhandlers[$eventname]);
+ continue;
+
+ } else {
+ // same event name matches, this event has been updated, update the datebase
+ $handler = new stdClass();
+ $handler->id = $cachedhandlers[$eventname]['id'];
+ $handler->handlerfile = $filehandler['handlerfile'];
+ $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
+ $handler->schedule = $filehandler['schedule'];
+ $handler->internal = $filehandler['internal'];
+
+ $DB->update_record('events_handlers', $handler);
+
+ unset($cachedhandlers[$eventname]);
+ continue;
+ }
+
+ } else {
+ // if we are here, this event handler is not present in db (new)
+ // add it
+ $handler = new stdClass();
+ $handler->eventname = $eventname;
+ $handler->component = $component;
+ $handler->handlerfile = $filehandler['handlerfile'];
+ $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
+ $handler->schedule = $filehandler['schedule'];
+ $handler->status = 0;
+ $handler->internal = $filehandler['internal'];
+
+ $DB->insert_record('events_handlers', $handler);
+ }
+ }
+
+ // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
+ // and should be removed, delete from db
+ events_cleanup($component, $cachedhandlers);
+
+ events_get_handlers('reset');
+
+ return true;
+}
+
+/**
+ * Events cron will try to empty the events queue by processing all the queued events handlers
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @category event
+ * @param string $eventname empty means all
+ * @return int number of dispatched events
+ */
+function events_cron($eventname='') {
+ global $DB;
+
+ $failed = array();
+ $processed = 0;
+
+ if ($eventname) {
+ $sql = "SELECT qh.*
+ FROM {events_queue_handlers} qh, {events_handlers} h
+ WHERE qh.handlerid = h.id AND h.eventname=?
+ ORDER BY qh.id";
+ $params = array($eventname);
+ } else {
+ $sql = "SELECT *
+ FROM {events_queue_handlers}
+ ORDER BY id";
+ $params = array();
+ }
+
+ $rs = $DB->get_recordset_sql($sql, $params);
+ if ($rs->valid()) {
+ debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+ }
+
+ foreach ($rs as $qhandler) {
+ if (isset($failed[$qhandler->handlerid])) {
+ // do not try to dispatch any later events when one already asked for retry or ended with exception
+ continue;
+ }
+ $status = events_process_queued_handler($qhandler);
+ if ($status === false) {
+ // handler is asking for retry, do not send other events to this handler now
+ $failed[$qhandler->handlerid] = $qhandler->handlerid;
+ } else if ($status === NULL) {
+ // means completely broken handler, event data was purged
+ $failed[$qhandler->handlerid] = $qhandler->handlerid;
+ } else {
+ $processed++;
+ }
+ }
+ $rs->close();
+
+ // remove events that do not have any handlers waiting
+ $sql = "SELECT eq.id
+ FROM {events_queue} eq
+ LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
+ WHERE qh.id IS NULL";
+ $rs = $DB->get_recordset_sql($sql);
+ foreach ($rs as $event) {
+ //debugging('Purging stale event '.$event->id);
+ $DB->delete_records('events_queue', array('id'=>$event->id));
+ }
+ $rs->close();
+
+ return $processed;
+}
+
+/**
+ * Do not call directly, this is intended to be used from new event base only.
+ *
+ * @private
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @param mixed $eventdata event data object
+ * @return int number of failed events
+ */
+function events_trigger_legacy($eventname, $eventdata) {
+ global $CFG, $USER, $DB;
+
+ $failedcount = 0; // number of failed events.
+
+ // pull out all registered event handlers
+ if ($handlers = events_get_handlers($eventname)) {
+ foreach ($handlers as $handler) {
+ $errormessage = '';
+
+ if ($handler->schedule === 'instant') {
+ if ($handler->status) {
+ //check if previous pending events processed
+ if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
+ // ok, queue is empty, lets reset the status back to 0 == ok
+ $handler->status = 0;
+ $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
+ // reset static handler cache
+ events_get_handlers('reset');
+ }
+ }
+
+ // dispatch the event only if instant schedule and status ok
+ if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
+ // increment the error status counter
+ $handler->status++;
+ $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
+ // reset static handler cache
+ events_get_handlers('reset');
+
+ } else {
+ $errormessage = 'Unknown error';
+ $result = events_dispatch($handler, $eventdata, $errormessage);
+ if ($result === true) {
+ // everything is fine - event dispatched
+ continue;
+ } else if ($result === false) {
+ // retry later - set error count to 1 == send next instant into cron queue
+ $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
+ // reset static handler cache
+ events_get_handlers('reset');
+ } else {
+ // internal problem - ignore the event completely
+ $failedcount ++;
+ continue;
+ }
+ }
+
+ // update the failed counter
+ $failedcount ++;
+
+ } else if ($handler->schedule === 'cron') {
+ //ok - use queueing of events only
+
+ } else {
+ // unknown schedule - ignore event completely
+ debugging("Unknown handler schedule type: $handler->schedule");
+ $failedcount ++;
+ continue;
+ }
+
+ // if even type is not instant, or dispatch asked for retry, queue it
+ $event = new stdClass();
+ $event->userid = $USER->id;
+ $event->eventdata = base64_encode(serialize($eventdata));
+ $event->timecreated = time();
+ if (debugging()) {
+ $dump = '';
+ $callers = debug_backtrace();
+ foreach ($callers as $caller) {
+ if (!isset($caller['line'])) {
+ $caller['line'] = '?';
+ }
+ if (!isset($caller['file'])) {
+ $caller['file'] = '?';
+ }
+ $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
+ if (isset($caller['function'])) {
+ $dump .= ': call to ';
+ if (isset($caller['class'])) {
+ $dump .= $caller['class'] . $caller['type'];
+ }
+ $dump .= $caller['function'] . '()';
+ }
+ $dump .= "\n";
+ }
+ $event->stackdump = $dump;
+ } else {
+ $event->stackdump = '';
+ }
+ $event->id = $DB->insert_record('events_queue', $event);
+ events_queue_handler($handler, $event, $errormessage);
+ }
+ } else {
+ // No handler found for this event name - this is ok!
+ }
+
+ return $failedcount;
+}
+
+/**
+ * checks if an event is registered for this component
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @param string $component component name, can be mod/data or moodle
+ * @return bool
+ */
+function events_is_registered($eventname, $component) {
+ global $DB;
+
+ debugging('events_is_registered() has been deprecated along with all Events 1 API in favour of Events 2 API,' .
+ ' please use it instead.', DEBUG_DEVELOPER);
+
+ return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
+}
+
+/**
+ * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @return int number of queued events
+ */
+function events_pending_count($eventname) {
+ global $DB;
+
+ debugging('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2 API,' .
+ ' please use it instead.', DEBUG_DEVELOPER);
+
+ $sql = "SELECT COUNT('x')
+ FROM {events_queue_handlers} qh
+ JOIN {events_handlers} h ON h.id = qh.handlerid
+ WHERE h.eventname = ?";
+
+ return $DB->count_records_sql($sql, array($eventname));
+}
defined('MOODLE_INTERNAL') || die();
-/**
- * Loads the events definitions for the component (from file). If no
- * events are defined for the component, we simply return an empty array.
- *
- * @access protected To be used from eventslib only
- *
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @return array Array of capabilities or empty array if not exists
- */
-function events_load_def($component) {
- global $CFG;
- if ($component === 'unittest') {
- $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
- } else {
- $defpath = core_component::get_component_directory($component).'/db/events.php';
- }
-
- $handlers = array();
-
- if (file_exists($defpath)) {
- require($defpath);
- }
-
- // make sure the definitions are valid and complete; tell devs what is wrong
- foreach ($handlers as $eventname => $handler) {
- if ($eventname === 'reset') {
- debugging("'reset' can not be used as event name.");
- unset($handlers['reset']);
- continue;
- }
- if (!is_array($handler)) {
- debugging("Handler of '$eventname' must be specified as array'");
- unset($handlers[$eventname]);
- continue;
- }
- if (!isset($handler['handlerfile'])) {
- debugging("Handler of '$eventname' must include 'handlerfile' key'");
- unset($handlers[$eventname]);
- continue;
- }
- if (!isset($handler['handlerfunction'])) {
- debugging("Handler of '$eventname' must include 'handlerfunction' key'");
- unset($handlers[$eventname]);
- continue;
- }
- if (!isset($handler['schedule'])) {
- $handler['schedule'] = 'instant';
- }
- if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
- debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
- unset($handlers[$eventname]);
- continue;
- }
- if (!isset($handler['internal'])) {
- $handler['internal'] = 1;
- }
- $handlers[$eventname] = $handler;
- }
-
- return $handlers;
-}
-
/**
* Gets the capabilities that have been cached in the database for this
* component.
return $cachedhandlers;
}
-/**
- * Updates all of the event definitions within the database.
- *
- * Unfortunately this isn't as simple as removing them all and then readding
- * the updated event definitions. Chances are queued items are referencing the
- * existing definitions.
- *
- * Note that the absence of the db/events.php event definition file
- * will cause any queued events for the component to be removed from
- * the database.
- *
- * @category event
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @return boolean always returns true
- */
-function events_update_definition($component='moodle') {
- global $DB;
-
- // load event definition from events.php
- $filehandlers = events_load_def($component);
-
- // load event definitions from db tables
- // if we detect an event being already stored, we discard from this array later
- // the remaining needs to be removed
- $cachedhandlers = events_get_cached($component);
-
- foreach ($filehandlers as $eventname => $filehandler) {
- if (!empty($cachedhandlers[$eventname])) {
- if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
- $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
- $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
- $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
- // exact same event handler already present in db, ignore this entry
-
- unset($cachedhandlers[$eventname]);
- continue;
-
- } else {
- // same event name matches, this event has been updated, update the datebase
- $handler = new stdClass();
- $handler->id = $cachedhandlers[$eventname]['id'];
- $handler->handlerfile = $filehandler['handlerfile'];
- $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
- $handler->schedule = $filehandler['schedule'];
- $handler->internal = $filehandler['internal'];
-
- $DB->update_record('events_handlers', $handler);
-
- unset($cachedhandlers[$eventname]);
- continue;
- }
-
- } else {
- // if we are here, this event handler is not present in db (new)
- // add it
- $handler = new stdClass();
- $handler->eventname = $eventname;
- $handler->component = $component;
- $handler->handlerfile = $filehandler['handlerfile'];
- $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
- $handler->schedule = $filehandler['schedule'];
- $handler->status = 0;
- $handler->internal = $filehandler['internal'];
-
- $DB->insert_record('events_handlers', $handler);
- }
- }
-
- // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
- // and should be removed, delete from db
- events_cleanup($component, $cachedhandlers);
-
- events_get_handlers('reset');
-
- return true;
-}
-
/**
* Remove all event handlers and queued events
*
return $deletecount;
}
-/****************** End of Events handler Definition code *******************/
-
-/**
- * Puts a handler on queue
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $handler event handler object from db
- * @param stdClass $event event data object
- * @param string $errormessage The error message indicating the problem
- * @return int id number of new queue handler
- */
-function events_queue_handler($handler, $event, $errormessage) {
- global $DB;
-
- if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
- debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
- return $qhandler->id;
- }
-
- // make a new queue handler
- $qhandler = new stdClass();
- $qhandler->queuedeventid = $event->id;
- $qhandler->handlerid = $handler->id;
- $qhandler->errormessage = $errormessage;
- $qhandler->timemodified = time();
- if ($handler->schedule === 'instant' and $handler->status == 1) {
- $qhandler->status = 1; //already one failed attempt to dispatch this event
- } else {
- $qhandler->status = 0;
- }
-
- return $DB->insert_record('events_queue_handlers', $qhandler);
-}
-
-/**
- * trigger a single event with a specified handler
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $handler This shoudl be a row from the events_handlers table.
- * @param stdClass $eventdata An object containing information about the event
- * @param string $errormessage error message indicating problem
- * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
- */
-function events_dispatch($handler, $eventdata, &$errormessage) {
- global $CFG;
-
- $function = unserialize($handler->handlerfunction);
-
- if (is_callable($function)) {
- // oki, no need for includes
-
- } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
- include_once($CFG->dirroot.$handler->handlerfile);
-
- } else {
- $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
- return null;
- }
-
- // checks for handler validity
- if (is_callable($function)) {
- $result = call_user_func($function, $eventdata);
- if ($result === false) {
- $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
- return false;
- }
- return true;
-
- } else {
- $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
- return null;
- }
-}
-
-/**
- * given a queued handler, call the respective event handler to process the event
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $qhandler events_queued_handler row from db
- * @return boolean true means event processed, false means retry later, NULL means fatal failure
- */
-function events_process_queued_handler($qhandler) {
- global $DB;
-
- // get handler
- if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
- debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
- //irrecoverable error, remove broken queue handler
- events_dequeue($qhandler);
- return NULL;
- }
-
- // get event object
- if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
- // can't proceed with no event object - might happen when two crons running at the same time
- debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
- //irrecoverable error, remove broken queue handler
- events_dequeue($qhandler);
- return NULL;
- }
-
- // call the function specified by the handler
- try {
- $errormessage = 'Unknown error';
- if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
- //everything ok
- events_dequeue($qhandler);
- return true;
- }
- } catch (Exception $e) {
- // the problem here is that we do not want one broken handler to stop all others,
- // cron handlers are very tricky because the needed data might have been deleted before the cron execution
- $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
- $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
- if (!empty($e->debuginfo)) {
- $errormessage .= $e->debuginfo;
- }
- }
-
- //dispatching failed
- $qh = new stdClass();
- $qh->id = $qhandler->id;
- $qh->errormessage = $errormessage;
- $qh->timemodified = time();
- $qh->status = $qhandler->status + 1;
- $DB->update_record('events_queue_handlers', $qh);
-
- debugging($errormessage);
-
- return false;
-}
-
/**
* Removes this queued handler from the events_queued_handler table
*
return $handlers[$eventname];
}
-
-/**
- * Events cron will try to empty the events queue by processing all the queued events handlers
- *
- * @access public Part of the public API
- * @category event
- * @param string $eventname empty means all
- * @return int number of dispatched events
- */
-function events_cron($eventname='') {
- global $DB;
-
- $failed = array();
- $processed = 0;
-
- if ($eventname) {
- $sql = "SELECT qh.*
- FROM {events_queue_handlers} qh, {events_handlers} h
- WHERE qh.handlerid = h.id AND h.eventname=?
- ORDER BY qh.id";
- $params = array($eventname);
- } else {
- $sql = "SELECT *
- FROM {events_queue_handlers}
- ORDER BY id";
- $params = array();
- }
-
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $qhandler) {
- if (isset($failed[$qhandler->handlerid])) {
- // do not try to dispatch any later events when one already asked for retry or ended with exception
- continue;
- }
- $status = events_process_queued_handler($qhandler);
- if ($status === false) {
- // handler is asking for retry, do not send other events to this handler now
- $failed[$qhandler->handlerid] = $qhandler->handlerid;
- } else if ($status === NULL) {
- // means completely broken handler, event data was purged
- $failed[$qhandler->handlerid] = $qhandler->handlerid;
- } else {
- $processed++;
- }
- }
- $rs->close();
-
- // remove events that do not have any handlers waiting
- $sql = "SELECT eq.id
- FROM {events_queue} eq
- LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
- WHERE qh.id IS NULL";
- $rs = $DB->get_recordset_sql($sql);
- foreach ($rs as $event) {
- //debugging('Purging stale event '.$event->id);
- $DB->delete_records('events_queue', array('id'=>$event->id));
- }
- $rs->close();
-
- return $processed;
-}
-
-/**
- * Do not call directly, this is intended to be used from new event base only.
- *
- * @private
- * @param string $eventname name of the event
- * @param mixed $eventdata event data object
- * @return int number of failed events
- */
-function events_trigger_legacy($eventname, $eventdata) {
- global $CFG, $USER, $DB;
-
- $failedcount = 0; // number of failed events.
-
- // pull out all registered event handlers
- if ($handlers = events_get_handlers($eventname)) {
- foreach ($handlers as $handler) {
- $errormessage = '';
-
- if ($handler->schedule === 'instant') {
- if ($handler->status) {
- //check if previous pending events processed
- if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
- // ok, queue is empty, lets reset the status back to 0 == ok
- $handler->status = 0;
- $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
- // reset static handler cache
- events_get_handlers('reset');
- }
- }
-
- // dispatch the event only if instant schedule and status ok
- if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
- // increment the error status counter
- $handler->status++;
- $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
- // reset static handler cache
- events_get_handlers('reset');
-
- } else {
- $errormessage = 'Unknown error';
- $result = events_dispatch($handler, $eventdata, $errormessage);
- if ($result === true) {
- // everything is fine - event dispatched
- continue;
- } else if ($result === false) {
- // retry later - set error count to 1 == send next instant into cron queue
- $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
- // reset static handler cache
- events_get_handlers('reset');
- } else {
- // internal problem - ignore the event completely
- $failedcount ++;
- continue;
- }
- }
-
- // update the failed counter
- $failedcount ++;
-
- } else if ($handler->schedule === 'cron') {
- //ok - use queueing of events only
-
- } else {
- // unknown schedule - ignore event completely
- debugging("Unknown handler schedule type: $handler->schedule");
- $failedcount ++;
- continue;
- }
-
- // if even type is not instant, or dispatch asked for retry, queue it
- $event = new stdClass();
- $event->userid = $USER->id;
- $event->eventdata = base64_encode(serialize($eventdata));
- $event->timecreated = time();
- if (debugging()) {
- $dump = '';
- $callers = debug_backtrace();
- foreach ($callers as $caller) {
- if (!isset($caller['line'])) {
- $caller['line'] = '?';
- }
- if (!isset($caller['file'])) {
- $caller['file'] = '?';
- }
- $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
- if (isset($caller['function'])) {
- $dump .= ': call to ';
- if (isset($caller['class'])) {
- $dump .= $caller['class'] . $caller['type'];
- }
- $dump .= $caller['function'] . '()';
- }
- $dump .= "\n";
- }
- $event->stackdump = $dump;
- } else {
- $event->stackdump = '';
- }
- $event->id = $DB->insert_record('events_queue', $event);
- events_queue_handler($handler, $event, $errormessage);
- }
- } else {
- // No handler found for this event name - this is ok!
- }
-
- return $failedcount;
-}
-
-/**
- * checks if an event is registered for this component
- *
- * @access public Part of the public API
- *
- * @param string $eventname name of the event
- * @param string $component component name, can be mod/data or moodle
- * @return bool
- */
-function events_is_registered($eventname, $component) {
- global $DB;
- return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
-}
-
-/**
- * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
- *
- * @access public Part of the public API
- *
- * @param string $eventname name of the event
- * @return int number of queued events
- */
-function events_pending_count($eventname) {
- global $DB;
-
- $sql = "SELECT COUNT('x')
- FROM {events_queue_handlers} qh
- JOIN {events_handlers} h ON h.id = qh.handlerid
- WHERE h.eventname = ?";
-
- return $DB->count_records_sql($sql, array($eventname));
-}
$this->resetDebugging();
}
+ /**
+ * Asserts how many times debugging has been called.
+ *
+ * @param int $expectedcount The expected number of times
+ * @param array $debugmessages Expected debugging messages, one for each expected message.
+ * @param array $debuglevels Expected debugging levels, one for each expected message.
+ * @param string $message
+ * @return void
+ */
+ public function assertDebuggingCalledCount($expectedcount, $debugmessages = array(), $debuglevels = array(), $message = '') {
+ if (!is_int($expectedcount)) {
+ throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
+ }
+
+ $debugging = $this->getDebuggingMessages();
+ $this->assertEquals($expectedcount, count($debugging), $message);
+
+ if ($debugmessages) {
+ if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
+ throw new coding_exception('assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages');
+ }
+ foreach ($debugmessages as $key => $debugmessage) {
+ $this->assertSame($debugmessage, $debugging[$key]->message, $message);
+ }
+ }
+
+ if ($debuglevels) {
+ if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
+ throw new coding_exception('assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages');
+ }
+ foreach ($debuglevels as $key => $debuglevel) {
+ $this->assertSame($debuglevel, $debugging[$key]->level, $message);
+ }
+ }
+
+ $this->resetDebugging();
+ }
+
/**
* Call when no debugging() messages expected.
* @param string $message
class core_event_testcase extends advanced_testcase {
+ const DEBUGGING_MSG = 'Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.';
+
public function test_event_properties() {
global $USER;
$DB->delete_records('log', array());
events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$DB->delete_records_select('events_handlers', "component <> 'unittest'");
events_get_handlers('reset');
$this->assertEquals(3, $DB->count_records('events_handlers'));
$event1 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
$event1->trigger();
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$event2 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>6, 'xx'=>11)));
$event2->nest = true;
$event2->trigger();
+ $this->assertDebuggingCalledCount(2, array(self::DEBUGGING_MSG, self::DEBUGGING_MSG), array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
$this->assertSame(
array('observe_all-5', 'observe_one-5', 'legacy_handler-0', 'observe_all-nesting-6', 'legacy_handler-0', 'observe_one-6', 'observe_all-666', 'observe_one-666', 'legacy_handler-0'),
class core_eventslib_testcase extends advanced_testcase {
+ const DEBUGGING_MSG = 'Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.';
+
/**
* Create temporary entries in the database for these tests.
* These tests have to work no matter the data currently in the database
// Set global category settings to -1 (not force).
eventslib_sample_function_handler('reset');
eventslib_sample_handler_class::static_method('reset');
- events_update_definition('unittest');
$this->resetAfterTest();
}
public function test_events_update_definition__install() {
global $DB;
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$dbcount = $DB->count_records('events_handlers', array('component'=>'unittest'));
$handlers = array();
require(__DIR__.'/fixtures/events.php');
public function test_events_update_definition__uninstall() {
global $DB;
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
events_uninstall('unittest');
$this->assertEquals(0, $DB->count_records('events_handlers', array('component'=>'unittest')), 'All handlers should be uninstalled: %s');
}
*/
public function test_events_update_definition__update() {
global $DB;
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
// First modify directly existing handler.
$handler = $DB->get_record('events_handlers', array('component'=>'unittest', 'eventname'=>'test_instant'));
// Update the definition, it should revert the handler back.
events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$handler = $DB->get_record('events_handlers', array('component'=>'unittest', 'eventname'=>'test_instant'));
$this->assertSame($handler->handlerfunction, $original, 'update should sync db with file definition: %s');
}
* Tests events_trigger_is_registered() function.
*/
public function test_events_is_registered() {
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$this->assertTrue(events_is_registered('test_instant', 'unittest'));
+ $this->assertDebuggingCalled('events_is_registered() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
}
/**
* Tests events_trigger_legacy() function.
*/
public function test_events_trigger_legacy_instant() {
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'));
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'));
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$this->assertEquals(2, eventslib_sample_function_handler('status'));
}
* Tests events_trigger_legacy() function.
*/
public function test_events_trigger__cron() {
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$this->assertEquals(0, events_trigger_legacy('test_cron', 'ok'));
$this->assertEquals(0, eventslib_sample_handler_class::static_method('status'));
events_cron('test_cron');
+ // The events_cron one + one for each triggered event above (triggered in events_dispatch).
+ $this->assertDebuggingCalledCount(2, array(self::DEBUGGING_MSG, self::DEBUGGING_MSG),
+ array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
$this->assertEquals(1, eventslib_sample_handler_class::static_method('status'));
}
* Tests events_pending_count() function.
*/
public function test_events_pending_count() {
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
events_trigger_legacy('test_cron', 'ok');
+ $this->assertDebuggingNotCalled();
events_trigger_legacy('test_cron', 'ok');
+ $this->assertDebuggingNotCalled();
events_cron('test_cron');
+ // The events_cron one + one for each triggered event above (triggered in events_dispatch).
+ $this->assertDebuggingCalledCount(3);
$this->assertEquals(0, events_pending_count('test_cron'), 'all messages should be already dequeued: %s');
+ $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
}
/**
*/
public function test_events_trigger__failed_instant() {
global $CFG;
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$olddebug = $CFG->debug;
$this->assertEquals(1, events_trigger_legacy('test_instant', 'fail'), 'fail first event: %s');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
$this->assertEquals(1, events_trigger_legacy('test_instant', 'ok'), 'this one should fail too: %s');
+ $this->assertDebuggingNotCalled();
$this->assertEquals(0, events_cron('test_instant'), 'all events should stay in queue: %s');
- $this->assertDebuggingCalled();
+ // events_cron + one for each dispatched event.
+ $this->assertDebuggingCalledCount(3);
$this->assertEquals(2, events_pending_count('test_instant'), 'two events should in queue: %s');
+ $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
+
$this->assertEquals(0, eventslib_sample_function_handler('status'), 'verify no event dispatched yet: %s');
eventslib_sample_function_handler('ignorefail'); // Ignore "fail" eventdata from now on.
$this->assertEquals(1, events_trigger_legacy('test_instant', 'ok'), 'this one should go to queue directly: %s');
+ $this->assertDebuggingNotCalled();
+
$this->assertEquals(3, events_pending_count('test_instant'), 'three events should in queue: %s');
+ $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
+
$this->assertEquals(0, eventslib_sample_function_handler('status'), 'verify previous event was not dispatched: %s');
$this->assertEquals(3, events_cron('test_instant'), 'all events should be dispatched: %s');
+ // events_cron + one for each dispatched event.
+ $this->assertDebuggingCalledCount(4);
+
$this->assertEquals(3, eventslib_sample_function_handler('status'), 'verify three events were dispatched: %s');
$this->assertEquals(0, events_pending_count('test_instant'), 'no events should in queue: %s');
+ $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
+
$this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'), 'this event should be dispatched immediately: %s');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$this->assertEquals(4, eventslib_sample_function_handler('status'), 'verify event was dispatched: %s');
$this->assertEquals(0, events_pending_count('test_instant'), 'no events should in queue: %s');
+ $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+ ' API, please use it instead.', DEBUG_DEVELOPER);
}
/**
* Tests events_trigger() function.
*/
public function test_events_trigger_debugging() {
+
+ events_update_definition('unittest');
+ $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
$this->assertEquals(0, events_trigger('test_instant', 'ok'));
- $this->assertDebuggingCalled();
+ $debugmessages = array('events_trigger() is deprecated, please use new events instead', self::DEBUGGING_MSG);
+ $this->assertDebuggingCalledCount(2, $debugmessages, array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
}
}
* groups_delete_group_members() $showfeedback parameter has been removed and is no longer
respected. Users of this function should output their own feedback if required.
* Number of changes to Tags API, see tag/upgrade.txt for more details
+* The previous events API handlers are being deprecated in favour of events 2 API, debugging messages are being displayed if
+ there are 3rd party plugins using it. Switch to events 2 API please, see https://docs.moodle.org/dev/Event_2#Event_dispatching_and_observers
+ Note than you will need to bump the plugin version so moodle is aware that you removed the plugin's event handlers.
=== 3.0 ===