Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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   * Event vault class
  19   *
  20   * @package    core_calendar
  21   * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_calendar\local\event\data_access;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use core_calendar\local\event\entities\action_event_interface;
  30  use core_calendar\local\event\entities\event_interface;
  31  use core_calendar\local\event\exceptions\limit_invalid_parameter_exception;
  32  use core_calendar\local\event\factories\action_factory_interface;
  33  use core_calendar\local\event\factories\event_factory_interface;
  34  use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface;
  35  
  36  /**
  37   * Event vault class.
  38   *
  39   * This class will handle interacting with the database layer to retrieve
  40   * the records. This is required to house the complex logic required for
  41   * pagination because it's not a one-to-one mapping between database records
  42   * and users.
  43   *
  44   * This is a repository. It's called a vault to reduce confusion because
  45   * Moodle has already taken the name repository. Vault is cooler anyway.
  46   *
  47   * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
  48   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class event_vault implements event_vault_interface {
  51  
  52      /**
  53       * @var event_factory_interface $factory Factory for creating events.
  54       */
  55      protected $factory;
  56  
  57      /**
  58       * @var raw_event_retrieval_strategy_interface $retrievalstrategy Strategy for getting events from the DB.
  59       */
  60      protected $retrievalstrategy;
  61  
  62      /**
  63       * Create an event vault.
  64       *
  65       * @param event_factory_interface $factory An event factory
  66       * @param raw_event_retrieval_strategy_interface $retrievalstrategy
  67       */
  68      public function __construct(
  69          event_factory_interface $factory,
  70          raw_event_retrieval_strategy_interface $retrievalstrategy
  71      ) {
  72          $this->factory = $factory;
  73          $this->retrievalstrategy = $retrievalstrategy;
  74      }
  75  
  76      public function get_event_by_id($id) {
  77          global $DB;
  78  
  79          if ($record = $DB->get_record('event', ['id' => $id])) {
  80              return $this->transform_from_database_record($record);
  81          } else {
  82              return false;
  83          }
  84      }
  85  
  86      public function get_events(
  87          $timestartfrom = null,
  88          $timestartto = null,
  89          $timesortfrom = null,
  90          $timesortto = null,
  91          event_interface $timestartafterevent = null,
  92          event_interface $timesortafterevent = null,
  93          $limitnum = 20,
  94          $type = null,
  95          array $usersfilter = null,
  96          array $groupsfilter = null,
  97          array $coursesfilter = null,
  98          array $categoriesfilter = null,
  99          $withduration = true,
 100          $ignorehidden = true,
 101          callable $filter = null,
 102          ?string $searchvalue = null
 103      ) {
 104  
 105          $fromquery = function($field, $timefrom, $lastseenmethod, $afterevent, $withduration) {
 106              if (!$timefrom) {
 107                  return false;
 108              }
 109  
 110              return $this->timefield_pagination_from(
 111                  $field,
 112                  $timefrom,
 113                  $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null,
 114                  $afterevent ? $afterevent->get_id() : null,
 115                  $withduration
 116              );
 117          };
 118  
 119          $toquery = function($field, $timeto, $lastseenmethod, $afterevent) {
 120              if (!$timeto) {
 121                  return false;
 122              }
 123  
 124              return $this->timefield_pagination_to(
 125                  $field,
 126                  $timeto,
 127                  $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null,
 128                  $afterevent ? $afterevent->get_id() : null
 129              );
 130          };
 131  
 132          $timesortfromquery = $fromquery('timesort', $timesortfrom, 'get_sort_time', $timesortafterevent, $withduration);
 133          $timesorttoquery = $toquery('timesort', $timesortto, 'get_sort_time', $timesortafterevent);
 134          $timestartfromquery = $fromquery('timestart', $timestartfrom, 'get_start_time', $timestartafterevent, $withduration);
 135          $timestarttoquery = $toquery('timestart', $timestartto, 'get_start_time', $timestartafterevent);
 136  
 137          if (($timesortto && !$timesorttoquery) || ($timestartto && !$timestarttoquery)) {
 138              return [];
 139          }
 140  
 141          $searchquery = $this->generate_search_subquery($searchvalue);
 142  
 143          $params = array_merge(
 144              $type ? ['type' => $type] : [],
 145              $timesortfromquery ? $timesortfromquery['params'] : [],
 146              $timesorttoquery ? $timesorttoquery['params'] : [],
 147              $timestartfromquery ? $timestartfromquery['params'] : [],
 148              $timestarttoquery ? $timestarttoquery['params'] : [],
 149              $searchquery ? $searchquery['params'] : [],
 150          );
 151  
 152          $where = array_merge(
 153              $type ? ['type = :type'] : [],
 154              $timesortfromquery ? $timesortfromquery['where'] : [],
 155              $timesorttoquery ? $timesorttoquery['where'] : [],
 156              $timestartfromquery ? $timestartfromquery['where'] : [],
 157              $timestarttoquery ? $timestarttoquery['where'] : [],
 158              $searchquery ? [$searchquery['where']] : [],
 159          );
 160  
 161          $offset = 0;
 162          $events = [];
 163  
 164          while ($records = array_values($this->retrievalstrategy->get_raw_events(
 165              $usersfilter,
 166              $groupsfilter,
 167              $coursesfilter,
 168              $categoriesfilter,
 169              $where,
 170              $params,
 171              "COALESCE(e.timesort, e.timestart) ASC, e.id ASC",
 172              $offset,
 173              $limitnum,
 174              $ignorehidden
 175          ))) {
 176              foreach ($records as $record) {
 177                  if ($event = $this->transform_from_database_record($record)) {
 178                      $filtertest = $filter ? $filter($event) : true;
 179  
 180                      if ($event && $filtertest) {
 181                          $events[] = $event;
 182                      }
 183  
 184                      if (count($events) == $limitnum) {
 185                          // We've got all of the events so break both loops.
 186                          break 2;
 187                      }
 188                  }
 189              }
 190  
 191              if (!$limitnum) {
 192                  break;
 193              } else {
 194                  $offset += $limitnum;
 195              }
 196          }
 197  
 198          return $events;
 199      }
 200  
 201      public function get_action_events_by_timesort(
 202          \stdClass $user,
 203          $timesortfrom = null,
 204          $timesortto = null,
 205          event_interface $afterevent = null,
 206          $limitnum = 20,
 207          $limittononsuspendedevents = false,
 208          ?string $searchvalue = null
 209      ) {
 210          $courseids = array_map(function($course) {
 211              return $course->id;
 212          }, enrol_get_all_users_courses($user->id, $limittononsuspendedevents));
 213  
 214          $groupids = array_reduce($courseids, function($carry, $courseid) use ($user) {
 215              $groupings = groups_get_user_groups($courseid, $user->id);
 216              // Grouping 0 is all groups.
 217              return array_merge($carry, $groupings[0]);
 218          }, []);
 219  
 220          // Always include the site events.
 221          $courseids = $courseids ? array_merge($courseids, [SITEID]) : $courseids;
 222  
 223          return $this->get_events(
 224              null,
 225              null,
 226              $timesortfrom,
 227              $timesortto,
 228              null,
 229              $afterevent,
 230              $limitnum,
 231              CALENDAR_EVENT_TYPE_ACTION,
 232              [$user->id],
 233              $groupids ? $groupids : null,
 234              $courseids ? $courseids : null,
 235              null, // All categories.
 236              true,
 237              true,
 238              function ($event) {
 239                  return $event instanceof action_event_interface;
 240              },
 241              $searchvalue
 242          );
 243      }
 244  
 245      public function get_action_events_by_course(
 246          \stdClass $user,
 247          \stdClass $course,
 248          $timesortfrom = null,
 249          $timesortto = null,
 250          event_interface $afterevent = null,
 251          $limitnum = 20,
 252          ?string $searchvalue = null
 253      ) {
 254          $groupings = groups_get_user_groups($course->id, $user->id);
 255          return array_values(
 256              $this->get_events(
 257                  null,
 258                  null,
 259                  $timesortfrom,
 260                  $timesortto,
 261                  null,
 262                  $afterevent,
 263                  $limitnum,
 264                  CALENDAR_EVENT_TYPE_ACTION,
 265                  [$user->id],
 266                  $groupings[0] ? $groupings[0] : null,
 267                  [$course->id],
 268                  [],
 269                  true,
 270                  true,
 271                  function ($event) use ($course) {
 272                      return $event instanceof action_event_interface && $event->get_course()->get('id') == $course->id;
 273                  },
 274                  $searchvalue
 275              )
 276          );
 277      }
 278  
 279      /**
 280       * Generates SQL subquery and parameters for 'from' pagination.
 281       *
 282       * @param string    $field
 283       * @param int       $timefrom
 284       * @param int|null  $lastseentime
 285       * @param int|null  $lastseenid
 286       * @param bool      $withduration
 287       * @return array
 288       */
 289      protected function timefield_pagination_from(
 290          $field,
 291          $timefrom,
 292          $lastseentime = null,
 293          $lastseenid = null,
 294          $withduration = true
 295      ) {
 296          $where = '';
 297          $params = [];
 298  
 299          if ($lastseentime && $lastseentime >= $timefrom) {
 300              $where = '((timesort = :timefrom1 AND e.id > :timefromid) OR timesort > :timefrom2)';
 301              if ($field === 'timestart') {
 302                  $where = '((timestart = :timefrom1 AND e.id > :timefromid) OR timestart > :timefrom2' .
 303                         ($withduration ? ' OR timestart + timeduration > :timefrom3' : '') . ')';
 304              }
 305              $params['timefromid'] = $lastseenid;
 306              $params['timefrom1'] = $lastseentime;
 307              $params['timefrom2'] = $lastseentime;
 308              $params['timefrom3'] = $lastseentime;
 309          } else {
 310              $where = 'timesort >= :timefrom';
 311              if ($field === 'timestart') {
 312                  $where = '(timestart >= :timefrom' .
 313                         ($withduration ? ' OR timestart + timeduration > :timefrom2' : '') . ')';
 314              }
 315  
 316              $params['timefrom'] = $timefrom;
 317              $params['timefrom2'] = $timefrom;
 318          }
 319  
 320          return ['where' => [$where], 'params' => $params];
 321      }
 322  
 323      /**
 324       * Generates SQL subquery and parameters for 'to' pagination.
 325       *
 326       * @param string   $field
 327       * @param int      $timeto
 328       * @param int|null $lastseentime
 329       * @param int|null $lastseenid
 330       * @return array|bool
 331       */
 332      protected function timefield_pagination_to(
 333          $field,
 334          $timeto,
 335          $lastseentime = null,
 336          $lastseenid = null
 337      ) {
 338          $where = [];
 339          $params = [];
 340  
 341          if ($lastseentime && $lastseentime > $timeto) {
 342              // The last seen event from this set is after the time sort range which
 343              // means all events in this range have been seen, so we can just return
 344              // early here.
 345              return false;
 346          } else if ($lastseentime && $lastseentime == $timeto) {
 347              $where[] = '((timesort = :timeto1 AND e.id > :timetoid) OR timesort < :timeto2)';
 348              if ($field === 'timestart') {
 349                  $where[] = '((timestart = :timeto1 AND e.id > :timetoid) OR timestart < :timeto2)';
 350              }
 351              $params['timetoid'] = $lastseenid;
 352              $params['timeto1'] = $timeto;
 353              $params['timeto2'] = $timeto;
 354          } else {
 355              $where[] = ($field === 'timestart' ? 'timestart' : 'timesort') . ' <= :timeto';
 356              $params['timeto'] = $timeto;
 357          }
 358  
 359          return ['where' => $where, 'params' => $params];
 360      }
 361  
 362      /**
 363       * Create an event from a database record.
 364       *
 365       * @param \stdClass $record The database record
 366       * @return event_interface|null
 367       */
 368      protected function transform_from_database_record(\stdClass $record) {
 369          return $this->factory->create_instance($record);
 370      }
 371  
 372      /**
 373       * Fetches records from DB.
 374       *
 375       * @param int    $userid
 376       * @param array|null $whereconditions
 377       * @param array  $whereparams
 378       * @param string $ordersql
 379       * @param int    $offset
 380       * @param int    $limitnum
 381       * @return array
 382       */
 383      protected function get_from_db(
 384          $userid,
 385          $whereconditions,
 386          $whereparams,
 387          $ordersql,
 388          $offset,
 389          $limitnum
 390      ) {
 391          return array_values(
 392              $this->retrievalstrategy->get_raw_events(
 393                  [$userid],
 394                  null,
 395                  null,
 396                  null,
 397                  $whereconditions,
 398                  $whereparams,
 399                  $ordersql,
 400                  $offset,
 401                  $limitnum
 402              )
 403          );
 404      }
 405  
 406      /**
 407       * Generates SQL subquery and parameters for event searching.
 408       *
 409       * @param string|null $searchvalue Search value.
 410       * @return array|null
 411       */
 412      protected function generate_search_subquery(?string $searchvalue): ?array {
 413          global $CFG, $DB;
 414          if (!$searchvalue) {
 415              return null;
 416          }
 417  
 418          $parts = preg_split('/\s+/', $searchvalue);
 419          $wherecoursenameconditions = [];
 420          $whereactivitynameconditions = [];
 421          foreach ($parts as $index => $part) {
 422              // Course name searching.
 423              $wherecoursenameconditions[] = $DB->sql_like('c.fullname', ':cfullname' . $index, false);
 424              $params['cfullname'. $index] = '%' . $DB->sql_like_escape($part) . '%';
 425  
 426              // Activity name searching.
 427              $whereactivitynameconditions[] = $DB->sql_like('e.name', ':eventname' . $index, false);
 428              $params['eventname'. $index] = '%' . $DB->sql_like_escape($part) . '%';
 429          }
 430  
 431          // Activity type searching.
 432          $whereconditions[] = $DB->sql_like('e.modulename', ':modulename', false);
 433          $params['modulename'] = '%' . $DB->sql_like_escape($searchvalue) . '%';
 434  
 435          // Activity type searching (localised type name).
 436          require_once($CFG->dirroot . '/course/lib.php');
 437          // Search in modules' singular and plural names.
 438          $modules = array_keys(array_merge(
 439              preg_grep('/' . $searchvalue . '/i', get_module_types_names()) ?: [],
 440              preg_grep('/' . $searchvalue . '/i', get_module_types_names(true)) ?: [],
 441          ));
 442          if ($modules) {
 443              [$insql, $inparams] = $DB->get_in_or_equal($modules, SQL_PARAMS_NAMED, 'exactmodulename');
 444              $whereconditions[] = 'e.modulename ' . $insql;
 445              $params += $inparams;
 446          }
 447  
 448          $whereclause = '(';
 449          $whereclause .= implode(' OR ', $whereconditions);
 450          $whereclause .= ' OR (' . implode(' AND ', $wherecoursenameconditions) . ')';
 451          $whereclause .= ' OR (' . implode(' AND ', $whereactivitynameconditions) . ')';
 452          $whereclause .= ')';
 453  
 454          return ['where' => $whereclause, 'params' => $params];
 455      }
 456  }