Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • /lib/ -> eventslib.php (source)

    Differences Between: [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35]

       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  
      17  /**
      18   * Library of functions for events manipulation.
      19   *
      20   * The public API is all at the end of this file.
      21   *
      22   * @package core
      23   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
      24   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      25   */
      26  
      27  defined('MOODLE_INTERNAL') || die();
      28  
      29  /**
      30   * Loads the events definitions for the component (from file). If no
      31   * events are defined for the component, we simply return an empty array.
      32   *
      33   * @access protected To be used from eventslib only
      34   *
      35   * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
      36   * @return array Array of capabilities or empty array if not exists
      37   */
      38  function events_load_def($component) {
      39      global $CFG;
      40      if ($component === 'unittest') {
      41          $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
      42      } else {
      43          $defpath = core_component::get_component_directory($component).'/db/events.php';
      44      }
      45  
      46      $handlers = array();
      47  
      48      if (file_exists($defpath)) {
      49          require($defpath);
      50      }
      51  
      52      // make sure the definitions are valid and complete; tell devs what is wrong
      53      foreach ($handlers as $eventname => $handler) {
      54          if ($eventname === 'reset') {
      55              debugging("'reset' can not be used as event name.");
      56              unset($handlers['reset']);
      57              continue;
      58          }
      59          if (!is_array($handler)) {
      60              debugging("Handler of '$eventname' must be specified as array'");
      61              unset($handlers[$eventname]);
      62              continue;
      63          }
      64          if (!isset($handler['handlerfile'])) {
      65              debugging("Handler of '$eventname' must include 'handlerfile' key'");
      66              unset($handlers[$eventname]);
      67              continue;
      68          }
      69          if (!isset($handler['handlerfunction'])) {
      70              debugging("Handler of '$eventname' must include 'handlerfunction' key'");
      71              unset($handlers[$eventname]);
      72              continue;
      73          }
      74          if (!isset($handler['schedule'])) {
      75              $handler['schedule'] = 'instant';
      76          }
      77          if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
      78              debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
      79              unset($handlers[$eventname]);
      80              continue;
      81          }
      82          if (!isset($handler['internal'])) {
      83              $handler['internal'] = 1;
      84          }
      85          $handlers[$eventname] = $handler;
      86      }
      87  
      88      return $handlers;
      89  }
      90  
      91  /**
      92   * Gets the capabilities that have been cached in the database for this
      93   * component.
      94   *
      95   * @access protected To be used from eventslib only
      96   *
      97   * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
      98   * @return array of events
      99   */
     100  function events_get_cached($component) {
     101      global $DB;
     102  
     103      $cachedhandlers = array();
     104  
     105      if ($storedhandlers = $DB->get_records('events_handlers', array('component'=>$component))) {
     106          foreach ($storedhandlers as $handler) {
     107              $cachedhandlers[$handler->eventname] = array (
     108                  'id'              => $handler->id,
     109                  'handlerfile'     => $handler->handlerfile,
     110                  'handlerfunction' => $handler->handlerfunction,
     111                  'schedule'        => $handler->schedule,
     112                  'internal'        => $handler->internal);
     113          }
     114      }
     115  
     116      return $cachedhandlers;
     117  }
     118  
     119  /**
     120   * Updates all of the event definitions within the database.
     121   *
     122   * Unfortunately this isn't as simple as removing them all and then readding
     123   * the updated event definitions. Chances are queued items are referencing the
     124   * existing definitions.
     125   *
     126   * Note that the absence of the db/events.php event definition file
     127   * will cause any queued events for the component to be removed from
     128   * the database.
     129   *
     130   * @category event
     131   * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
     132   * @return boolean always returns true
     133   */
     134  function events_update_definition($component='moodle') {
     135      global $DB;
     136  
     137      // load event definition from events.php
     138      $filehandlers = events_load_def($component);
     139  
     140      // load event definitions from db tables
     141      // if we detect an event being already stored, we discard from this array later
     142      // the remaining needs to be removed
     143      $cachedhandlers = events_get_cached($component);
     144  
     145      foreach ($filehandlers as $eventname => $filehandler) {
     146          if (!empty($cachedhandlers[$eventname])) {
     147              if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
     148                  $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
     149                  $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
     150                  $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
     151                  // exact same event handler already present in db, ignore this entry
     152  
     153                  unset($cachedhandlers[$eventname]);
     154                  continue;
     155  
     156              } else {
     157                  // same event name matches, this event has been updated, update the datebase
     158                  $handler = new stdClass();
     159                  $handler->id              = $cachedhandlers[$eventname]['id'];
     160                  $handler->handlerfile     = $filehandler['handlerfile'];
     161                  $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
     162                  $handler->schedule        = $filehandler['schedule'];
     163                  $handler->internal        = $filehandler['internal'];
     164  
     165                  $DB->update_record('events_handlers', $handler);
     166  
     167                  unset($cachedhandlers[$eventname]);
     168                  continue;
     169              }
     170  
     171          } else {
     172              // if we are here, this event handler is not present in db (new)
     173              // add it
     174              $handler = new stdClass();
     175              $handler->eventname       = $eventname;
     176              $handler->component       = $component;
     177              $handler->handlerfile     = $filehandler['handlerfile'];
     178              $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
     179              $handler->schedule        = $filehandler['schedule'];
     180              $handler->status          = 0;
     181              $handler->internal        = $filehandler['internal'];
     182  
     183              $DB->insert_record('events_handlers', $handler);
     184          }
     185      }
     186  
     187      // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
     188      // and should be removed, delete from db
     189      events_cleanup($component, $cachedhandlers);
     190  
     191      events_get_handlers('reset');
     192  
     193      return true;
     194  }
     195  
     196  /**
     197   * Remove all event handlers and queued events
     198   *
     199   * @category event
     200   * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
     201   */
     202  function events_uninstall($component) {
     203      $cachedhandlers = events_get_cached($component);
     204      events_cleanup($component, $cachedhandlers);
     205  
     206      events_get_handlers('reset');
     207  }
     208  
     209  /**
     210   * Deletes cached events that are no longer needed by the component.
     211   *
     212   * @access protected To be used from eventslib only
     213   *
     214   * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
     215   * @param array $cachedhandlers array of the cached events definitions that will be
     216   * @return int number of unused handlers that have been removed
     217   */
     218  function events_cleanup($component, $cachedhandlers) {
     219      global $DB;
     220  
     221      $deletecount = 0;
     222      foreach ($cachedhandlers as $eventname => $cachedhandler) {
     223          if ($qhandlers = $DB->get_records('events_queue_handlers', array('handlerid'=>$cachedhandler['id']))) {
     224              //debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
     225              foreach ($qhandlers as $qhandler) {
     226                  events_dequeue($qhandler);
     227              }
     228          }
     229          $DB->delete_records('events_handlers', array('eventname'=>$eventname, 'component'=>$component));
     230          $deletecount++;
     231      }
     232  
     233      return $deletecount;
     234  }
     235  
     236  /****************** End of Events handler Definition code *******************/
     237  
     238  /**
     239   * Puts a handler on queue
     240   *
     241   * @access protected To be used from eventslib only
     242   *
     243   * @param stdClass $handler event handler object from db
     244   * @param stdClass $event event data object
     245   * @param string $errormessage The error message indicating the problem
     246   * @return int id number of new queue handler
     247   */
     248  function events_queue_handler($handler, $event, $errormessage) {
     249      global $DB;
     250  
     251      if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
     252          debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
     253          return $qhandler->id;
     254      }
     255  
     256      // make a new queue handler
     257      $qhandler = new stdClass();
     258      $qhandler->queuedeventid  = $event->id;
     259      $qhandler->handlerid      = $handler->id;
     260      $qhandler->errormessage   = $errormessage;
     261      $qhandler->timemodified   = time();
     262      if ($handler->schedule === 'instant' and $handler->status == 1) {
     263          $qhandler->status     = 1; //already one failed attempt to dispatch this event
     264      } else {
     265          $qhandler->status     = 0;
     266      }
     267  
     268      return $DB->insert_record('events_queue_handlers', $qhandler);
     269  }
     270  
     271  /**
     272   * trigger a single event with a specified handler
     273   *
     274   * @access protected To be used from eventslib only
     275   *
     276   * @param stdClass $handler This shoudl be a row from the events_handlers table.
     277   * @param stdClass $eventdata An object containing information about the event
     278   * @param string $errormessage error message indicating problem
     279   * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
     280   */
     281  function events_dispatch($handler, $eventdata, &$errormessage) {
     282      global $CFG;
     283  
     284      $function = unserialize($handler->handlerfunction);
     285  
     286      if (is_callable($function)) {
     287          // oki, no need for includes
     288  
     289      } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
     290          include_once($CFG->dirroot.$handler->handlerfile);
     291  
     292      } else {
     293          $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
     294          return null;
     295      }
     296  
     297      // checks for handler validity
     298      if (is_callable($function)) {
     299          $result = call_user_func($function, $eventdata);
     300          if ($result === false) {
     301              $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
     302              return false;
     303          }
     304          return true;
     305  
     306      } else {
     307          $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
     308          return null;
     309      }
     310  }
     311  
     312  /**
     313   * given a queued handler, call the respective event handler to process the event
     314   *
     315   * @access protected To be used from eventslib only
     316   *
     317   * @param stdClass $qhandler events_queued_handler row from db
     318   * @return boolean true means event processed, false means retry later, NULL means fatal failure
     319   */
     320  function events_process_queued_handler($qhandler) {
     321      global $DB;
     322  
     323      // get handler
     324      if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
     325          debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
     326          //irrecoverable error, remove broken queue handler
     327          events_dequeue($qhandler);
     328          return NULL;
     329      }
     330  
     331      // get event object
     332      if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
     333          // can't proceed with no event object - might happen when two crons running at the same time
     334          debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
     335          //irrecoverable error, remove broken queue handler
     336          events_dequeue($qhandler);
     337          return NULL;
     338      }
     339  
     340      // call the function specified by the handler
     341      try {
     342          $errormessage = 'Unknown error';
     343          if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
     344              //everything ok
     345              events_dequeue($qhandler);
     346              return true;
     347          }
     348      } catch (Exception $e) {
     349          // the problem here is that we do not want one broken handler to stop all others,
     350          // cron handlers are very tricky because the needed data might have been deleted before the cron execution
     351          $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
     352                  $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
     353          if (!empty($e->debuginfo)) {
     354              $errormessage .= $e->debuginfo;
     355          }
     356      }
     357  
     358      //dispatching failed
     359      $qh = new stdClass();
     360      $qh->id           = $qhandler->id;
     361      $qh->errormessage = $errormessage;
     362      $qh->timemodified = time();
     363      $qh->status       = $qhandler->status + 1;
     364      $DB->update_record('events_queue_handlers', $qh);
     365  
     366      debugging($errormessage);
     367  
     368      return false;
     369  }
     370  
     371  /**
     372   * Removes this queued handler from the events_queued_handler table
     373   *
     374   * Removes events_queue record from events_queue if no more references to this event object exists
     375   *
     376   * @access protected To be used from eventslib only
     377   *
     378   * @param stdClass $qhandler A row from the events_queued_handler table
     379   */
     380  function events_dequeue($qhandler) {
     381      global $DB;
     382  
     383      // first delete the queue handler
     384      $DB->delete_records('events_queue_handlers', array('id'=>$qhandler->id));
     385  
     386      // if no more queued handler is pointing to the same event - delete the event too
     387      if (!$DB->record_exists('events_queue_handlers', array('queuedeventid'=>$qhandler->queuedeventid))) {
     388          $DB->delete_records('events_queue', array('id'=>$qhandler->queuedeventid));
     389      }
     390  }
     391  
     392  /**
     393   * Returns handlers for given event. Uses caching for better perf.
     394   *
     395   * @access protected To be used from eventslib only
     396   *
     397   * @staticvar array $handlers
     398   * @param string $eventname name of event or 'reset'
     399   * @return array|false array of handlers or false otherwise
     400   */
     401  function events_get_handlers($eventname) {
     402      global $DB;
     403      static $handlers = array();
     404  
     405      if ($eventname === 'reset') {
     406          $handlers = array();
     407          return false;
     408      }
     409  
     410      if (!array_key_exists($eventname, $handlers)) {
     411          $handlers[$eventname] = $DB->get_records('events_handlers', array('eventname'=>$eventname));
     412      }
     413  
     414      return $handlers[$eventname];
     415  }
     416  
     417  /**
     418   * Events cron will try to empty the events queue by processing all the queued events handlers
     419   *
     420   * @access public Part of the public API
     421   * @category event
     422   * @param string $eventname empty means all
     423   * @return int number of dispatched events
     424   */
     425  function events_cron($eventname='') {
     426      global $DB;
     427  
     428      $failed = array();
     429      $processed = 0;
     430  
     431      if ($eventname) {
     432          $sql = "SELECT qh.*
     433                    FROM {events_queue_handlers} qh, {events_handlers} h
     434                   WHERE qh.handlerid = h.id AND h.eventname=?
     435                ORDER BY qh.id";
     436          $params = array($eventname);
     437      } else {
     438          $sql = "SELECT *
     439                    FROM {events_queue_handlers}
     440                ORDER BY id";
     441          $params = array();
     442      }
     443  
     444      $rs = $DB->get_recordset_sql($sql, $params);
     445      foreach ($rs as $qhandler) {
     446          if (isset($failed[$qhandler->handlerid])) {
     447              // do not try to dispatch any later events when one already asked for retry or ended with exception
     448              continue;
     449          }
     450          $status = events_process_queued_handler($qhandler);
     451          if ($status === false) {
     452              // handler is asking for retry, do not send other events to this handler now
     453              $failed[$qhandler->handlerid] = $qhandler->handlerid;
     454          } else if ($status === NULL) {
     455              // means completely broken handler, event data was purged
     456              $failed[$qhandler->handlerid] = $qhandler->handlerid;
     457          } else {
     458              $processed++;
     459          }
     460      }
     461      $rs->close();
     462  
     463      // remove events that do not have any handlers waiting
     464      $sql = "SELECT eq.id
     465                FROM {events_queue} eq
     466                LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
     467               WHERE qh.id IS NULL";
     468      $rs = $DB->get_recordset_sql($sql);
     469      foreach ($rs as $event) {
     470          //debugging('Purging stale event '.$event->id);
     471          $DB->delete_records('events_queue', array('id'=>$event->id));
     472      }
     473      $rs->close();
     474  
     475      return $processed;
     476  }
     477  
     478  /**
     479   * Do not call directly, this is intended to be used from new event base only.
     480   *
     481   * @private
     482   * @param string $eventname name of the event
     483   * @param mixed $eventdata event data object
     484   * @return int number of failed events
     485   */
     486  function events_trigger_legacy($eventname, $eventdata) {
     487      global $CFG, $USER, $DB;
     488  
     489      $failedcount = 0; // number of failed events.
     490  
     491      // pull out all registered event handlers
     492      if ($handlers = events_get_handlers($eventname)) {
     493          foreach ($handlers as $handler) {
     494              $errormessage = '';
     495  
     496              if ($handler->schedule === 'instant') {
     497                  if ($handler->status) {
     498                      //check if previous pending events processed
     499                      if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
     500                          // ok, queue is empty, lets reset the status back to 0 == ok
     501                          $handler->status = 0;
     502                          $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
     503                          // reset static handler cache
     504                          events_get_handlers('reset');
     505                      }
     506                  }
     507  
     508                  // dispatch the event only if instant schedule and status ok
     509                  if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
     510                      // increment the error status counter
     511                      $handler->status++;
     512                      $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
     513                      // reset static handler cache
     514                      events_get_handlers('reset');
     515  
     516                  } else {
     517                      $errormessage = 'Unknown error';
     518                      $result = events_dispatch($handler, $eventdata, $errormessage);
     519                      if ($result === true) {
     520                          // everything is fine - event dispatched
     521                          continue;
     522                      } else if ($result === false) {
     523                          // retry later - set error count to 1 == send next instant into cron queue
     524                          $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
     525                          // reset static handler cache
     526                          events_get_handlers('reset');
     527                      } else {
     528                          // internal problem - ignore the event completely
     529                          $failedcount ++;
     530                          continue;
     531                      }
     532                  }
     533  
     534                  // update the failed counter
     535                  $failedcount ++;
     536  
     537              } else if ($handler->schedule === 'cron') {
     538                  //ok - use queueing of events only
     539  
     540              } else {
     541                  // unknown schedule - ignore event completely
     542                  debugging("Unknown handler schedule type: $handler->schedule");
     543                  $failedcount ++;
     544                  continue;
     545              }
     546  
     547              // if even type is not instant, or dispatch asked for retry, queue it
     548              $event = new stdClass();
     549              $event->userid      = $USER->id;
     550              $event->eventdata   = base64_encode(serialize($eventdata));
     551              $event->timecreated = time();
     552              if (debugging()) {
     553                  $dump = '';
     554                  $callers = debug_backtrace();
     555                  foreach ($callers as $caller) {
     556                      if (!isset($caller['line'])) {
     557                          $caller['line'] = '?';
     558                      }
     559                      if (!isset($caller['file'])) {
     560                          $caller['file'] = '?';
     561                      }
     562                      $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
     563                      if (isset($caller['function'])) {
     564                          $dump .= ': call to ';
     565                          if (isset($caller['class'])) {
     566                              $dump .= $caller['class'] . $caller['type'];
     567                          }
     568                          $dump .= $caller['function'] . '()';
     569                      }
     570                      $dump .= "\n";
     571                  }
     572                  $event->stackdump = $dump;
     573              } else {
     574                  $event->stackdump = '';
     575              }
     576              $event->id = $DB->insert_record('events_queue', $event);
     577              events_queue_handler($handler, $event, $errormessage);
     578          }
     579      } else {
     580          // No handler found for this event name - this is ok!
     581      }
     582  
     583      return $failedcount;
     584  }
     585  
     586  /**
     587   * checks if an event is registered for this component
     588   *
     589   * @access public Part of the public API
     590   *
     591   * @param string $eventname name of the event
     592   * @param string $component component name, can be mod/data or moodle
     593   * @return bool
     594   */
     595  function events_is_registered($eventname, $component) {
     596      global $DB;
     597      return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
     598  }
     599  
     600  /**
     601   * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
     602   *
     603   * @access public Part of the public API
     604   *
     605   * @param string $eventname name of the event
     606   * @return int number of queued events
     607   */
     608  function events_pending_count($eventname) {
     609      global $DB;
     610  
     611      $sql = "SELECT COUNT('x')
     612                FROM {events_queue_handlers} qh
     613                JOIN {events_handlers} h ON h.id = qh.handlerid
     614               WHERE h.eventname = ?";
     615  
     616      return $DB->count_records_sql($sql, array($eventname));
     617  }
    

    Search This Site: