PageRenderTime 45ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/classes/event/base.php

http://github.com/moodle/moodle
PHP | 1003 lines | 497 code | 109 blank | 397 comment | 108 complexity | 735c7de56cd8811767f95b28d53dfec7 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  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.
  13. //
  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/>.
  16. namespace core\event;
  17. defined('MOODLE_INTERNAL') || die();
  18. /**
  19. * Base event class.
  20. *
  21. * @package core
  22. * @copyright 2013 Petr Skoda {@link http://skodak.org}
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. /**
  26. * All other event classes must extend this class.
  27. *
  28. * @package core
  29. * @since Moodle 2.6
  30. * @copyright 2013 Petr Skoda {@link http://skodak.org}
  31. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32. *
  33. * @property-read string $eventname Name of the event (=== class name with leading \)
  34. * @property-read string $component Full frankenstyle component name
  35. * @property-read string $action what happened
  36. * @property-read string $target what/who was target of the action
  37. * @property-read string $objecttable name of database table where is object record stored
  38. * @property-read int $objectid optional id of the object
  39. * @property-read string $crud letter indicating event type
  40. * @property-read int $edulevel log level (one of the constants LEVEL_)
  41. * @property-read int $contextid
  42. * @property-read int $contextlevel
  43. * @property-read int $contextinstanceid
  44. * @property-read int $userid who did this?
  45. * @property-read int $courseid the courseid of the event context, 0 for contexts above course
  46. * @property-read int $relateduserid
  47. * @property-read int $anonymous 1 means event should not be visible in reports, 0 means normal event,
  48. * create() argument may be also true/false.
  49. * @property-read mixed $other array or scalar, can not contain objects
  50. * @property-read int $timecreated
  51. */
  52. abstract class base implements \IteratorAggregate {
  53. /**
  54. * Other level.
  55. */
  56. const LEVEL_OTHER = 0;
  57. /**
  58. * Teaching level.
  59. *
  60. * Any event that is performed by someone (typically a teacher) and has a teaching value,
  61. * anything that is affecting the learning experience/environment of the students.
  62. */
  63. const LEVEL_TEACHING = 1;
  64. /**
  65. * Participating level.
  66. *
  67. * Any event that is performed by a user, and is related (or could be related) to his learning experience.
  68. */
  69. const LEVEL_PARTICIPATING = 2;
  70. /**
  71. * The value used when an id can not be mapped during a restore.
  72. */
  73. const NOT_MAPPED = -31337;
  74. /**
  75. * The value used when an id can not be found during a restore.
  76. */
  77. const NOT_FOUND = -31338;
  78. /**
  79. * User id to use when the user is not logged in.
  80. */
  81. const USER_NOTLOGGEDIN = 0;
  82. /**
  83. * User id to use when actor is not an actual user but system, cli or cron.
  84. */
  85. const USER_OTHER = -1;
  86. /** @var array event data */
  87. protected $data;
  88. /** @var array the format is standardised by logging API */
  89. protected $logextra;
  90. /** @var \context of this event */
  91. protected $context;
  92. /**
  93. * @var bool indicates if event was already triggered,
  94. * this prevents second attempt to trigger event.
  95. */
  96. private $triggered;
  97. /**
  98. * @var bool indicates if event was already dispatched,
  99. * this prevents direct calling of manager::dispatch($event).
  100. */
  101. private $dispatched;
  102. /**
  103. * @var bool indicates if event was restored from storage,
  104. * this prevents triggering of restored events.
  105. */
  106. private $restored;
  107. /** @var array list of event properties */
  108. private static $fields = array(
  109. 'eventname', 'component', 'action', 'target', 'objecttable', 'objectid', 'crud', 'edulevel', 'contextid',
  110. 'contextlevel', 'contextinstanceid', 'userid', 'courseid', 'relateduserid', 'anonymous', 'other',
  111. 'timecreated');
  112. /** @var array simple record cache */
  113. private $recordsnapshots = array();
  114. /**
  115. * Private constructor, use create() or restore() methods instead.
  116. */
  117. private final function __construct() {
  118. $this->data = array_fill_keys(self::$fields, null);
  119. // Define some basic details.
  120. $classname = get_called_class();
  121. $parts = explode('\\', $classname);
  122. if (count($parts) !== 3 or $parts[1] !== 'event') {
  123. throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\
  124. namespace");
  125. }
  126. $this->data['eventname'] = '\\'.$classname;
  127. $this->data['component'] = $parts[0];
  128. $pos = strrpos($parts[2], '_');
  129. if ($pos === false) {
  130. throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating
  131. object and action words");
  132. }
  133. $this->data['target'] = substr($parts[2], 0, $pos);
  134. $this->data['action'] = substr($parts[2], $pos + 1);
  135. }
  136. /**
  137. * Create new event.
  138. *
  139. * The optional data keys as:
  140. * 1/ objectid - the id of the object specified in class name
  141. * 2/ context - the context of this event
  142. * 3/ other - the other data describing the event, can not contain objects
  143. * 4/ relateduserid - the id of user which is somehow related to this event
  144. *
  145. * @param array $data
  146. * @return \core\event\base returns instance of new event
  147. *
  148. * @throws \coding_exception
  149. */
  150. public static final function create(array $data = null) {
  151. global $USER, $CFG;
  152. $data = (array)$data;
  153. /** @var \core\event\base $event */
  154. $event = new static();
  155. $event->triggered = false;
  156. $event->restored = false;
  157. $event->dispatched = false;
  158. // By default all events are visible in logs.
  159. $event->data['anonymous'] = 0;
  160. // Set static event data specific for child class.
  161. $event->init();
  162. if (isset($event->data['level'])) {
  163. if (!isset($event->data['edulevel'])) {
  164. debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
  165. $event->data['edulevel'] = $event->data['level'];
  166. }
  167. unset($event->data['level']);
  168. }
  169. // Set automatic data.
  170. $event->data['timecreated'] = time();
  171. // Set optional data or use defaults.
  172. $event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
  173. $event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
  174. $event->data['userid'] = isset($data['userid']) ? $data['userid'] : $USER->id;
  175. $event->data['other'] = isset($data['other']) ? $data['other'] : null;
  176. $event->data['relateduserid'] = isset($data['relateduserid']) ? $data['relateduserid'] : null;
  177. if (isset($data['anonymous'])) {
  178. $event->data['anonymous'] = $data['anonymous'];
  179. }
  180. $event->data['anonymous'] = (int)(bool)$event->data['anonymous'];
  181. if (isset($event->context)) {
  182. if (isset($data['context'])) {
  183. debugging('Context was already set in init() method, ignoring context parameter', DEBUG_DEVELOPER);
  184. }
  185. } else if (!empty($data['context'])) {
  186. $event->context = $data['context'];
  187. } else if (!empty($data['contextid'])) {
  188. $event->context = \context::instance_by_id($data['contextid'], MUST_EXIST);
  189. } else {
  190. throw new \coding_exception('context (or contextid) is a required event property, system context may be hardcoded in init() method.');
  191. }
  192. $event->data['contextid'] = $event->context->id;
  193. $event->data['contextlevel'] = $event->context->contextlevel;
  194. $event->data['contextinstanceid'] = $event->context->instanceid;
  195. if (!isset($event->data['courseid'])) {
  196. if ($coursecontext = $event->context->get_course_context(false)) {
  197. $event->data['courseid'] = $coursecontext->instanceid;
  198. } else {
  199. $event->data['courseid'] = 0;
  200. }
  201. }
  202. if (!array_key_exists('relateduserid', $data) and $event->context->contextlevel == CONTEXT_USER) {
  203. $event->data['relateduserid'] = $event->context->instanceid;
  204. }
  205. // Warn developers if they do something wrong.
  206. if ($CFG->debugdeveloper) {
  207. static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
  208. static $initkeys = array('crud', 'level', 'objecttable', 'edulevel');
  209. foreach ($data as $key => $ignored) {
  210. if ($key === 'context') {
  211. continue;
  212. } else if (in_array($key, $automatickeys)) {
  213. debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically", DEBUG_DEVELOPER);
  214. } else if (in_array($key, $initkeys)) {
  215. debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method", DEBUG_DEVELOPER);
  216. } else if (!in_array($key, self::$fields)) {
  217. debugging("Data key '$key' does not exist in \\core\\event\\base");
  218. }
  219. }
  220. $expectedcourseid = 0;
  221. if ($coursecontext = $event->context->get_course_context(false)) {
  222. $expectedcourseid = $coursecontext->instanceid;
  223. }
  224. if ($expectedcourseid != $event->data['courseid']) {
  225. debugging("Inconsistent courseid - context combination detected.", DEBUG_DEVELOPER);
  226. }
  227. }
  228. // Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
  229. $event->validate_data();
  230. return $event;
  231. }
  232. /**
  233. * Override in subclass.
  234. *
  235. * Set all required data properties:
  236. * 1/ crud - letter [crud]
  237. * 2/ edulevel - using a constant self::LEVEL_*.
  238. * 3/ objecttable - name of database table if objectid specified
  239. *
  240. * Optionally it can set:
  241. * a/ fixed system context
  242. *
  243. * @return void
  244. */
  245. protected abstract function init();
  246. /**
  247. * Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
  248. *
  249. * Throw \coding_exception or debugging() notice in case of any problems.
  250. */
  251. protected function validate_data() {
  252. // Override if you want to validate event properties when
  253. // creating new events.
  254. }
  255. /**
  256. * Returns localised general event name.
  257. *
  258. * Override in subclass, we can not make it static and abstract at the same time.
  259. *
  260. * @return string
  261. */
  262. public static function get_name() {
  263. // Override in subclass with real lang string.
  264. $parts = explode('\\', get_called_class());
  265. if (count($parts) !== 3) {
  266. return get_string('unknownevent', 'error');
  267. }
  268. return $parts[0].': '.str_replace('_', ' ', $parts[2]);
  269. }
  270. /**
  271. * Returns the event name complete with metadata information.
  272. *
  273. * This includes information about whether the event has been deprecated so should not be used in all situations -
  274. * for example within reports themselves.
  275. *
  276. * If overriding this function, please ensure that you call the parent version too.
  277. *
  278. * @return string
  279. */
  280. public static function get_name_with_info() {
  281. $return = static::get_name();
  282. if (static::is_deprecated()) {
  283. $return = get_string('deprecatedeventname', 'core', $return);
  284. }
  285. return $return;
  286. }
  287. /**
  288. * Returns non-localised event description with id's for admin use only.
  289. *
  290. * @return string
  291. */
  292. public function get_description() {
  293. return null;
  294. }
  295. /**
  296. * This method was originally intended for granular
  297. * access control on the event level, unfortunately
  298. * the proper implementation would be too expensive
  299. * in many cases.
  300. *
  301. * @deprecated since 2.7
  302. *
  303. * @param int|\stdClass $user_or_id ID of the user.
  304. * @return bool True if the user can view the event, false otherwise.
  305. */
  306. public function can_view($user_or_id = null) {
  307. debugging('can_view() method is deprecated, use anonymous flag instead if necessary.', DEBUG_DEVELOPER);
  308. return is_siteadmin($user_or_id);
  309. }
  310. /**
  311. * Restore event from existing historic data.
  312. *
  313. * @param array $data
  314. * @param array $logextra the format is standardised by logging API
  315. * @return bool|\core\event\base
  316. */
  317. public static final function restore(array $data, array $logextra) {
  318. $classname = $data['eventname'];
  319. $component = $data['component'];
  320. $action = $data['action'];
  321. $target = $data['target'];
  322. // Security: make 100% sure this really is an event class.
  323. if ($classname !== "\\{$component}\\event\\{$target}_{$action}") {
  324. return false;
  325. }
  326. if (!class_exists($classname)) {
  327. return self::restore_unknown($data, $logextra);
  328. }
  329. $event = new $classname();
  330. if (!($event instanceof \core\event\base)) {
  331. return false;
  332. }
  333. $event->init(); // Init method of events could be setting custom properties.
  334. $event->restored = true;
  335. $event->triggered = true;
  336. $event->dispatched = true;
  337. $event->logextra = $logextra;
  338. foreach (self::$fields as $key) {
  339. if (!array_key_exists($key, $data)) {
  340. debugging("Event restore data must contain key $key");
  341. $data[$key] = null;
  342. }
  343. }
  344. if (count($data) != count(self::$fields)) {
  345. foreach ($data as $key => $value) {
  346. if (!in_array($key, self::$fields)) {
  347. debugging("Event restore data cannot contain key $key");
  348. unset($data[$key]);
  349. }
  350. }
  351. }
  352. $event->data = $data;
  353. return $event;
  354. }
  355. /**
  356. * Restore unknown event.
  357. *
  358. * @param array $data
  359. * @param array $logextra
  360. * @return unknown_logged
  361. */
  362. protected static final function restore_unknown(array $data, array $logextra) {
  363. $classname = '\core\event\unknown_logged';
  364. /** @var unknown_logged $event */
  365. $event = new $classname();
  366. $event->restored = true;
  367. $event->triggered = true;
  368. $event->dispatched = true;
  369. $event->data = $data;
  370. $event->logextra = $logextra;
  371. return $event;
  372. }
  373. /**
  374. * Create fake event from legacy log data.
  375. *
  376. * @param \stdClass $legacy
  377. * @return base
  378. */
  379. public static final function restore_legacy($legacy) {
  380. $classname = get_called_class();
  381. /** @var base $event */
  382. $event = new $classname();
  383. $event->restored = true;
  384. $event->triggered = true;
  385. $event->dispatched = true;
  386. $context = false;
  387. $component = 'legacy';
  388. if ($legacy->cmid) {
  389. $context = \context_module::instance($legacy->cmid, IGNORE_MISSING);
  390. $component = 'mod_'.$legacy->module;
  391. } else if ($legacy->course) {
  392. $context = \context_course::instance($legacy->course, IGNORE_MISSING);
  393. }
  394. if (!$context) {
  395. $context = \context_system::instance();
  396. }
  397. $event->data = array();
  398. $event->data['eventname'] = $legacy->module.'_'.$legacy->action;
  399. $event->data['component'] = $component;
  400. $event->data['action'] = $legacy->action;
  401. $event->data['target'] = null;
  402. $event->data['objecttable'] = null;
  403. $event->data['objectid'] = null;
  404. if (strpos($legacy->action, 'view') !== false) {
  405. $event->data['crud'] = 'r';
  406. } else if (strpos($legacy->action, 'print') !== false) {
  407. $event->data['crud'] = 'r';
  408. } else if (strpos($legacy->action, 'update') !== false) {
  409. $event->data['crud'] = 'u';
  410. } else if (strpos($legacy->action, 'hide') !== false) {
  411. $event->data['crud'] = 'u';
  412. } else if (strpos($legacy->action, 'move') !== false) {
  413. $event->data['crud'] = 'u';
  414. } else if (strpos($legacy->action, 'write') !== false) {
  415. $event->data['crud'] = 'u';
  416. } else if (strpos($legacy->action, 'tag') !== false) {
  417. $event->data['crud'] = 'u';
  418. } else if (strpos($legacy->action, 'remove') !== false) {
  419. $event->data['crud'] = 'u';
  420. } else if (strpos($legacy->action, 'delete') !== false) {
  421. $event->data['crud'] = 'p';
  422. } else if (strpos($legacy->action, 'create') !== false) {
  423. $event->data['crud'] = 'c';
  424. } else if (strpos($legacy->action, 'post') !== false) {
  425. $event->data['crud'] = 'c';
  426. } else if (strpos($legacy->action, 'add') !== false) {
  427. $event->data['crud'] = 'c';
  428. } else {
  429. // End of guessing...
  430. $event->data['crud'] = 'r';
  431. }
  432. $event->data['edulevel'] = $event::LEVEL_OTHER;
  433. $event->data['contextid'] = $context->id;
  434. $event->data['contextlevel'] = $context->contextlevel;
  435. $event->data['contextinstanceid'] = $context->instanceid;
  436. $event->data['userid'] = ($legacy->userid ? $legacy->userid : null);
  437. $event->data['courseid'] = ($legacy->course ? $legacy->course : null);
  438. $event->data['relateduserid'] = ($legacy->userid ? $legacy->userid : null);
  439. $event->data['timecreated'] = $legacy->time;
  440. $event->logextra = array();
  441. if ($legacy->ip) {
  442. $event->logextra['origin'] = 'web';
  443. $event->logextra['ip'] = $legacy->ip;
  444. } else {
  445. $event->logextra['origin'] = 'cli';
  446. $event->logextra['ip'] = null;
  447. }
  448. $event->logextra['realuserid'] = null;
  449. $event->data['other'] = (array)$legacy;
  450. return $event;
  451. }
  452. /**
  453. * This is used when restoring course logs where it is required that we
  454. * map the objectid to it's new value in the new course.
  455. *
  456. * Does nothing in the base class except display a debugging message warning
  457. * the user that the event does not contain the required functionality to
  458. * map this information. For events that do not store an objectid this won't
  459. * be called, so no debugging message will be displayed.
  460. *
  461. * Example of usage:
  462. *
  463. * return array('db' => 'assign_submissions', 'restore' => 'submission');
  464. *
  465. * If the objectid can not be mapped during restore set the value to \core\event\base::NOT_MAPPED, example -
  466. *
  467. * return array('db' => 'some_table', 'restore' => \core\event\base::NOT_MAPPED);
  468. *
  469. * Note - it isn't necessary to specify the 'db' and 'restore' values in this case, so you can also use -
  470. *
  471. * return \core\event\base::NOT_MAPPED;
  472. *
  473. * The 'db' key refers to the database table and the 'restore' key refers to
  474. * the name of the restore element the objectid is associated with. In many
  475. * cases these will be the same.
  476. *
  477. * @return string the name of the restore mapping the objectid links to
  478. */
  479. public static function get_objectid_mapping() {
  480. debugging('In order to restore course logs accurately the event "' . get_called_class() . '" must define the
  481. function get_objectid_mapping().', DEBUG_DEVELOPER);
  482. return false;
  483. }
  484. /**
  485. * This is used when restoring course logs where it is required that we
  486. * map the information in 'other' to it's new value in the new course.
  487. *
  488. * Does nothing in the base class except display a debugging message warning
  489. * the user that the event does not contain the required functionality to
  490. * map this information. For events that do not store any other information this
  491. * won't be called, so no debugging message will be displayed.
  492. *
  493. * Example of usage:
  494. *
  495. * $othermapped = array();
  496. * $othermapped['discussionid'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion');
  497. * $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum');
  498. * return $othermapped;
  499. *
  500. * If an id can not be mapped during restore we set it to \core\event\base::NOT_MAPPED, example -
  501. *
  502. * $othermapped = array();
  503. * $othermapped['someid'] = array('db' => 'some_table', 'restore' => \core\event\base::NOT_MAPPED);
  504. * return $othermapped;
  505. *
  506. * Note - it isn't necessary to specify the 'db' and 'restore' values in this case, so you can also use -
  507. *
  508. * $othermapped = array();
  509. * $othermapped['someid'] = \core\event\base::NOT_MAPPED;
  510. * return $othermapped;
  511. *
  512. * The 'db' key refers to the database table and the 'restore' key refers to
  513. * the name of the restore element the other value is associated with. In many
  514. * cases these will be the same.
  515. *
  516. * @return array an array of other values and their corresponding mapping
  517. */
  518. public static function get_other_mapping() {
  519. debugging('In order to restore course logs accurately the event "' . get_called_class() . '" must define the
  520. function get_other_mapping().', DEBUG_DEVELOPER);
  521. }
  522. /**
  523. * Get static information about an event.
  524. * This is used in reports and is not for general use.
  525. *
  526. * @return array Static information about the event.
  527. */
  528. public static final function get_static_info() {
  529. /** Var \core\event\base $event. */
  530. $event = new static();
  531. // Set static event data specific for child class.
  532. $event->init();
  533. return array(
  534. 'eventname' => $event->data['eventname'],
  535. 'component' => $event->data['component'],
  536. 'target' => $event->data['target'],
  537. 'action' => $event->data['action'],
  538. 'crud' => $event->data['crud'],
  539. 'edulevel' => $event->data['edulevel'],
  540. 'objecttable' => $event->data['objecttable'],
  541. );
  542. }
  543. /**
  544. * Get an explanation of what the class does.
  545. * By default returns the phpdocs from the child event class. Ideally this should
  546. * be overridden to return a translatable get_string style markdown.
  547. * e.g. return new lang_string('eventyourspecialevent', 'plugin_type');
  548. *
  549. * @return string An explanation of the event formatted in markdown style.
  550. */
  551. public static function get_explanation() {
  552. $ref = new \ReflectionClass(get_called_class());
  553. $docblock = $ref->getDocComment();
  554. // Check that there is something to work on.
  555. if (empty($docblock)) {
  556. return null;
  557. }
  558. $docblocklines = explode("\n", $docblock);
  559. // Remove the bulk of the comment characters.
  560. $pattern = "/(^\s*\/\*\*|^\s+\*\s|^\s+\*)/";
  561. $cleanline = array();
  562. foreach ($docblocklines as $line) {
  563. $templine = preg_replace($pattern, '', $line);
  564. // If there is nothing on the line then don't add it to the array.
  565. if (!empty($templine)) {
  566. $cleanline[] = rtrim($templine);
  567. }
  568. // If we get to a line starting with an @ symbol then we don't want the rest.
  569. if (preg_match("/^@|\//", $templine)) {
  570. // Get rid of the last entry (it contains an @ symbol).
  571. array_pop($cleanline);
  572. // Break out of this foreach loop.
  573. break;
  574. }
  575. }
  576. // Add a line break to the sanitised lines.
  577. $explanation = implode("\n", $cleanline);
  578. return $explanation;
  579. }
  580. /**
  581. * Returns event context.
  582. * @return \context
  583. */
  584. public function get_context() {
  585. if (isset($this->context)) {
  586. return $this->context;
  587. }
  588. $this->context = \context::instance_by_id($this->data['contextid'], IGNORE_MISSING);
  589. return $this->context;
  590. }
  591. /**
  592. * Returns relevant URL, override in subclasses.
  593. * @return \moodle_url
  594. */
  595. public function get_url() {
  596. return null;
  597. }
  598. /**
  599. * Return standardised event data as array.
  600. *
  601. * @return array All elements are scalars except the 'other' field which is array.
  602. */
  603. public function get_data() {
  604. return $this->data;
  605. }
  606. /**
  607. * Return auxiliary data that was stored in logs.
  608. *
  609. * List of standard properties:
  610. * - origin: IP number, cli,cron
  611. * - realuserid: id of the user when logged-in-as
  612. *
  613. * @return array the format is standardised by logging API
  614. */
  615. public function get_logextra() {
  616. return $this->logextra;
  617. }
  618. /**
  619. * Does this event replace legacy event?
  620. *
  621. * Note: do not use directly!
  622. *
  623. * @return null|string legacy event name
  624. */
  625. public static function get_legacy_eventname() {
  626. return null;
  627. }
  628. /**
  629. * Legacy event data if get_legacy_eventname() is not empty.
  630. *
  631. * Note: do not use directly!
  632. *
  633. * @return mixed
  634. */
  635. protected function get_legacy_eventdata() {
  636. return null;
  637. }
  638. /**
  639. * Doest this event replace add_to_log() statement?
  640. *
  641. * Note: do not use directly!
  642. *
  643. * @return null|array of parameters to be passed to legacy add_to_log() function.
  644. */
  645. protected function get_legacy_logdata() {
  646. return null;
  647. }
  648. /**
  649. * Validate all properties right before triggering the event.
  650. *
  651. * This throws coding exceptions for fatal problems and debugging for minor problems.
  652. *
  653. * @throws \coding_exception
  654. */
  655. protected final function validate_before_trigger() {
  656. global $DB, $CFG;
  657. if (empty($this->data['crud'])) {
  658. throw new \coding_exception('crud must be specified in init() method of each method');
  659. }
  660. if (!isset($this->data['edulevel'])) {
  661. throw new \coding_exception('edulevel must be specified in init() method of each method');
  662. }
  663. if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
  664. throw new \coding_exception('objecttable must be specified in init() method if objectid present');
  665. }
  666. if ($CFG->debugdeveloper) {
  667. // Ideally these should be coding exceptions, but we need to skip these for performance reasons
  668. // on production servers.
  669. if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
  670. debugging("Invalid event crud value specified.", DEBUG_DEVELOPER);
  671. }
  672. if (!in_array($this->data['edulevel'], array(self::LEVEL_OTHER, self::LEVEL_TEACHING, self::LEVEL_PARTICIPATING))) {
  673. // Bitwise combination of levels is not allowed at this stage.
  674. debugging('Event property edulevel must a constant value, see event_base::LEVEL_*', DEBUG_DEVELOPER);
  675. }
  676. if (self::$fields !== array_keys($this->data)) {
  677. debugging('Number of event data fields must not be changed in event classes', DEBUG_DEVELOPER);
  678. }
  679. $encoded = json_encode($this->data['other']);
  680. // The comparison here is not set to strict as whole float numbers will be converted to integers through JSON encoding /
  681. // decoding and send an unwanted debugging message.
  682. if ($encoded === false or $this->data['other'] != json_decode($encoded, true)) {
  683. debugging('other event data must be compatible with json encoding', DEBUG_DEVELOPER);
  684. }
  685. if ($this->data['userid'] and !is_number($this->data['userid'])) {
  686. debugging('Event property userid must be a number', DEBUG_DEVELOPER);
  687. }
  688. if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
  689. debugging('Event property courseid must be a number', DEBUG_DEVELOPER);
  690. }
  691. if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
  692. debugging('Event property objectid must be a number', DEBUG_DEVELOPER);
  693. }
  694. if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
  695. debugging('Event property relateduserid must be a number', DEBUG_DEVELOPER);
  696. }
  697. if ($this->data['objecttable']) {
  698. if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
  699. debugging('Unknown table specified in objecttable field', DEBUG_DEVELOPER);
  700. }
  701. if (!isset($this->data['objectid'])) {
  702. debugging('Event property objectid must be set when objecttable is defined', DEBUG_DEVELOPER);
  703. }
  704. }
  705. }
  706. }
  707. /**
  708. * Trigger event.
  709. */
  710. public final function trigger() {
  711. global $CFG;
  712. if ($this->restored) {
  713. throw new \coding_exception('Can not trigger restored event');
  714. }
  715. if ($this->triggered or $this->dispatched) {
  716. throw new \coding_exception('Can not trigger event twice');
  717. }
  718. $this->validate_before_trigger();
  719. $this->triggered = true;
  720. if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) {
  721. if ($data = $this->get_legacy_logdata()) {
  722. $manager = get_log_manager();
  723. if (method_exists($manager, 'legacy_add_to_log')) {
  724. if (is_array($data[0])) {
  725. // Some events require several entries in 'log' table.
  726. foreach ($data as $d) {
  727. call_user_func_array(array($manager, 'legacy_add_to_log'), $d);
  728. }
  729. } else {
  730. call_user_func_array(array($manager, 'legacy_add_to_log'), $data);
  731. }
  732. }
  733. }
  734. }
  735. if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) {
  736. $this->dispatched = true;
  737. \phpunit_util::event_triggered($this);
  738. return;
  739. }
  740. \core\event\manager::dispatch($this);
  741. $this->dispatched = true;
  742. }
  743. /**
  744. * Was this event already triggered?
  745. *
  746. * @return bool
  747. */
  748. public final function is_triggered() {
  749. return $this->triggered;
  750. }
  751. /**
  752. * Used from event manager to prevent direct access.
  753. *
  754. * @return bool
  755. */
  756. public final function is_dispatched() {
  757. return $this->dispatched;
  758. }
  759. /**
  760. * Was this event restored?
  761. *
  762. * @return bool
  763. */
  764. public final function is_restored() {
  765. return $this->restored;
  766. }
  767. /**
  768. * Add cached data that will be most probably used in event observers.
  769. *
  770. * This is used to improve performance, but it is required for data
  771. * that was just deleted.
  772. *
  773. * @param string $tablename
  774. * @param \stdClass $record
  775. *
  776. * @throws \coding_exception if used after ::trigger()
  777. */
  778. public final function add_record_snapshot($tablename, $record) {
  779. global $DB, $CFG;
  780. if ($this->triggered) {
  781. throw new \coding_exception('It is not possible to add snapshots after triggering of events');
  782. }
  783. // Special case for course module, allow instance of cm_info to be passed instead of stdClass.
  784. if ($tablename === 'course_modules' && $record instanceof \cm_info) {
  785. $record = $record->get_course_module_record();
  786. }
  787. // NOTE: this might use some kind of MUC cache,
  788. // hopefully we will not run out of memory here...
  789. if ($CFG->debugdeveloper) {
  790. if (!($record instanceof \stdClass)) {
  791. debugging('Argument $record must be an instance of stdClass.', DEBUG_DEVELOPER);
  792. }
  793. if (!$DB->get_manager()->table_exists($tablename)) {
  794. debugging("Invalid table name '$tablename' specified, database table does not exist.", DEBUG_DEVELOPER);
  795. } else {
  796. $columns = $DB->get_columns($tablename);
  797. $missingfields = array_diff(array_keys($columns), array_keys((array)$record));
  798. if (!empty($missingfields)) {
  799. debugging("Fields list in snapshot record does not match fields list in '$tablename'. Record is missing fields: ".
  800. join(', ', $missingfields), DEBUG_DEVELOPER);
  801. }
  802. }
  803. }
  804. $this->recordsnapshots[$tablename][$record->id] = $record;
  805. }
  806. /**
  807. * Returns cached record or fetches data from database if not cached.
  808. *
  809. * @param string $tablename
  810. * @param int $id
  811. * @return \stdClass
  812. *
  813. * @throws \coding_exception if used after ::restore()
  814. */
  815. public final function get_record_snapshot($tablename, $id) {
  816. global $DB;
  817. if ($this->restored) {
  818. throw new \coding_exception('It is not possible to get snapshots from restored events');
  819. }
  820. if (isset($this->recordsnapshots[$tablename][$id])) {
  821. return clone($this->recordsnapshots[$tablename][$id]);
  822. }
  823. $record = $DB->get_record($tablename, array('id'=>$id));
  824. $this->recordsnapshots[$tablename][$id] = $record;
  825. return $record;
  826. }
  827. /**
  828. * Magic getter for read only access.
  829. *
  830. * @param string $name
  831. * @return mixed
  832. */
  833. public function __get($name) {
  834. if ($name === 'level') {
  835. debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
  836. return $this->data['edulevel'];
  837. }
  838. if (array_key_exists($name, $this->data)) {
  839. return $this->data[$name];
  840. }
  841. debugging("Accessing non-existent event property '$name'");
  842. }
  843. /**
  844. * Magic setter.
  845. *
  846. * Note: we must not allow modification of data from outside,
  847. * after trigger() the data MUST NOT CHANGE!!!
  848. *
  849. * @param string $name
  850. * @param mixed $value
  851. *
  852. * @throws \coding_exception
  853. */
  854. public function __set($name, $value) {
  855. throw new \coding_exception('Event properties must not be modified.');
  856. }
  857. /**
  858. * Is data property set?
  859. *
  860. * @param string $name
  861. * @return bool
  862. */
  863. public function __isset($name) {
  864. if ($name === 'level') {
  865. debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
  866. return isset($this->data['edulevel']);
  867. }
  868. return isset($this->data[$name]);
  869. }
  870. /**
  871. * Create an iterator because magic vars can't be seen by 'foreach'.
  872. *
  873. * @return \ArrayIterator
  874. */
  875. public function getIterator() {
  876. return new \ArrayIterator($this->data);
  877. }
  878. /**
  879. * Whether this event has been marked as deprecated.
  880. *
  881. * Events cannot be deprecated in the normal fashion as they must remain to support historical data.
  882. * Once they are deprecated, there is no way to trigger the event, so it does not make sense to list it in some
  883. * parts of the UI (e.g. Event Monitor).
  884. *
  885. * @return boolean
  886. */
  887. public static function is_deprecated() {
  888. return false;
  889. }
  890. }