MDL-50891 useragent: Move web crawler checks to useragent class
[moodle.git] / lib / classes / event / manager.php
blob28f9d13583f1319650942f9323e6c30bd0bf37c3
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace core\event;
19 defined('MOODLE_INTERNAL') || die();
21 /**
22 * New event manager class.
24 * @package core
25 * @since Moodle 2.6
26 * @copyright 2013 Petr Skoda {@link http://skodak.org}
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 /**
31 * Class used for event dispatching.
33 * Note: Do NOT use directly in your code, it is intended to be used from
34 * base event class only.
36 class manager {
37 /** @var array buffer of event for dispatching */
38 protected static $buffer = array();
40 /** @var array buffer for events that were not sent to external observers when DB transaction in progress */
41 protected static $extbuffer = array();
43 /** @var bool evert dispatching already in progress - prevents nesting */
44 protected static $dispatching = false;
46 /** @var array cache of all observers */
47 protected static $allobservers = null;
49 /** @var bool should we reload observers after the test? */
50 protected static $reloadaftertest = false;
52 /**
53 * Trigger new event.
55 * @internal to be used only from \core\event\base::trigger() method.
56 * @param \core\event\base $event
58 * @throws \coding_Exception if used directly.
60 public static function dispatch(\core\event\base $event) {
61 if (during_initial_install()) {
62 return;
64 if (!$event->is_triggered() or $event->is_dispatched()) {
65 throw new \coding_exception('Illegal event dispatching attempted.');
68 self::$buffer[] = $event;
70 if (self::$dispatching) {
71 return;
74 self::$dispatching = true;
75 self::process_buffers();
76 self::$dispatching = false;
79 /**
80 * Notification from DML layer.
81 * @internal to be used from DML layer only.
83 public static function database_transaction_commited() {
84 if (self::$dispatching or empty(self::$extbuffer)) {
85 return;
88 self::$dispatching = true;
89 self::process_buffers();
90 self::$dispatching = false;
93 /**
94 * Notification from DML layer.
95 * @internal to be used from DML layer only.
97 public static function database_transaction_rolledback() {
98 self::$extbuffer = array();
101 protected static function process_buffers() {
102 global $DB, $CFG;
103 self::init_all_observers();
105 while (self::$buffer or self::$extbuffer) {
107 $fromextbuffer = false;
108 $addedtoextbuffer = false;
110 if (self::$extbuffer and !$DB->is_transaction_started()) {
111 $fromextbuffer = true;
112 $event = reset(self::$extbuffer);
113 unset(self::$extbuffer[key(self::$extbuffer)]);
115 } else if (self::$buffer) {
116 $event = reset(self::$buffer);
117 unset(self::$buffer[key(self::$buffer)]);
119 } else {
120 return;
123 $observingclasses = self::get_observing_classes($event);
124 foreach ($observingclasses as $observingclass) {
125 if (!isset(self::$allobservers[$observingclass])) {
126 continue;
128 foreach (self::$allobservers[$observingclass] as $observer) {
129 if ($observer->internal) {
130 if ($fromextbuffer) {
131 // Do not send buffered external events to internal handlers,
132 // they processed them already.
133 continue;
135 } else {
136 if ($DB->is_transaction_started()) {
137 if ($fromextbuffer) {
138 // Weird!
139 continue;
141 // Do not notify external observers while in DB transaction.
142 if (!$addedtoextbuffer) {
143 self::$extbuffer[] = $event;
144 $addedtoextbuffer = true;
146 continue;
150 if (isset($observer->includefile) and file_exists($observer->includefile)) {
151 include_once($observer->includefile);
153 if (is_callable($observer->callable)) {
154 try {
155 call_user_func($observer->callable, $event);
156 } catch (\Exception $e) {
157 // Observers are notified before installation and upgrade, this may throw errors.
158 if (empty($CFG->upgraderunning)) {
159 // Ignore errors during upgrade, otherwise warn developers.
160 debugging("Exception encountered in event observer '$observer->callable': ".$e->getMessage(), DEBUG_DEVELOPER, $e->getTrace());
163 } else {
164 debugging("Can not execute event observer '$observer->callable'");
169 // TODO: Invent some infinite loop protection in case events cross-trigger one another.
174 * Returns list of classes related to this event.
175 * @param \core\event\base $event
176 * @return array
178 protected static function get_observing_classes(\core\event\base $event) {
179 $observers = array('\core\event\base', '\\'.get_class($event));
180 return $observers;
182 // Note if we ever decide to observe events by parent class name use the following code instead.
184 $classname = get_class($event);
185 $observers = array('\\'.$classname);
186 while ($classname = get_parent_class($classname)) {
187 $observers[] = '\\'.$classname;
189 $observers = array_reverse($observers, false);
191 return $observers;
196 * Initialise the list of observers.
198 protected static function init_all_observers() {
199 global $CFG;
201 if (is_array(self::$allobservers)) {
202 return;
205 if (!PHPUNIT_TEST and !during_initial_install()) {
206 $cache = \cache::make('core', 'observers');
207 $cached = $cache->get('all');
208 $dirroot = $cache->get('dirroot');
209 if ($dirroot === $CFG->dirroot and is_array($cached)) {
210 self::$allobservers = $cached;
211 return;
215 self::$allobservers = array();
217 $plugintypes = \core_component::get_plugin_types();
218 $plugintypes = array_merge(array('core' => 'not used'), $plugintypes);
219 $systemdone = false;
220 foreach ($plugintypes as $plugintype => $ignored) {
221 if ($plugintype === 'core') {
222 $plugins['core'] = "$CFG->dirroot/lib";
223 } else {
224 $plugins = \core_component::get_plugin_list($plugintype);
227 foreach ($plugins as $plugin => $fulldir) {
228 if (!file_exists("$fulldir/db/events.php")) {
229 continue;
231 $observers = null;
232 include("$fulldir/db/events.php");
233 if (!is_array($observers)) {
234 continue;
236 self::add_observers($observers, "$fulldir/db/events.php", $plugintype, $plugin);
240 self::order_all_observers();
242 if (!PHPUNIT_TEST and !during_initial_install()) {
243 $cache->set('all', self::$allobservers);
244 $cache->set('dirroot', $CFG->dirroot);
249 * Add observers.
250 * @param array $observers
251 * @param string $file
252 * @param string $plugintype Plugin type of the observer.
253 * @param string $plugin Plugin of the observer.
255 protected static function add_observers(array $observers, $file, $plugintype = null, $plugin = null) {
256 global $CFG;
258 foreach ($observers as $observer) {
259 if (empty($observer['eventname']) or !is_string($observer['eventname'])) {
260 debugging("Invalid 'eventname' detected in $file observer definition", DEBUG_DEVELOPER);
261 continue;
263 if ($observer['eventname'] === '*') {
264 $observer['eventname'] = '\core\event\base';
266 if (strpos($observer['eventname'], '\\') !== 0) {
267 $observer['eventname'] = '\\'.$observer['eventname'];
269 if (empty($observer['callback'])) {
270 debugging("Invalid 'callback' detected in $file observer definition", DEBUG_DEVELOPER);
271 continue;
273 $o = new \stdClass();
274 $o->callable = $observer['callback'];
275 if (!isset($observer['priority'])) {
276 $o->priority = 0;
277 } else {
278 $o->priority = (int)$observer['priority'];
280 if (!isset($observer['internal'])) {
281 $o->internal = true;
282 } else {
283 $o->internal = (bool)$observer['internal'];
285 if (empty($observer['includefile'])) {
286 $o->includefile = null;
287 } else {
288 if ($CFG->admin !== 'admin' and strpos($observer['includefile'], '/admin/') === 0) {
289 $observer['includefile'] = preg_replace('|^/admin/|', '/'.$CFG->admin.'/', $observer['includefile']);
291 $observer['includefile'] = $CFG->dirroot . '/' . ltrim($observer['includefile'], '/');
292 if (!file_exists($observer['includefile'])) {
293 debugging("Invalid 'includefile' detected in $file observer definition", DEBUG_DEVELOPER);
294 continue;
296 $o->includefile = $observer['includefile'];
298 $o->plugintype = $plugintype;
299 $o->plugin = $plugin;
300 self::$allobservers[$observer['eventname']][] = $o;
305 * Reorder observers to allow quick lookup of observer for each event.
307 protected static function order_all_observers() {
308 foreach (self::$allobservers as $classname => $observers) {
309 \core_collator::asort_objects_by_property($observers, 'priority', \core_collator::SORT_NUMERIC);
310 self::$allobservers[$classname] = array_reverse($observers);
315 * Returns all observers in the system. This is only for use for reporting on the list of observers in the system.
317 * @access private
318 * @return array An array of stdClass with all core observer details.
320 public static function get_all_observers() {
321 self::init_all_observers();
322 return self::$allobservers;
326 * Replace all standard observers.
327 * @param array $observers
328 * @return array
330 * @throws \coding_Exception if used outside of unit tests.
332 public static function phpunit_replace_observers(array $observers) {
333 if (!PHPUNIT_TEST) {
334 throw new \coding_exception('Cannot override event observers outside of phpunit tests!');
337 self::phpunit_reset();
338 self::$allobservers = array();
339 self::$reloadaftertest = true;
341 self::add_observers($observers, 'phpunit');
342 self::order_all_observers();
344 return self::$allobservers;
348 * Reset everything if necessary.
349 * @private
351 * @throws \coding_Exception if used outside of unit tests.
353 public static function phpunit_reset() {
354 if (!PHPUNIT_TEST) {
355 throw new \coding_exception('Cannot reset event manager outside of phpunit tests!');
357 self::$buffer = array();
358 self::$extbuffer = array();
359 self::$dispatching = false;
360 if (!self::$reloadaftertest) {
361 self::$allobservers = null;
363 self::$reloadaftertest = false;