PageRenderTime 73ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/calendar/lib.php

http://github.com/moodle/moodle
PHP | 3885 lines | 2371 code | 447 blank | 1067 comment | 673 complexity | 671e32887ce9663ec86541bb86c15376 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. /**
  17. * Calendar extension
  18. *
  19. * @package core_calendar
  20. * @copyright 2004 Greek School Network (http://www.sch.gr), Jon Papaioannou,
  21. * Avgoustos Tsinakos
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. if (!defined('MOODLE_INTERNAL')) {
  25. die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
  26. }
  27. /**
  28. * These are read by the administration component to provide default values
  29. */
  30. /**
  31. * CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD - default value of upcoming event preference
  32. */
  33. define('CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD', 21);
  34. /**
  35. * CALENDAR_DEFAULT_UPCOMING_MAXEVENTS - default value to display the maximum number of upcoming event
  36. */
  37. define('CALENDAR_DEFAULT_UPCOMING_MAXEVENTS', 10);
  38. /**
  39. * CALENDAR_DEFAULT_STARTING_WEEKDAY - default value to display the starting weekday
  40. */
  41. define('CALENDAR_DEFAULT_STARTING_WEEKDAY', 1);
  42. // This is a packed bitfield: day X is "weekend" if $field & (1 << X) is true
  43. // Default value = 65 = 64 + 1 = 2^6 + 2^0 = Saturday & Sunday
  44. /**
  45. * CALENDAR_DEFAULT_WEEKEND - default value for weekend (Saturday & Sunday)
  46. */
  47. define('CALENDAR_DEFAULT_WEEKEND', 65);
  48. /**
  49. * CALENDAR_URL - path to calendar's folder
  50. */
  51. define('CALENDAR_URL', $CFG->wwwroot.'/calendar/');
  52. /**
  53. * CALENDAR_TF_24 - Calendar time in 24 hours format
  54. */
  55. define('CALENDAR_TF_24', '%H:%M');
  56. /**
  57. * CALENDAR_TF_12 - Calendar time in 12 hours format
  58. */
  59. define('CALENDAR_TF_12', '%I:%M %p');
  60. /**
  61. * CALENDAR_EVENT_GLOBAL - Site calendar event types
  62. * @deprecated since 3.8
  63. */
  64. define('CALENDAR_EVENT_GLOBAL', 1);
  65. /**
  66. * CALENDAR_EVENT_SITE - Site calendar event types
  67. */
  68. define('CALENDAR_EVENT_SITE', 1);
  69. /**
  70. * CALENDAR_EVENT_COURSE - Course calendar event types
  71. */
  72. define('CALENDAR_EVENT_COURSE', 2);
  73. /**
  74. * CALENDAR_EVENT_GROUP - group calendar event types
  75. */
  76. define('CALENDAR_EVENT_GROUP', 4);
  77. /**
  78. * CALENDAR_EVENT_USER - user calendar event types
  79. */
  80. define('CALENDAR_EVENT_USER', 8);
  81. /**
  82. * CALENDAR_EVENT_COURSECAT - Course category calendar event types
  83. */
  84. define('CALENDAR_EVENT_COURSECAT', 16);
  85. /**
  86. * CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
  87. */
  88. define('CALENDAR_IMPORT_FROM_FILE', 0);
  89. /**
  90. * CALENDAR_IMPORT_FROM_URL - import the calendar from a URL
  91. */
  92. define('CALENDAR_IMPORT_FROM_URL', 1);
  93. /**
  94. * CALENDAR_IMPORT_EVENT_UPDATED_SKIPPED - imported event was skipped
  95. */
  96. define('CALENDAR_IMPORT_EVENT_SKIPPED', -1);
  97. /**
  98. * CALENDAR_IMPORT_EVENT_UPDATED - imported event was updated
  99. */
  100. define('CALENDAR_IMPORT_EVENT_UPDATED', 1);
  101. /**
  102. * CALENDAR_IMPORT_EVENT_INSERTED - imported event was added by insert
  103. */
  104. define('CALENDAR_IMPORT_EVENT_INSERTED', 2);
  105. /**
  106. * CALENDAR_SUBSCRIPTION_UPDATE - Used to represent update action for subscriptions in various forms.
  107. */
  108. define('CALENDAR_SUBSCRIPTION_UPDATE', 1);
  109. /**
  110. * CALENDAR_SUBSCRIPTION_REMOVE - Used to represent remove action for subscriptions in various forms.
  111. */
  112. define('CALENDAR_SUBSCRIPTION_REMOVE', 2);
  113. /**
  114. * CALENDAR_EVENT_USER_OVERRIDE_PRIORITY - Constant for the user override priority.
  115. */
  116. define('CALENDAR_EVENT_USER_OVERRIDE_PRIORITY', 0);
  117. /**
  118. * CALENDAR_EVENT_TYPE_STANDARD - Standard events.
  119. */
  120. define('CALENDAR_EVENT_TYPE_STANDARD', 0);
  121. /**
  122. * CALENDAR_EVENT_TYPE_ACTION - Action events.
  123. */
  124. define('CALENDAR_EVENT_TYPE_ACTION', 1);
  125. /**
  126. * Manage calendar events.
  127. *
  128. * This class provides the required functionality in order to manage calendar events.
  129. * It was introduced as part of Moodle 2.0 and was created in order to provide a
  130. * better framework for dealing with calendar events in particular regard to file
  131. * handling through the new file API.
  132. *
  133. * @package core_calendar
  134. * @category calendar
  135. * @copyright 2009 Sam Hemelryk
  136. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  137. *
  138. * @property int $id The id within the event table
  139. * @property string $name The name of the event
  140. * @property string $description The description of the event
  141. * @property int $format The format of the description FORMAT_?
  142. * @property int $courseid The course the event is associated with (0 if none)
  143. * @property int $groupid The group the event is associated with (0 if none)
  144. * @property int $userid The user the event is associated with (0 if none)
  145. * @property int $repeatid If this is a repeated event this will be set to the
  146. * id of the original
  147. * @property string $modulename If added by a module this will be the module name
  148. * @property int $instance If added by a module this will be the module instance
  149. * @property string $eventtype The event type
  150. * @property int $timestart The start time as a timestamp
  151. * @property int $timeduration The duration of the event in seconds
  152. * @property int $visible 1 if the event is visible
  153. * @property int $uuid ?
  154. * @property int $sequence ?
  155. * @property int $timemodified The time last modified as a timestamp
  156. */
  157. class calendar_event {
  158. /** @var array An object containing the event properties can be accessed via the magic __get/set methods */
  159. protected $properties = null;
  160. /** @var string The converted event discription with file paths resolved.
  161. * This gets populated when someone requests description for the first time */
  162. protected $_description = null;
  163. /** @var array The options to use with this description editor */
  164. protected $editoroptions = array(
  165. 'subdirs' => false,
  166. 'forcehttps' => false,
  167. 'maxfiles' => -1,
  168. 'maxbytes' => null,
  169. 'trusttext' => false);
  170. /** @var object The context to use with the description editor */
  171. protected $editorcontext = null;
  172. /**
  173. * Instantiates a new event and optionally populates its properties with the data provided.
  174. *
  175. * @param \stdClass $data Optional. An object containing the properties to for
  176. * an event
  177. */
  178. public function __construct($data = null) {
  179. global $CFG, $USER;
  180. // First convert to object if it is not already (should either be object or assoc array).
  181. if (!is_object($data)) {
  182. $data = (object) $data;
  183. }
  184. $this->editoroptions['maxbytes'] = $CFG->maxbytes;
  185. $data->eventrepeats = 0;
  186. if (empty($data->id)) {
  187. $data->id = null;
  188. }
  189. if (!empty($data->subscriptionid)) {
  190. $data->subscription = calendar_get_subscription($data->subscriptionid);
  191. }
  192. // Default to a user event.
  193. if (empty($data->eventtype)) {
  194. $data->eventtype = 'user';
  195. }
  196. // Default to the current user.
  197. if (empty($data->userid)) {
  198. $data->userid = $USER->id;
  199. }
  200. if (!empty($data->timeduration) && is_array($data->timeduration)) {
  201. $data->timeduration = make_timestamp(
  202. $data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'],
  203. $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart;
  204. }
  205. if (!empty($data->description) && is_array($data->description)) {
  206. $data->format = $data->description['format'];
  207. $data->description = $data->description['text'];
  208. } else if (empty($data->description)) {
  209. $data->description = '';
  210. $data->format = editors_get_preferred_format();
  211. }
  212. // Ensure form is defaulted correctly.
  213. if (empty($data->format)) {
  214. $data->format = editors_get_preferred_format();
  215. }
  216. $this->properties = $data;
  217. }
  218. /**
  219. * Magic set method.
  220. *
  221. * Attempts to call a set_$key method if one exists otherwise falls back
  222. * to simply set the property.
  223. *
  224. * @param string $key property name
  225. * @param mixed $value value of the property
  226. */
  227. public function __set($key, $value) {
  228. if (method_exists($this, 'set_'.$key)) {
  229. $this->{'set_'.$key}($value);
  230. }
  231. $this->properties->{$key} = $value;
  232. }
  233. /**
  234. * Magic get method.
  235. *
  236. * Attempts to call a get_$key method to return the property and ralls over
  237. * to return the raw property.
  238. *
  239. * @param string $key property name
  240. * @return mixed property value
  241. * @throws \coding_exception
  242. */
  243. public function __get($key) {
  244. if (method_exists($this, 'get_'.$key)) {
  245. return $this->{'get_'.$key}();
  246. }
  247. if (!property_exists($this->properties, $key)) {
  248. throw new \coding_exception('Undefined property requested');
  249. }
  250. return $this->properties->{$key};
  251. }
  252. /**
  253. * Magic isset method.
  254. *
  255. * PHP needs an isset magic method if you use the get magic method and
  256. * still want empty calls to work.
  257. *
  258. * @param string $key $key property name
  259. * @return bool|mixed property value, false if property is not exist
  260. */
  261. public function __isset($key) {
  262. return !empty($this->properties->{$key});
  263. }
  264. /**
  265. * Calculate the context value needed for an event.
  266. *
  267. * Event's type can be determine by the available value store in $data
  268. * It is important to check for the existence of course/courseid to determine
  269. * the course event.
  270. * Default value is set to CONTEXT_USER
  271. *
  272. * @return \stdClass The context object.
  273. */
  274. protected function calculate_context() {
  275. global $USER, $DB;
  276. $context = null;
  277. if (isset($this->properties->categoryid) && $this->properties->categoryid > 0) {
  278. $context = \context_coursecat::instance($this->properties->categoryid);
  279. } else if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
  280. $context = \context_course::instance($this->properties->courseid);
  281. } else if (isset($this->properties->course) && $this->properties->course > 0) {
  282. $context = \context_course::instance($this->properties->course);
  283. } else if (isset($this->properties->groupid) && $this->properties->groupid > 0) {
  284. $group = $DB->get_record('groups', array('id' => $this->properties->groupid));
  285. $context = \context_course::instance($group->courseid);
  286. } else if (isset($this->properties->userid) && $this->properties->userid > 0
  287. && $this->properties->userid == $USER->id) {
  288. $context = \context_user::instance($this->properties->userid);
  289. } else if (isset($this->properties->userid) && $this->properties->userid > 0
  290. && $this->properties->userid != $USER->id &&
  291. isset($this->properties->instance) && $this->properties->instance > 0) {
  292. $cm = get_coursemodule_from_instance($this->properties->modulename, $this->properties->instance, 0,
  293. false, MUST_EXIST);
  294. $context = \context_course::instance($cm->course);
  295. } else {
  296. $context = \context_user::instance($this->properties->userid);
  297. }
  298. return $context;
  299. }
  300. /**
  301. * Returns the context for this event. The context is calculated
  302. * the first time is is requested and then stored in a member
  303. * variable to be returned each subsequent time.
  304. *
  305. * This is a magical getter function that will be called when
  306. * ever the context property is accessed, e.g. $event->context.
  307. *
  308. * @return context
  309. */
  310. protected function get_context() {
  311. if (!isset($this->properties->context)) {
  312. $this->properties->context = $this->calculate_context();
  313. }
  314. return $this->properties->context;
  315. }
  316. /**
  317. * Returns an array of editoroptions for this event.
  318. *
  319. * @return array event editor options
  320. */
  321. protected function get_editoroptions() {
  322. return $this->editoroptions;
  323. }
  324. /**
  325. * Returns an event description: Called by __get
  326. * Please use $blah = $event->description;
  327. *
  328. * @return string event description
  329. */
  330. protected function get_description() {
  331. global $CFG;
  332. require_once($CFG->libdir . '/filelib.php');
  333. if ($this->_description === null) {
  334. // Check if we have already resolved the context for this event.
  335. if ($this->editorcontext === null) {
  336. // Switch on the event type to decide upon the appropriate context to use for this event.
  337. $this->editorcontext = $this->get_context();
  338. if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
  339. return clean_text($this->properties->description, $this->properties->format);
  340. }
  341. }
  342. // Work out the item id for the editor, if this is a repeated event
  343. // then the files will be associated with the original.
  344. if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
  345. $itemid = $this->properties->repeatid;
  346. } else {
  347. $itemid = $this->properties->id;
  348. }
  349. // Convert file paths in the description so that things display correctly.
  350. $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php',
  351. $this->editorcontext->id, 'calendar', 'event_description', $itemid);
  352. // Clean the text so no nasties get through.
  353. $this->_description = clean_text($this->_description, $this->properties->format);
  354. }
  355. // Finally return the description.
  356. return $this->_description;
  357. }
  358. /**
  359. * Return the number of repeat events there are in this events series.
  360. *
  361. * @return int number of event repeated
  362. */
  363. public function count_repeats() {
  364. global $DB;
  365. if (!empty($this->properties->repeatid)) {
  366. $this->properties->eventrepeats = $DB->count_records('event',
  367. array('repeatid' => $this->properties->repeatid));
  368. // We don't want to count ourselves.
  369. $this->properties->eventrepeats--;
  370. }
  371. return $this->properties->eventrepeats;
  372. }
  373. /**
  374. * Update or create an event within the database
  375. *
  376. * Pass in a object containing the event properties and this function will
  377. * insert it into the database and deal with any associated files
  378. *
  379. * Capability checking should be performed if the user is directly manipulating the event
  380. * and no other capability has been tested. However if the event is not being manipulated
  381. * directly by the user and another capability has been checked for them to do this then
  382. * capabilites should not be checked.
  383. *
  384. * For example if a user is editing an event in the calendar the check should be true,
  385. * but if you are updating an event in an activities settings are changed then the calendar
  386. * capabilites should not be checked.
  387. *
  388. * @see self::create()
  389. * @see self::update()
  390. *
  391. * @param \stdClass $data object of event
  392. * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
  393. * @return bool event updated
  394. */
  395. public function update($data, $checkcapability=true) {
  396. global $DB, $USER;
  397. foreach ($data as $key => $value) {
  398. $this->properties->$key = $value;
  399. }
  400. $this->properties->timemodified = time();
  401. $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description));
  402. // Prepare event data.
  403. $eventargs = array(
  404. 'context' => $this->get_context(),
  405. 'objectid' => $this->properties->id,
  406. 'other' => array(
  407. 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
  408. 'timestart' => $this->properties->timestart,
  409. 'name' => $this->properties->name
  410. )
  411. );
  412. if (empty($this->properties->id) || $this->properties->id < 1) {
  413. if ($checkcapability) {
  414. if (!calendar_add_event_allowed($this->properties)) {
  415. print_error('nopermissiontoupdatecalendar');
  416. }
  417. }
  418. if ($usingeditor) {
  419. switch ($this->properties->eventtype) {
  420. case 'user':
  421. $this->properties->courseid = 0;
  422. $this->properties->course = 0;
  423. $this->properties->groupid = 0;
  424. $this->properties->userid = $USER->id;
  425. break;
  426. case 'site':
  427. $this->properties->courseid = SITEID;
  428. $this->properties->course = SITEID;
  429. $this->properties->groupid = 0;
  430. $this->properties->userid = $USER->id;
  431. break;
  432. case 'course':
  433. $this->properties->groupid = 0;
  434. $this->properties->userid = $USER->id;
  435. break;
  436. case 'category':
  437. $this->properties->groupid = 0;
  438. $this->properties->category = 0;
  439. $this->properties->userid = $USER->id;
  440. break;
  441. case 'group':
  442. $this->properties->userid = $USER->id;
  443. break;
  444. default:
  445. // We should NEVER get here, but just incase we do lets fail gracefully.
  446. $usingeditor = false;
  447. break;
  448. }
  449. // If we are actually using the editor, we recalculate the context because some default values
  450. // were set when calculate_context() was called from the constructor.
  451. if ($usingeditor) {
  452. $this->properties->context = $this->calculate_context();
  453. $this->editorcontext = $this->get_context();
  454. }
  455. $editor = $this->properties->description;
  456. $this->properties->format = $this->properties->description['format'];
  457. $this->properties->description = $this->properties->description['text'];
  458. }
  459. // Insert the event into the database.
  460. $this->properties->id = $DB->insert_record('event', $this->properties);
  461. if ($usingeditor) {
  462. $this->properties->description = file_save_draft_area_files(
  463. $editor['itemid'],
  464. $this->editorcontext->id,
  465. 'calendar',
  466. 'event_description',
  467. $this->properties->id,
  468. $this->editoroptions,
  469. $editor['text'],
  470. $this->editoroptions['forcehttps']);
  471. $DB->set_field('event', 'description', $this->properties->description,
  472. array('id' => $this->properties->id));
  473. }
  474. // Log the event entry.
  475. $eventargs['objectid'] = $this->properties->id;
  476. $eventargs['context'] = $this->get_context();
  477. $event = \core\event\calendar_event_created::create($eventargs);
  478. $event->trigger();
  479. $repeatedids = array();
  480. if (!empty($this->properties->repeat)) {
  481. $this->properties->repeatid = $this->properties->id;
  482. $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id));
  483. $eventcopy = clone($this->properties);
  484. unset($eventcopy->id);
  485. $timestart = new \DateTime('@' . $eventcopy->timestart);
  486. $timestart->setTimezone(\core_date::get_user_timezone_object());
  487. for ($i = 1; $i < $eventcopy->repeats; $i++) {
  488. $timestart->add(new \DateInterval('P7D'));
  489. $eventcopy->timestart = $timestart->getTimestamp();
  490. // Get the event id for the log record.
  491. $eventcopyid = $DB->insert_record('event', $eventcopy);
  492. // If the context has been set delete all associated files.
  493. if ($usingeditor) {
  494. $fs = get_file_storage();
  495. $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description',
  496. $this->properties->id);
  497. foreach ($files as $file) {
  498. $fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file);
  499. }
  500. }
  501. $repeatedids[] = $eventcopyid;
  502. // Trigger an event.
  503. $eventargs['objectid'] = $eventcopyid;
  504. $eventargs['other']['timestart'] = $eventcopy->timestart;
  505. $event = \core\event\calendar_event_created::create($eventargs);
  506. $event->trigger();
  507. }
  508. }
  509. return true;
  510. } else {
  511. if ($checkcapability) {
  512. if (!calendar_edit_event_allowed($this->properties)) {
  513. print_error('nopermissiontoupdatecalendar');
  514. }
  515. }
  516. if ($usingeditor) {
  517. if ($this->editorcontext !== null) {
  518. $this->properties->description = file_save_draft_area_files(
  519. $this->properties->description['itemid'],
  520. $this->editorcontext->id,
  521. 'calendar',
  522. 'event_description',
  523. $this->properties->id,
  524. $this->editoroptions,
  525. $this->properties->description['text'],
  526. $this->editoroptions['forcehttps']);
  527. } else {
  528. $this->properties->format = $this->properties->description['format'];
  529. $this->properties->description = $this->properties->description['text'];
  530. }
  531. }
  532. $event = $DB->get_record('event', array('id' => $this->properties->id));
  533. $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
  534. if ($updaterepeated) {
  535. $sqlset = 'name = ?,
  536. description = ?,
  537. timeduration = ?,
  538. timemodified = ?,
  539. groupid = ?,
  540. courseid = ?';
  541. // Note: Group and course id may not be set. If not, keep their current values.
  542. $params = [
  543. $this->properties->name,
  544. $this->properties->description,
  545. $this->properties->timeduration,
  546. time(),
  547. isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
  548. isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
  549. ];
  550. // Note: Only update start date, if it was changed by the user.
  551. if ($this->properties->timestart != $event->timestart) {
  552. $timestartoffset = $this->properties->timestart - $event->timestart;
  553. $sqlset .= ', timestart = timestart + ?';
  554. $params[] = $timestartoffset;
  555. }
  556. // Note: Only update location, if it was changed by the user.
  557. $updatelocation = (!empty($this->properties->location) && $this->properties->location !== $event->location);
  558. if ($updatelocation) {
  559. $sqlset .= ', location = ?';
  560. $params[] = $this->properties->location;
  561. }
  562. // Update all.
  563. $sql = "UPDATE {event}
  564. SET $sqlset
  565. WHERE repeatid = ?";
  566. $params[] = $event->repeatid;
  567. $DB->execute($sql, $params);
  568. // Trigger an update event for each of the calendar event.
  569. $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', '*');
  570. foreach ($events as $calendarevent) {
  571. $eventargs['objectid'] = $calendarevent->id;
  572. $eventargs['other']['timestart'] = $calendarevent->timestart;
  573. $event = \core\event\calendar_event_updated::create($eventargs);
  574. $event->add_record_snapshot('event', $calendarevent);
  575. $event->trigger();
  576. }
  577. } else {
  578. $DB->update_record('event', $this->properties);
  579. $event = self::load($this->properties->id);
  580. $this->properties = $event->properties();
  581. // Trigger an update event.
  582. $event = \core\event\calendar_event_updated::create($eventargs);
  583. $event->add_record_snapshot('event', $this->properties);
  584. $event->trigger();
  585. }
  586. return true;
  587. }
  588. }
  589. /**
  590. * Deletes an event and if selected an repeated events in the same series
  591. *
  592. * This function deletes an event, any associated events if $deleterepeated=true,
  593. * and cleans up any files associated with the events.
  594. *
  595. * @see self::delete()
  596. *
  597. * @param bool $deleterepeated delete event repeatedly
  598. * @return bool succession of deleting event
  599. */
  600. public function delete($deleterepeated = false) {
  601. global $DB;
  602. // If $this->properties->id is not set then something is wrong.
  603. if (empty($this->properties->id)) {
  604. debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER);
  605. return false;
  606. }
  607. $calevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST);
  608. // Delete the event.
  609. $DB->delete_records('event', array('id' => $this->properties->id));
  610. // Trigger an event for the delete action.
  611. $eventargs = array(
  612. 'context' => $this->get_context(),
  613. 'objectid' => $this->properties->id,
  614. 'other' => array(
  615. 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
  616. 'timestart' => $this->properties->timestart,
  617. 'name' => $this->properties->name
  618. ));
  619. $event = \core\event\calendar_event_deleted::create($eventargs);
  620. $event->add_record_snapshot('event', $calevent);
  621. $event->trigger();
  622. // If we are deleting parent of a repeated event series, promote the next event in the series as parent.
  623. if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) {
  624. $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC",
  625. array($this->properties->id), IGNORE_MULTIPLE);
  626. if (!empty($newparent)) {
  627. $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?",
  628. array($newparent, $this->properties->id));
  629. // Get all records where the repeatid is the same as the event being removed.
  630. $events = $DB->get_records('event', array('repeatid' => $newparent));
  631. // For each of the returned events trigger an update event.
  632. foreach ($events as $calendarevent) {
  633. // Trigger an event for the update.
  634. $eventargs['objectid'] = $calendarevent->id;
  635. $eventargs['other']['timestart'] = $calendarevent->timestart;
  636. $event = \core\event\calendar_event_updated::create($eventargs);
  637. $event->add_record_snapshot('event', $calendarevent);
  638. $event->trigger();
  639. }
  640. }
  641. }
  642. // If the editor context hasn't already been set then set it now.
  643. if ($this->editorcontext === null) {
  644. $this->editorcontext = $this->get_context();
  645. }
  646. // If the context has been set delete all associated files.
  647. if ($this->editorcontext !== null) {
  648. $fs = get_file_storage();
  649. $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id);
  650. foreach ($files as $file) {
  651. $file->delete();
  652. }
  653. }
  654. // If we need to delete repeated events then we will fetch them all and delete one by one.
  655. if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
  656. // Get all records where the repeatid is the same as the event being removed.
  657. $events = $DB->get_records('event', array('repeatid' => $this->properties->repeatid));
  658. // For each of the returned events populate an event object and call delete.
  659. // make sure the arg passed is false as we are already deleting all repeats.
  660. foreach ($events as $event) {
  661. $event = new calendar_event($event);
  662. $event->delete(false);
  663. }
  664. }
  665. return true;
  666. }
  667. /**
  668. * Fetch all event properties.
  669. *
  670. * This function returns all of the events properties as an object and optionally
  671. * can prepare an editor for the description field at the same time. This is
  672. * designed to work when the properties are going to be used to set the default
  673. * values of a moodle forms form.
  674. *
  675. * @param bool $prepareeditor If set to true a editor is prepared for use with
  676. * the mforms editor element. (for description)
  677. * @return \stdClass Object containing event properties
  678. */
  679. public function properties($prepareeditor = false) {
  680. global $DB;
  681. // First take a copy of the properties. We don't want to actually change the
  682. // properties or we'd forever be converting back and forwards between an
  683. // editor formatted description and not.
  684. $properties = clone($this->properties);
  685. // Clean the description here.
  686. $properties->description = clean_text($properties->description, $properties->format);
  687. // If set to true we need to prepare the properties for use with an editor
  688. // and prepare the file area.
  689. if ($prepareeditor) {
  690. // We may or may not have a property id. If we do then we need to work
  691. // out the context so we can copy the existing files to the draft area.
  692. if (!empty($properties->id)) {
  693. if ($properties->eventtype === 'site') {
  694. // Site context.
  695. $this->editorcontext = $this->get_context();
  696. } else if ($properties->eventtype === 'user') {
  697. // User context.
  698. $this->editorcontext = $this->get_context();
  699. } else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') {
  700. // First check the course is valid.
  701. $course = $DB->get_record('course', array('id' => $properties->courseid));
  702. if (!$course) {
  703. print_error('invalidcourse');
  704. }
  705. // Course context.
  706. $this->editorcontext = $this->get_context();
  707. // We have a course and are within the course context so we had
  708. // better use the courses max bytes value.
  709. $this->editoroptions['maxbytes'] = $course->maxbytes;
  710. } else if ($properties->eventtype === 'category') {
  711. // First check the course is valid.
  712. \core_course_category::get($properties->categoryid, MUST_EXIST, true);
  713. // Course context.
  714. $this->editorcontext = $this->get_context();
  715. } else {
  716. // If we get here we have a custom event type as used by some
  717. // modules. In this case the event will have been added by
  718. // code and we won't need the editor.
  719. $this->editoroptions['maxbytes'] = 0;
  720. $this->editoroptions['maxfiles'] = 0;
  721. }
  722. if (empty($this->editorcontext) || empty($this->editorcontext->id)) {
  723. $contextid = false;
  724. } else {
  725. // Get the context id that is what we really want.
  726. $contextid = $this->editorcontext->id;
  727. }
  728. } else {
  729. // If we get here then this is a new event in which case we don't need a
  730. // context as there is no existing files to copy to the draft area.
  731. $contextid = null;
  732. }
  733. // If the contextid === false we don't support files so no preparing
  734. // a draft area.
  735. if ($contextid !== false) {
  736. // Just encase it has already been submitted.
  737. $draftiddescription = file_get_submitted_draft_itemid('description');
  738. // Prepare the draft area, this copies existing files to the draft area as well.
  739. $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar',
  740. 'event_description', $properties->id, $this->editoroptions, $properties->description);
  741. } else {
  742. $draftiddescription = 0;
  743. }
  744. // Structure the description field as the editor requires.
  745. $properties->description = array('text' => $properties->description, 'format' => $properties->format,
  746. 'itemid' => $draftiddescription);
  747. }
  748. // Finally return the properties.
  749. return $properties;
  750. }
  751. /**
  752. * Toggles the visibility of an event
  753. *
  754. * @param null|bool $force If it is left null the events visibility is flipped,
  755. * If it is false the event is made hidden, if it is true it
  756. * is made visible.
  757. * @return bool if event is successfully updated, toggle will be visible
  758. */
  759. public function toggle_visibility($force = null) {
  760. global $DB;
  761. // Set visible to the default if it is not already set.
  762. if (empty($this->properties->visible)) {
  763. $this->properties->visible = 1;
  764. }
  765. if ($force === true || ($force !== false && $this->properties->visible == 0)) {
  766. // Make this event visible.
  767. $this->properties->visible = 1;
  768. } else {
  769. // Make this event hidden.
  770. $this->properties->visible = 0;
  771. }
  772. // Update the database to reflect this change.
  773. $success = $DB->set_field('event', 'visible', $this->properties->visible, array('id' => $this->properties->id));
  774. $calendarevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST);
  775. // Prepare event data.
  776. $eventargs = array(
  777. 'context' => $this->get_context(),
  778. 'objectid' => $this->properties->id,
  779. 'other' => array(
  780. 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
  781. 'timestart' => $this->properties->timestart,
  782. 'name' => $this->properties->name
  783. )
  784. );
  785. $event = \core\event\calendar_event_updated::create($eventargs);
  786. $event->add_record_snapshot('event', $calendarevent);
  787. $event->trigger();
  788. return $success;
  789. }
  790. /**
  791. * Returns an event object when provided with an event id.
  792. *
  793. * This function makes use of MUST_EXIST, if the event id passed in is invalid
  794. * it will result in an exception being thrown.
  795. *
  796. * @param int|object $param event object or event id
  797. * @return calendar_event
  798. */
  799. public static function load($param) {
  800. global $DB;
  801. if (is_object($param)) {
  802. $event = new calendar_event($param);
  803. } else {
  804. $event = $DB->get_record('event', array('id' => (int)$param), '*', MUST_EXIST);
  805. $event = new calendar_event($event);
  806. }
  807. return $event;
  808. }
  809. /**
  810. * Creates a new event and returns an event object.
  811. *
  812. * Capability checking should be performed if the user is directly creating the event
  813. * and no other capability has been tested. However if the event is not being created
  814. * directly by the user and another capability has been checked for them to do this then
  815. * capabilites should not be checked.
  816. *
  817. * For example if a user is creating an event in the calendar the check should be true,
  818. * but if you are creating an event in an activity when it is created then the calendar
  819. * capabilites should not be checked.
  820. *
  821. * @param \stdClass|array $properties An object containing event properties
  822. * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
  823. * @throws \coding_exception
  824. *
  825. * @return calendar_event|bool The event object or false if it failed
  826. */
  827. public static function create($properties, $checkcapability = true) {
  828. if (is_array($properties)) {
  829. $properties = (object)$properties;
  830. }
  831. if (!is_object($properties)) {
  832. throw new \coding_exception('When creating an event properties should be either an object or an assoc array');
  833. }
  834. $event = new calendar_event($properties);
  835. if ($event->update($properties, $checkcapability)) {
  836. return $event;
  837. } else {
  838. return false;
  839. }
  840. }
  841. /**
  842. * Format the text using the external API.
  843. *
  844. * This function should we used when text formatting is required in external functions.
  845. *
  846. * @return array an array containing the text formatted and the text format
  847. */
  848. public function format_external_text() {
  849. if ($this->editorcontext === null) {
  850. // Switch on the event type to decide upon the appropriate context to use for this event.
  851. $this->editorcontext = $this->get_context();
  852. if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
  853. // We don't have a context here, do a normal format_text.
  854. return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
  855. }
  856. }
  857. // Work out the item id for the editor, if this is a repeated event then the files will be associated with the original.
  858. if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
  859. $itemid = $this->properties->repeatid;
  860. } else {
  861. $itemid = $this->properties->id;
  862. }
  863. return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id,
  864. 'calendar', 'event_description', $itemid);
  865. }
  866. }
  867. /**
  868. * Calendar information class
  869. *
  870. * This class is used simply to organise the information pertaining to a calendar
  871. * and is used primarily to make information easily available.
  872. *
  873. * @package core_calendar
  874. * @category calendar
  875. * @copyright 2010 Sam Hemelryk
  876. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  877. */
  878. class calendar_information {
  879. /**
  880. * @var int The timestamp
  881. *
  882. * Rather than setting the day, month and year we will set a timestamp which will be able
  883. * to be used by multiple calendars.
  884. */
  885. public $time;
  886. /** @var int A course id */
  887. public $courseid = null;
  888. /** @var array An array of categories */
  889. public $categories = array();
  890. /** @var int The current category */
  891. public $categoryid = null;
  892. /** @var array An array of courses */
  893. public $courses = array();
  894. /** @var array An array of groups */
  895. public $groups = array();
  896. /** @var array An array of users */
  897. public $users = array();
  898. /** @var context The anticipated context that the calendar is viewed in */
  899. public $context = null;
  900. /**
  901. * Creates a new instance
  902. *
  903. * @param int $day the number of the day
  904. * @param int $month the number of the month
  905. * @param int $year the number of the year
  906. * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
  907. * and $calyear to support multiple calendars
  908. */
  909. public function __construct($day = 0, $month = 0, $year = 0, $time = 0) {
  910. // If a day, month and year were passed then convert it to a timestamp. If these were passed
  911. // then we can assume the day, month and year are passed as Gregorian, as no where in core
  912. // should we be passing these values rather than the time. This is done for BC.
  913. if (!empty($day) || !empty($month) || !empty($year)) {
  914. $date = usergetdate(time());
  915. if (empty($day)) {
  916. $day = $date['mday'];
  917. }
  918. if (empty($month)) {
  919. $month = $date['mon'];
  920. }
  921. if (empty($year)) {
  922. $year = $date['year'];
  923. }
  924. if (checkdate($month, $day, $year)) {
  925. $time = make_timestamp($year, $month, $day);
  926. } else {
  927. $time = time();
  928. }
  929. }
  930. $this->set_time($time);
  931. }
  932. /**
  933. * Creates and set up a instance.
  934. *
  935. * @param int $time the unixtimestamp representing the date we want to view.
  936. * @param int $courseid The ID of the course the user wishes to view.
  937. * @param int $categoryid The ID of the category the user wishes to view
  938. * If a courseid is specified, this value is ignored.
  939. * @return calendar_information
  940. */
  941. public static function create($time, int $courseid, int $categoryid = null) : calendar_information {
  942. $calendar = new static(0, 0, 0, $time);
  943. if ($courseid != SITEID && !empty($courseid)) {
  944. // Course ID must be valid and existing.
  945. $course = get_course($courseid);
  946. $calendar->context = context_course::instance($course->id);
  947. if (!$course->visible && !is_role_switched($course->id)) {
  948. require_capability('moodle/course:viewhiddencourses', $calendar->context);
  949. }
  950. $courses = [$course->id => $course];
  951. $category = (\core_course_category::get($course->category, MUST_EXIST, true))->get_db_record();
  952. } else if (!empty($categoryid)) {
  953. $course = get_site();
  954. $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
  955. // Filter available courses to those within this category or it's children.
  956. $ids = [$categoryid];
  957. $category = \core_course_category::get($categoryid);
  958. $ids = array_merge($ids, array_keys($category->get_children()));
  959. $courses = array_filter($courses, function($course) use ($ids) {
  960. return array_search($course->category, $ids) !== false;
  961. });
  962. $category = $category->get_db_record();
  963. $calendar->context = context_coursecat::instance($categoryid);
  964. } else {
  965. $course = get_site();
  966. $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
  967. $category = null;
  968. $calendar->context = context_system::instance();
  969. }
  970. $calendar->set_sources($course, $courses, $category);
  971. return $calendar;
  972. }
  973. /**
  974. * Set the time period of this instance.
  975. *
  976. * @param int $time the unixtimestamp representing the date we want to view.
  977. * @return $this
  978. */
  979. public function set_time($time = null) {
  980. if (empty($time)) {
  981. $this->time = time();
  982. } else {
  983. $this->time = $time;
  984. }
  985. return $this;
  986. }
  987. /**
  988. * Initialize calendar information
  989. *
  990. * @deprecated 3.4
  991. * @param stdClass $course object
  992. * @param array $coursestoload An array of courses [$course->id => $course]
  993. * @param bool $ignorefilters options to use filter
  994. */
  995. public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) {
  996. debugging('The prepare_for_view() function has been deprecated. Please update your code to use set_sources()',
  997. DEBUG_DEVELOPER);
  998. $this->set_sources($course, $coursestoload);
  999. }
  1000. /**
  1001. * Set the sources for events within the calendar.
  1002. *
  1003. * If no category is provided, then the category path for the current
  1004. * course will be used.
  1005. *
  1006. * @param stdClass $course The current course being viewed.
  1007. * @param stdClass[] $courses The list of all courses currently accessible.
  1008. * @param stdClass $category The current category to show.
  1009. */
  1010. public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
  1011. global $USER;
  1012. // A cousre must always be specified.
  1013. $this->course = $course;
  1014. $this->courseid = $course->id;
  1015. list($courseids, $group, $user) = calendar_set_filters($courses);
  1016. $this->courses = $courseids;
  1017. $this->groups = $group;
  1018. $this->users = $user;
  1019. // Do not show category events by default.
  1020. $this->categoryid = null;
  1021. $this->categories = null;
  1022. // Determine the correct category information to show.
  1023. // When called with a course, the category of that course is usually included too.
  1024. // When a category was specifically requested, it should be requested with the site id.
  1025. if (SITEID !== $this->courseid) {
  1026. // A specific course was requested.
  1027. // Fetch the category that this course is in, along with all parents.
  1028. // Do not include child categories of this category, as the user many not have enrolments in those siblings or children.
  1029. $category = \core_course_category::get($course->category, MUST_EXIST, true);
  1030. $this->categoryid = $category->id;
  1031. $this->categories = $category->get_parents();
  1032. $this->categories[] = $category->id;
  1033. } else if (null !== $category && $category->id > 0) {
  1034. // A specific category was requested.
  1035. // Fetch all parents of this category, along with all children too.
  1036. $category = \core_course_category::get($category->id);
  1037. $this->categoryid = $category->id;
  1038. // Build the category list.
  1039. // This includes the current category.
  1040. $this->categories = $category->get_parents();
  1041. $this->categories[] = $category->id;
  1042. $this->categories = array_merge($this->categories, $category->get_all_children_ids());
  1043. } else if (SITEID === $this->courseid) {
  1044. // The site was requested.
  1045. // Fetch all categories where this user has any enrolment, and all categories that this user can manage.
  1046. // Grab the list of categories that this user has courses in.
  1047. $coursecategories = array_flip(array_map(function($course) {
  1048. return $course->category;
  1049. }, $courses));
  1050. $calcatcache = cache::make('core', 'calendar_categories');
  1051. $this->categories = $calcatcache->get('site');
  1052. if ($this->categories === false) {
  1053. // Use the category id as the key in the following array. That way we do not have to remove duplicates.
  1054. $categories = [];
  1055. foreach (\core_course_category::get_all() as $category) {
  1056. if (isset($coursecategories[$category->id]) ||
  1057. has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
  1058. // If the user has access to a course in this category or can manage the category,
  1059. // then they can see all parent categories too.
  1060. $categories[$category->id] = true;
  1061. foreach ($category->get_parents() as $catid) {
  1062. $categories[$catid] = true;
  1063. }
  1064. }
  1065. }
  1066. $this->categories = array_keys($categories);
  1067. $calcatcache->set('site', $this->categories);
  1068. }
  1069. }
  1070. }
  1071. /**
  1072. * Ensures the date for the calendar is correct and either sets it to now
  1073. * or throws a moodle_exception if not
  1074. *
  1075. * @param bool $defaultonow use current time
  1076. * @throws moodle_exception
  1077. * @return bool validation of checkdate
  1078. */
  1079. public function checkdate($defaultonow = true) {
  1080. if (!checkdate($this->month, $this->day, $this->year)) {
  1081. if ($defaultonow) {
  1082. $now = usergetdate(time());
  1083. $this->day = intval($now['mday']);
  1084. $this->month = intval($now['mon']);
  1085. $this->year = intval($now['year']);
  1086. return true;
  1087. } else {
  1088. throw new moodle_exception('invaliddate');
  1089. }
  1090. }
  1091. return true;
  1092. }
  1093. /**
  1094. * Gets todays timestamp for the calendar
  1095. *
  1096. * @return int today timestamp
  1097. */
  1098. public function timestamp_today() {
  1099. return $this->time;
  1100. }
  1101. /**
  1102. * Gets tomorrows timestamp for the calendar
  1103. *
  1104. * @return int tomorrow timestamp
  1105. */
  1106. public function timestamp_tomorrow() {
  1107. return strtotime('+1 day', $this->time);
  1108. }
  1109. /**
  1110. * Adds the pretend blocks for the calendar
  1111. *
  1112. * @param core_calendar_renderer $renderer
  1113. * @param bool $showfilters display filters, false is set as default
  1114. * @param string|null $view preference view options (eg: day, month, upcoming)
  1115. */
  1116. public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) {
  1117. if ($showfilters) {
  1118. $filters = new block_contents();
  1119. $filters->content = $renderer->event_filter();
  1120. $filters->footer = '';
  1121. $filters->title = get_string('eventskey', 'calendar');
  1122. $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT);
  1123. }
  1124. $block = new block_contents;
  1125. $block->content = $renderer->fake_block_threemonths($this);
  1126. $block->footer = '';
  1127. $block->title = get_string('monthlyview', 'calendar');
  1128. $renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT);
  1129. }
  1130. }
  1131. /**
  1132. * Get calendar events.
  1133. *
  1134. * @param int $tstart Start time of time range for events
  1135. * @param int $tend End time of time range for events
  1136. * @param array|int|boolean $users array of users, user id or boolean for all/no user events
  1137. * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
  1138. * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
  1139. * @param boolean $withduration whether only events starting within time range selected
  1140. * or events in progress/already started selected as well
  1141. * @param boolean $ignorehidden whether to select only visible events or all events
  1142. * @param array|int|boolean $categories array of categories, category id or boolean for all/no course events
  1143. * @return array $events of selected events or an empty array if there aren't any (or there was an error)
  1144. */
  1145. function calendar_get_events($tstart, $tend, $users, $groups, $courses,
  1146. $withduration = true, $ignorehidden = true, $categories = []) {
  1147. global $DB;
  1148. $whereclause = '';
  1149. $params = array();
  1150. // Quick test.
  1151. if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
  1152. return array();
  1153. }
  1154. if ((is_array($users) && !empty($users)) or is_numeric($users)) {
  1155. // Events from a number of users
  1156. if(!empty($whereclause)) $whereclause .= ' OR';
  1157. list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
  1158. $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
  1159. $params = array_merge($params, $inparamsusers);
  1160. } else if($users === true) {
  1161. // Events from ALL users
  1162. if(!empty($whereclause)) $whereclause .= ' OR';
  1163. $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)';
  1164. } else if($users === false) {
  1165. // No user at all, do nothing
  1166. }
  1167. if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) {
  1168. // Events from a number of groups
  1169. if(!empty($whereclause)) $whereclause .= ' OR';
  1170. list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
  1171. $whereclause .= " e.groupid $insqlgroups ";
  1172. $params = array_merge($params, $inparamsgroups);
  1173. } else if($groups === true) {
  1174. // Events from ALL groups
  1175. if(!empty($whereclause)) $whereclause .= ' OR ';
  1176. $whereclause .= ' e.groupid != 0';
  1177. }
  1178. // boolean false (no groups at all): we don't need to do anything
  1179. if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) {
  1180. if(!empty($whereclause)) $whereclause .= ' OR';
  1181. list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
  1182. $whereclause .= " (e.groupid = 0 AND e.courseid $insqlcourses)";
  1183. $params = array_merge($params, $inparamscourses);
  1184. } else if ($courses === true) {
  1185. // Events from ALL courses
  1186. if(!empty($whereclause)) $whereclause .= ' OR';
  1187. $whereclause .= ' (e.groupid = 0 AND e.courseid != 0)';
  1188. }
  1189. if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) {
  1190. if (!empty($whereclause)) {
  1191. $whereclause .= ' OR';
  1192. }
  1193. list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
  1194. $whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
  1195. $params = array_merge($params, $inparamscategories);
  1196. } else if ($categories === true) {
  1197. // Events from ALL categories.
  1198. if (!empty($whereclause)) {
  1199. $whereclause .= ' OR';
  1200. }
  1201. $whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)';
  1202. }
  1203. // Security check: if, by now, we have NOTHING in $whereclause, then it means
  1204. // that NO event-selecting clauses were defined. Thus, we won't be returning ANY
  1205. // events no matter what. Allowing the code to proceed might return a completely
  1206. // valid query with only time constraints, thus selecting ALL events in that time frame!
  1207. if(empty($whereclause)) {
  1208. return array();
  1209. }
  1210. if($withduration) {
  1211. $timeclause = '(e.timestart >= '.$tstart.' OR e.timestart + e.timeduration > '.$tstart.') AND e.timestart <= '.$tend;
  1212. }
  1213. else {
  1214. $timeclause = 'e.timestart >= '.$tstart.' AND e.timestart <= '.$tend;
  1215. }
  1216. if(!empty($whereclause)) {
  1217. // We have additional constraints
  1218. $whereclause = $timeclause.' AND ('.$whereclause.')';
  1219. }
  1220. else {
  1221. // Just basic time filtering
  1222. $whereclause = $timeclause;
  1223. }
  1224. if ($ignorehidden) {
  1225. $whereclause .= ' AND e.visible = 1';
  1226. }
  1227. $sql = "SELECT e.*
  1228. FROM {event} e
  1229. LEFT JOIN {modules} m ON e.modulename = m.name
  1230. -- Non visible modules will have a value of 0.
  1231. WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause
  1232. ORDER BY e.timestart";
  1233. $events = $DB->get_records_sql($sql, $params);
  1234. if ($events === false) {
  1235. $events = array();
  1236. }
  1237. return $events;
  1238. }
  1239. /**
  1240. * Return the days of the week.
  1241. *
  1242. * @return array array of days
  1243. */
  1244. function calendar_get_days() {
  1245. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1246. return $calendartype->get_weekdays();
  1247. }
  1248. /**
  1249. * Get the subscription from a given id.
  1250. *
  1251. * @since Moodle 2.5
  1252. * @param int $id id of the subscription
  1253. * @return stdClass Subscription record from DB
  1254. * @throws moodle_exception for an invalid id
  1255. */
  1256. function calendar_get_subscription($id) {
  1257. global $DB;
  1258. $cache = \cache::make('core', 'calendar_subscriptions');
  1259. $subscription = $cache->get($id);
  1260. if (empty($subscription)) {
  1261. $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST);
  1262. $cache->set($id, $subscription);
  1263. }
  1264. return $subscription;
  1265. }
  1266. /**
  1267. * Gets the first day of the week.
  1268. *
  1269. * Used to be define('CALENDAR_STARTING_WEEKDAY', blah);
  1270. *
  1271. * @return int
  1272. */
  1273. function calendar_get_starting_weekday() {
  1274. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1275. return $calendartype->get_starting_weekday();
  1276. }
  1277. /**
  1278. * Get a HTML link to a course.
  1279. *
  1280. * @param int|stdClass $course the course id or course object
  1281. * @return string a link to the course (as HTML); empty if the course id is invalid
  1282. */
  1283. function calendar_get_courselink($course) {
  1284. if (!$course) {
  1285. return '';
  1286. }
  1287. if (!is_object($course)) {
  1288. $course = calendar_get_course_cached($coursecache, $course);
  1289. }
  1290. $context = \context_course::instance($course->id);
  1291. $fullname = format_string($course->fullname, true, array('context' => $context));
  1292. $url = new \moodle_url('/course/view.php', array('id' => $course->id));
  1293. $link = \html_writer::link($url, $fullname);
  1294. return $link;
  1295. }
  1296. /**
  1297. * Get current module cache.
  1298. *
  1299. * Only use this method if you do not know courseid. Otherwise use:
  1300. * get_fast_modinfo($courseid)->instances[$modulename][$instance]
  1301. *
  1302. * @param array $modulecache in memory module cache
  1303. * @param string $modulename name of the module
  1304. * @param int $instance module instance number
  1305. * @return stdClass|bool $module information
  1306. */
  1307. function calendar_get_module_cached(&$modulecache, $modulename, $instance) {
  1308. if (!isset($modulecache[$modulename . '_' . $instance])) {
  1309. $modulecache[$modulename . '_' . $instance] = get_coursemodule_from_instance($modulename, $instance);
  1310. }
  1311. return $modulecache[$modulename . '_' . $instance];
  1312. }
  1313. /**
  1314. * Get current course cache.
  1315. *
  1316. * @param array $coursecache list of course cache
  1317. * @param int $courseid id of the course
  1318. * @return stdClass $coursecache[$courseid] return the specific course cache
  1319. */
  1320. function calendar_get_course_cached(&$coursecache, $courseid) {
  1321. if (!isset($coursecache[$courseid])) {
  1322. $coursecache[$courseid] = get_course($courseid);
  1323. }
  1324. return $coursecache[$courseid];
  1325. }
  1326. /**
  1327. * Get group from groupid for calendar display
  1328. *
  1329. * @param int $groupid
  1330. * @return stdClass group object with fields 'id', 'name' and 'courseid'
  1331. */
  1332. function calendar_get_group_cached($groupid) {
  1333. static $groupscache = array();
  1334. if (!isset($groupscache[$groupid])) {
  1335. $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid');
  1336. }
  1337. return $groupscache[$groupid];
  1338. }
  1339. /**
  1340. * Add calendar event metadata
  1341. *
  1342. * @param stdClass $event event info
  1343. * @return stdClass $event metadata
  1344. */
  1345. function calendar_add_event_metadata($event) {
  1346. global $CFG, $OUTPUT;
  1347. // Support multilang in event->name.
  1348. $event->name = format_string($event->name, true);
  1349. if (!empty($event->modulename)) { // Activity event.
  1350. // The module name is set. I will assume that it has to be displayed, and
  1351. // also that it is an automatically-generated event. And of course that the
  1352. // instace id and modulename are set correctly.
  1353. $instances = get_fast_modinfo($event->courseid)->get_instances_of($event->modulename);
  1354. if (!array_key_exists($event->instance, $instances)) {
  1355. return;
  1356. }
  1357. $module = $instances[$event->instance];
  1358. $modulename = $module->get_module_type_name(false);
  1359. if (get_string_manager()->string_exists($event->eventtype, $event->modulename)) {
  1360. // Will be used as alt text if the event icon.
  1361. $eventtype = get_string($event->eventtype, $event->modulename);
  1362. } else {
  1363. $eventtype = '';
  1364. }
  1365. $event->icon = '<img src="' . s($module->get_icon_url()) . '" alt="' . s($eventtype) .
  1366. '" title="' . s($modulename) . '" class="icon" />';
  1367. $event->referer = html_writer::link($module->url, $event->name);
  1368. $event->courselink = calendar_get_courselink($module->get_course());
  1369. $event->cmid = $module->id;
  1370. } else if ($event->courseid == SITEID) { // Site event.
  1371. $event->icon = '<img src="' . $OUTPUT->image_url('i/siteevent') . '" alt="' .
  1372. get_string('siteevent', 'calendar') . '" class="icon" />';
  1373. $event->cssclass = 'calendar_event_site';
  1374. } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event.
  1375. $event->icon = '<img src="' . $OUTPUT->image_url('i/courseevent') . '" alt="' .
  1376. get_string('courseevent', 'calendar') . '" class="icon" />';
  1377. $event->courselink = calendar_get_courselink($event->courseid);
  1378. $event->cssclass = 'calendar_event_course';
  1379. } else if ($event->groupid) { // Group event.
  1380. if ($group = calendar_get_group_cached($event->groupid)) {
  1381. $groupname = format_string($group->name, true, \context_course::instance($group->courseid));
  1382. } else {
  1383. $groupname = '';
  1384. }
  1385. $event->icon = \html_writer::empty_tag('image', array('src' => $OUTPUT->image_url('i/groupevent'),
  1386. 'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon'));
  1387. $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname;
  1388. $event->cssclass = 'calendar_event_group';
  1389. } else if ($event->userid) { // User event.
  1390. $event->icon = '<img src="' . $OUTPUT->image_url('i/userevent') . '" alt="' .
  1391. get_string('userevent', 'calendar') . '" class="icon" />';
  1392. $event->cssclass = 'calendar_event_user';
  1393. }
  1394. return $event;
  1395. }
  1396. /**
  1397. * Get calendar events by id.
  1398. *
  1399. * @since Moodle 2.5
  1400. * @param array $eventids list of event ids
  1401. * @return array Array of event entries, empty array if nothing found
  1402. */
  1403. function calendar_get_events_by_id($eventids) {
  1404. global $DB;
  1405. if (!is_array($eventids) || empty($eventids)) {
  1406. return array();
  1407. }
  1408. list($wheresql, $params) = $DB->get_in_or_equal($eventids);
  1409. $wheresql = "id $wheresql";
  1410. return $DB->get_records_select('event', $wheresql, $params);
  1411. }
  1412. /**
  1413. * Get control options for calendar.
  1414. *
  1415. * @param string $type of calendar
  1416. * @param array $data calendar information
  1417. * @return string $content return available control for the calender in html
  1418. */
  1419. function calendar_top_controls($type, $data) {
  1420. global $PAGE, $OUTPUT;
  1421. // Get the calendar type we are using.
  1422. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1423. $content = '';
  1424. // Ensure course id passed if relevant.
  1425. $courseid = '';
  1426. if (!empty($data['id'])) {
  1427. $courseid = '&amp;course=' . $data['id'];
  1428. }
  1429. // If we are passing a month and year then we need to convert this to a timestamp to
  1430. // support multiple calendars. No where in core should these be passed, this logic
  1431. // here is for third party plugins that may use this function.
  1432. if (!empty($data['m']) && !empty($date['y'])) {
  1433. if (!isset($data['d'])) {
  1434. $data['d'] = 1;
  1435. }
  1436. if (!checkdate($data['m'], $data['d'], $data['y'])) {
  1437. $time = time();
  1438. } else {
  1439. $time = make_timestamp($data['y'], $data['m'], $data['d']);
  1440. }
  1441. } else if (!empty($data['time'])) {
  1442. $time = $data['time'];
  1443. } else {
  1444. $time = time();
  1445. }
  1446. // Get the date for the calendar type.
  1447. $date = $calendartype->timestamp_to_date_array($time);
  1448. $urlbase = $PAGE->url;
  1449. // We need to get the previous and next months in certain cases.
  1450. if ($type == 'frontpage' || $type == 'course' || $type == 'month') {
  1451. $prevmonth = calendar_sub_month($date['mon'], $date['year']);
  1452. $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1);
  1453. $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'],
  1454. $prevmonthtime['hour'], $prevmonthtime['minute']);
  1455. $nextmonth = calendar_add_month($date['mon'], $date['year']);
  1456. $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1);
  1457. $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'],
  1458. $nextmonthtime['hour'], $nextmonthtime['minute']);
  1459. }
  1460. switch ($type) {
  1461. case 'frontpage':
  1462. $prevlink = calendar_get_link_previous(get_string('monthprev', 'calendar'), $urlbase, false, false, false,
  1463. true, $prevmonthtime);
  1464. $nextlink = calendar_get_link_next(get_string('monthnext', 'calendar'), $urlbase, false, false, false, true,
  1465. $nextmonthtime);
  1466. $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
  1467. false, false, false, $time);
  1468. if (!empty($data['id'])) {
  1469. $calendarlink->param('course', $data['id']);
  1470. }
  1471. $right = $nextlink;
  1472. $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
  1473. $content .= $prevlink . '<span class="hide"> | </span>';
  1474. $content .= \html_writer::tag('span', \html_writer::link($calendarlink,
  1475. userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar'))
  1476. ), array('class' => 'current'));
  1477. $content .= '<span class="hide"> | </span>' . $right;
  1478. $content .= "<span class=\"clearer\"><!-- --></span>\n";
  1479. $content .= \html_writer::end_tag('div');
  1480. break;
  1481. case 'course':
  1482. $prevlink = calendar_get_link_previous(get_string('monthprev', 'calendar'), $urlbase, false, false, false,
  1483. true, $prevmonthtime);
  1484. $nextlink = calendar_get_link_next(get_string('monthnext', 'calendar'), $urlbase, false, false, false,
  1485. true, $nextmonthtime);
  1486. $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
  1487. false, false, false, $time);
  1488. if (!empty($data['id'])) {
  1489. $calendarlink->param('course', $data['id']);
  1490. }
  1491. $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
  1492. $content .= $prevlink . '<span class="hide"> | </span>';
  1493. $content .= \html_writer::tag('span', \html_writer::link($calendarlink,
  1494. userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar'))
  1495. ), array('class' => 'current'));
  1496. $content .= '<span class="hide"> | </span>' . $nextlink;
  1497. $content .= "<span class=\"clearer\"><!-- --></span>";
  1498. $content .= \html_writer::end_tag('div');
  1499. break;
  1500. case 'upcoming':
  1501. $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'upcoming')),
  1502. false, false, false, $time);
  1503. if (!empty($data['id'])) {
  1504. $calendarlink->param('course', $data['id']);
  1505. }
  1506. $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
  1507. $content .= \html_writer::tag('div', $calendarlink, array('class' => 'centered'));
  1508. break;
  1509. case 'display':
  1510. $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
  1511. false, false, false, $time);
  1512. if (!empty($data['id'])) {
  1513. $calendarlink->param('course', $data['id']);
  1514. }
  1515. $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
  1516. $content .= \html_writer::tag('h3', $calendarlink);
  1517. break;
  1518. case 'month':
  1519. $prevlink = calendar_get_link_previous(userdate($prevmonthtime, get_string('strftimemonthyear')),
  1520. 'view.php?view=month' . $courseid . '&amp;', false, false, false, false, $prevmonthtime);
  1521. $nextlink = calendar_get_link_next(userdate($nextmonthtime, get_string('strftimemonthyear')),
  1522. 'view.php?view=month' . $courseid . '&amp;', false, false, false, false, $nextmonthtime);
  1523. $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
  1524. $content .= $prevlink . '<span class="hide"> | </span>';
  1525. $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current');
  1526. $content .= '<span class="hide"> | </span>' . $nextlink;
  1527. $content .= '<span class="clearer"><!-- --></span>';
  1528. $content .= \html_writer::end_tag('div')."\n";
  1529. break;
  1530. case 'day':
  1531. $days = calendar_get_days();
  1532. $prevtimestamp = strtotime('-1 day', $time);
  1533. $nexttimestamp = strtotime('+1 day', $time);
  1534. $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp);
  1535. $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp);
  1536. $prevname = $days[$prevdate['wday']]['fullname'];
  1537. $nextname = $days[$nextdate['wday']]['fullname'];
  1538. $prevlink = calendar_get_link_previous($prevname, 'view.php?view=day' . $courseid . '&amp;', false, false,
  1539. false, false, $prevtimestamp);
  1540. $nextlink = calendar_get_link_next($nextname, 'view.php?view=day' . $courseid . '&amp;', false, false, false,
  1541. false, $nexttimestamp);
  1542. $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
  1543. $content .= $prevlink;
  1544. $content .= '<span class="hide"> | </span><span class="current">' .userdate($time,
  1545. get_string('strftimedaydate')) . '</span>';
  1546. $content .= '<span class="hide"> | </span>' . $nextlink;
  1547. $content .= "<span class=\"clearer\"><!-- --></span>";
  1548. $content .= \html_writer::end_tag('div') . "\n";
  1549. break;
  1550. }
  1551. return $content;
  1552. }
  1553. /**
  1554. * Return the representation day.
  1555. *
  1556. * @param int $tstamp Timestamp in GMT
  1557. * @param int|bool $now current Unix timestamp
  1558. * @param bool $usecommonwords
  1559. * @return string the formatted date/time
  1560. */
  1561. function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) {
  1562. static $shortformat;
  1563. if (empty($shortformat)) {
  1564. $shortformat = get_string('strftimedayshort');
  1565. }
  1566. if ($now === false) {
  1567. $now = time();
  1568. }
  1569. // To have it in one place, if a change is needed.
  1570. $formal = userdate($tstamp, $shortformat);
  1571. $datestamp = usergetdate($tstamp);
  1572. $datenow = usergetdate($now);
  1573. if ($usecommonwords == false) {
  1574. // We don't want words, just a date.
  1575. return $formal;
  1576. } else if ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) {
  1577. return get_string('today', 'calendar');
  1578. } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) ||
  1579. ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12
  1580. && $datenow['yday'] == 1)) {
  1581. return get_string('yesterday', 'calendar');
  1582. } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) ||
  1583. ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12
  1584. && $datestamp['yday'] == 1)) {
  1585. return get_string('tomorrow', 'calendar');
  1586. } else {
  1587. return $formal;
  1588. }
  1589. }
  1590. /**
  1591. * return the formatted representation time.
  1592. *
  1593. * @param int $time the timestamp in UTC, as obtained from the database
  1594. * @return string the formatted date/time
  1595. */
  1596. function calendar_time_representation($time) {
  1597. static $langtimeformat = null;
  1598. if ($langtimeformat === null) {
  1599. $langtimeformat = get_string('strftimetime');
  1600. }
  1601. $timeformat = get_user_preferences('calendar_timeformat');
  1602. if (empty($timeformat)) {
  1603. $timeformat = get_config(null, 'calendar_site_timeformat');
  1604. }
  1605. // Allow language customization of selected time format.
  1606. if ($timeformat === CALENDAR_TF_12) {
  1607. $timeformat = get_string('strftimetime12', 'langconfig');
  1608. } else if ($timeformat === CALENDAR_TF_24) {
  1609. $timeformat = get_string('strftimetime24', 'langconfig');
  1610. }
  1611. return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat);
  1612. }
  1613. /**
  1614. * Adds day, month, year arguments to a URL and returns a moodle_url object.
  1615. *
  1616. * @param string|moodle_url $linkbase
  1617. * @param int $d The number of the day.
  1618. * @param int $m The number of the month.
  1619. * @param int $y The number of the year.
  1620. * @param int $time the unixtime, used for multiple calendar support. The values $d,
  1621. * $m and $y are kept for backwards compatibility.
  1622. * @return moodle_url|null $linkbase
  1623. */
  1624. function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) {
  1625. if (empty($linkbase)) {
  1626. return null;
  1627. }
  1628. if (!($linkbase instanceof \moodle_url)) {
  1629. $linkbase = new \moodle_url($linkbase);
  1630. }
  1631. $linkbase->param('time', calendar_get_timestamp($d, $m, $y, $time));
  1632. return $linkbase;
  1633. }
  1634. /**
  1635. * Build and return a previous month HTML link, with an arrow.
  1636. *
  1637. * @param string $text The text label.
  1638. * @param string|moodle_url $linkbase The URL stub.
  1639. * @param int $d The number of the date.
  1640. * @param int $m The number of the month.
  1641. * @param int $y year The number of the year.
  1642. * @param bool $accesshide Default visible, or hide from all except screenreaders.
  1643. * @param int $time the unixtime, used for multiple calendar support. The values $d,
  1644. * $m and $y are kept for backwards compatibility.
  1645. * @return string HTML string.
  1646. */
  1647. function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
  1648. $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time);
  1649. if (empty($href)) {
  1650. return $text;
  1651. }
  1652. $attrs = [
  1653. 'data-time' => calendar_get_timestamp($d, $m, $y, $time),
  1654. 'data-drop-zone' => 'nav-link',
  1655. ];
  1656. return link_arrow_left($text, $href->out(false), $accesshide, 'previous', $attrs);
  1657. }
  1658. /**
  1659. * Build and return a next month HTML link, with an arrow.
  1660. *
  1661. * @param string $text The text label.
  1662. * @param string|moodle_url $linkbase The URL stub.
  1663. * @param int $d the number of the Day
  1664. * @param int $m The number of the month.
  1665. * @param int $y The number of the year.
  1666. * @param bool $accesshide Default visible, or hide from all except screenreaders.
  1667. * @param int $time the unixtime, used for multiple calendar support. The values $d,
  1668. * $m and $y are kept for backwards compatibility.
  1669. * @return string HTML string.
  1670. */
  1671. function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
  1672. $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time);
  1673. if (empty($href)) {
  1674. return $text;
  1675. }
  1676. $attrs = [
  1677. 'data-time' => calendar_get_timestamp($d, $m, $y, $time),
  1678. 'data-drop-zone' => 'nav-link',
  1679. ];
  1680. return link_arrow_right($text, $href->out(false), $accesshide, 'next', $attrs);
  1681. }
  1682. /**
  1683. * Return the number of days in month.
  1684. *
  1685. * @param int $month the number of the month.
  1686. * @param int $year the number of the year
  1687. * @return int
  1688. */
  1689. function calendar_days_in_month($month, $year) {
  1690. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1691. return $calendartype->get_num_days_in_month($year, $month);
  1692. }
  1693. /**
  1694. * Get the next following month.
  1695. *
  1696. * @param int $month the number of the month.
  1697. * @param int $year the number of the year.
  1698. * @return array the following month
  1699. */
  1700. function calendar_add_month($month, $year) {
  1701. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1702. return $calendartype->get_next_month($year, $month);
  1703. }
  1704. /**
  1705. * Get the previous month.
  1706. *
  1707. * @param int $month the number of the month.
  1708. * @param int $year the number of the year.
  1709. * @return array previous month
  1710. */
  1711. function calendar_sub_month($month, $year) {
  1712. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1713. return $calendartype->get_prev_month($year, $month);
  1714. }
  1715. /**
  1716. * Get per-day basis events
  1717. *
  1718. * @param array $events list of events
  1719. * @param int $month the number of the month
  1720. * @param int $year the number of the year
  1721. * @param array $eventsbyday event on specific day
  1722. * @param array $durationbyday duration of the event in days
  1723. * @param array $typesbyday event type (eg: site, course, user, or group)
  1724. * @param array $courses list of courses
  1725. * @return void
  1726. */
  1727. function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) {
  1728. $calendartype = \core_calendar\type_factory::get_calendar_instance();
  1729. $eventsbyday = array();
  1730. $typesbyday = array();
  1731. $durationbyday = array();
  1732. if ($events === false) {
  1733. return;
  1734. }
  1735. foreach ($events as $event) {
  1736. $startdate = $calendartype->timestamp_to_date_array($event->timestart);
  1737. if ($event->timeduration) {
  1738. $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1);
  1739. } else {
  1740. $enddate = $startdate;
  1741. }
  1742. // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair.
  1743. if (!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) &&
  1744. ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) {
  1745. continue;
  1746. }
  1747. $eventdaystart = intval($startdate['mday']);
  1748. if ($startdate['mon'] == $month && $startdate['year'] == $year) {
  1749. // Give the event to its day.
  1750. $eventsbyday[$eventdaystart][] = $event->id;
  1751. // Mark the day as having such an event.
  1752. if ($event->courseid == SITEID && $event->groupid == 0) {
  1753. $typesbyday[$eventdaystart]['startsite'] = true;
  1754. // Set event class for site event.
  1755. $events[$event->id]->class = 'calendar_event_site';
  1756. } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
  1757. $typesbyday[$eventdaystart]['startcourse'] = true;
  1758. // Set event class for course event.
  1759. $events[$event->id]->class = 'calendar_event_course';
  1760. } else if ($event->groupid) {
  1761. $typesbyday[$eventdaystart]['startgroup'] = true;
  1762. // Set event class for group event.
  1763. $events[$event->id]->class = 'calendar_event_group';
  1764. } else if ($event->userid) {
  1765. $typesbyday[$eventdaystart]['startuser'] = true;
  1766. // Set event class for user event.
  1767. $events[$event->id]->class = 'calendar_event_user';
  1768. }
  1769. }
  1770. if ($event->timeduration == 0) {
  1771. // Proceed with the next.
  1772. continue;
  1773. }
  1774. // The event starts on $month $year or before.
  1775. if ($startdate['mon'] == $month && $startdate['year'] == $year) {
  1776. $lowerbound = intval($startdate['mday']);
  1777. } else {
  1778. $lowerbound = 0;
  1779. }
  1780. // Also, it ends on $month $year or later.
  1781. if ($enddate['mon'] == $month && $enddate['year'] == $year) {
  1782. $upperbound = intval($enddate['mday']);
  1783. } else {
  1784. $upperbound = calendar_days_in_month($month, $year);
  1785. }
  1786. // Mark all days between $lowerbound and $upperbound (inclusive) as duration.
  1787. for ($i = $lowerbound + 1; $i <= $upperbound; ++$i) {
  1788. $durationbyday[$i][] = $event->id;
  1789. if ($event->courseid == SITEID && $event->groupid == 0) {
  1790. $typesbyday[$i]['durationsite'] = true;
  1791. } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
  1792. $typesbyday[$i]['durationcourse'] = true;
  1793. } else if ($event->groupid) {
  1794. $typesbyday[$i]['durationgroup'] = true;
  1795. } else if ($event->userid) {
  1796. $typesbyday[$i]['durationuser'] = true;
  1797. }
  1798. }
  1799. }
  1800. return;
  1801. }
  1802. /**
  1803. * Returns the courses to load events for.
  1804. *
  1805. * @param array $courseeventsfrom An array of courses to load calendar events for
  1806. * @param bool $ignorefilters specify the use of filters, false is set as default
  1807. * @param stdClass $user The user object. This defaults to the global $USER object.
  1808. * @return array An array of courses, groups, and user to load calendar events for based upon filters
  1809. */
  1810. function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, stdClass $user = null) {
  1811. global $CFG, $USER;
  1812. if (is_null($user)) {
  1813. $user = $USER;
  1814. }
  1815. $courses = array();
  1816. $userid = false;
  1817. $group = false;
  1818. // Get the capabilities that allow seeing group events from all groups.
  1819. $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries');
  1820. $isvaliduser = !empty($user->id);
  1821. if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE, $user)) {
  1822. $courses = array_keys($courseeventsfrom);
  1823. }
  1824. if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_SITE, $user)) {
  1825. $courses[] = SITEID;
  1826. }
  1827. $courses = array_unique($courses);
  1828. sort($courses);
  1829. if (!empty($courses) && in_array(SITEID, $courses)) {
  1830. // Sort courses for consistent colour highlighting.
  1831. // Effectively ignoring SITEID as setting as last course id.
  1832. $key = array_search(SITEID, $courses);
  1833. unset($courses[$key]);
  1834. $courses[] = SITEID;
  1835. }
  1836. if ($ignorefilters || ($isvaliduser && calendar_show_event_type(CALENDAR_EVENT_USER, $user))) {
  1837. $userid = $user->id;
  1838. }
  1839. if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) {
  1840. if (count($courseeventsfrom) == 1) {
  1841. $course = reset($courseeventsfrom);
  1842. if (has_any_capability($allgroupscaps, \context_course::instance($course->id))) {
  1843. $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id');
  1844. $group = array_keys($coursegroups);
  1845. }
  1846. }
  1847. if ($group === false) {
  1848. if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) {
  1849. $group = true;
  1850. } else if ($isvaliduser) {
  1851. $groupids = array();
  1852. foreach ($courseeventsfrom as $courseid => $course) {
  1853. // If the user is an editing teacher in there.
  1854. if (!empty($user->groupmember[$course->id])) {
  1855. // We've already cached the users groups for this course so we can just use that.
  1856. $groupids = array_merge($groupids, $user->groupmember[$course->id]);
  1857. } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
  1858. // If this course has groups, show events from all of those related to the current user.
  1859. $coursegroups = groups_get_user_groups($course->id, $user->id);
  1860. $groupids = array_merge($groupids, $coursegroups['0']);
  1861. }
  1862. }
  1863. if (!empty($groupids)) {
  1864. $group = $groupids;
  1865. }
  1866. }
  1867. }
  1868. }
  1869. if (empty($courses)) {
  1870. $courses = false;
  1871. }
  1872. return array($courses, $group, $userid);
  1873. }
  1874. /**
  1875. * Return the capability for viewing a calendar event.
  1876. *
  1877. * @param calendar_event $event event object
  1878. * @return boolean
  1879. */
  1880. function calendar_view_event_allowed(calendar_event $event) {
  1881. global $USER;
  1882. // Anyone can see site events.
  1883. if ($event->courseid && $event->courseid == SITEID) {
  1884. return true;
  1885. }
  1886. // If a user can manage events at the site level they can see any event.
  1887. $sitecontext = \context_system::instance();
  1888. // If user has manageentries at site level, return true.
  1889. if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
  1890. return true;
  1891. }
  1892. if (!empty($event->groupid)) {
  1893. // If it is a group event we need to be able to manage events in the course, or be in the group.
  1894. if (has_capability('moodle/calendar:manageentries', $event->context) ||
  1895. has_capability('moodle/calendar:managegroupentries', $event->context)) {
  1896. return true;
  1897. }
  1898. $mycourses = enrol_get_my_courses('id');
  1899. return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid);
  1900. } else if ($event->modulename) {
  1901. // If this is a module event we need to be able to see the module.
  1902. $coursemodules = [];
  1903. $courseid = 0;
  1904. // Override events do not have the courseid set.
  1905. if ($event->courseid) {
  1906. $courseid = $event->courseid;
  1907. $coursemodules = get_fast_modinfo($event->courseid)->instances;
  1908. } else {
  1909. $cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST);
  1910. $courseid = $cmraw->course;
  1911. $coursemodules = get_fast_modinfo($cmraw->course)->instances;
  1912. }
  1913. $hasmodule = isset($coursemodules[$event->modulename]);
  1914. $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
  1915. // If modinfo doesn't know about the module, return false to be safe.
  1916. if (!$hasmodule || !$hasinstance) {
  1917. return false;
  1918. }
  1919. // Must be able to see the course and the module - MDL-59304.
  1920. $cm = $coursemodules[$event->modulename][$event->instance];
  1921. if (!$cm->uservisible) {
  1922. return false;
  1923. }
  1924. $mycourses = enrol_get_my_courses('id');
  1925. return isset($mycourses[$courseid]);
  1926. } else if ($event->categoryid) {
  1927. // If this is a category we need to be able to see the category.
  1928. $cat = \core_course_category::get($event->categoryid, IGNORE_MISSING);
  1929. if (!$cat) {
  1930. return false;
  1931. }
  1932. return true;
  1933. } else if (!empty($event->courseid)) {
  1934. // If it is a course event we need to be able to manage events in the course, or be in the course.
  1935. if (has_capability('moodle/calendar:manageentries', $event->context)) {
  1936. return true;
  1937. }
  1938. return can_access_course(get_course($event->courseid));
  1939. } else if ($event->userid) {
  1940. if ($event->userid != $USER->id) {
  1941. // No-one can ever see another users events.
  1942. return false;
  1943. }
  1944. return true;
  1945. } else {
  1946. throw new moodle_exception('unknown event type');
  1947. }
  1948. return false;
  1949. }
  1950. /**
  1951. * Return the capability for editing calendar event.
  1952. *
  1953. * @param calendar_event $event event object
  1954. * @param bool $manualedit is the event being edited manually by the user
  1955. * @return bool capability to edit event
  1956. */
  1957. function calendar_edit_event_allowed($event, $manualedit = false) {
  1958. global $USER, $DB;
  1959. // Must be logged in.
  1960. if (!isloggedin()) {
  1961. return false;
  1962. }
  1963. // Can not be using guest account.
  1964. if (isguestuser()) {
  1965. return false;
  1966. }
  1967. if ($manualedit && !empty($event->modulename)) {
  1968. $hascallback = component_callback_exists(
  1969. 'mod_' . $event->modulename,
  1970. 'core_calendar_event_timestart_updated'
  1971. );
  1972. if (!$hascallback) {
  1973. // If the activity hasn't implemented the correct callback
  1974. // to handle changes to it's events then don't allow any
  1975. // manual changes to them.
  1976. return false;
  1977. }
  1978. $coursemodules = get_fast_modinfo($event->courseid)->instances;
  1979. $hasmodule = isset($coursemodules[$event->modulename]);
  1980. $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
  1981. // If modinfo doesn't know about the module, return false to be safe.
  1982. if (!$hasmodule || !$hasinstance) {
  1983. return false;
  1984. }
  1985. $coursemodule = $coursemodules[$event->modulename][$event->instance];
  1986. $context = context_module::instance($coursemodule->id);
  1987. // This is the capability that allows a user to modify the activity
  1988. // settings. Since the activity generated this event we need to check
  1989. // that the current user has the same capability before allowing them
  1990. // to update the event because the changes to the event will be
  1991. // reflected within the activity.
  1992. return has_capability('moodle/course:manageactivities', $context);
  1993. }
  1994. // You cannot edit URL based calendar subscription events presently.
  1995. if (!empty($event->subscriptionid)) {
  1996. if (!empty($event->subscription->url)) {
  1997. // This event can be updated externally, so it cannot be edited.
  1998. return false;
  1999. }
  2000. }
  2001. $sitecontext = \context_system::instance();
  2002. // If user has manageentries at site level, return true.
  2003. if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
  2004. return true;
  2005. }
  2006. // If groupid is set, it's definitely a group event.
  2007. if (!empty($event->groupid)) {
  2008. // Allow users to add/edit group events if -
  2009. // 1) They have manageentries for the course OR
  2010. // 2) They have managegroupentries AND are in the group.
  2011. $group = $DB->get_record('groups', array('id' => $event->groupid));
  2012. return $group && (
  2013. has_capability('moodle/calendar:manageentries', $event->context) ||
  2014. (has_capability('moodle/calendar:managegroupentries', $event->context)
  2015. && groups_is_member($event->groupid)));
  2016. } else if (!empty($event->courseid)) {
  2017. // If groupid is not set, but course is set, it's definitely a course event.
  2018. return has_capability('moodle/calendar:manageentries', $event->context);
  2019. } else if (!empty($event->categoryid)) {
  2020. // If groupid is not set, but category is set, it's definitely a category event.
  2021. return has_capability('moodle/calendar:manageentries', $event->context);
  2022. } else if (!empty($event->userid) && $event->userid == $USER->id) {
  2023. // If course is not set, but userid id set, it's a user event.
  2024. return (has_capability('moodle/calendar:manageownentries', $event->context));
  2025. } else if (!empty($event->userid)) {
  2026. return (has_capability('moodle/calendar:manageentries', $event->context));
  2027. }
  2028. return false;
  2029. }
  2030. /**
  2031. * Return the capability for deleting a calendar event.
  2032. *
  2033. * @param calendar_event $event The event object
  2034. * @return bool Whether the user has permission to delete the event or not.
  2035. */
  2036. function calendar_delete_event_allowed($event) {
  2037. // Only allow delete if you have capabilities and it is not an module event.
  2038. return (calendar_edit_event_allowed($event) && empty($event->modulename));
  2039. }
  2040. /**
  2041. * Returns the default courses to display on the calendar when there isn't a specific
  2042. * course to display.
  2043. *
  2044. * @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course).
  2045. * @param string $fields Comma separated list of course fields to return.
  2046. * @param bool $canmanage If true, this will return the list of courses the user can create events in, rather
  2047. * than the list of courses they see events from (an admin can always add events in a course
  2048. * calendar, even if they are not enrolled in the course).
  2049. * @param int $userid (optional) The user which this function returns the default courses for.
  2050. * By default the current user.
  2051. * @return array $courses Array of courses to display
  2052. */
  2053. function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage = false, int $userid = null) {
  2054. global $CFG, $USER;
  2055. if (!$userid) {
  2056. if (!isloggedin()) {
  2057. return array();
  2058. }
  2059. $userid = $USER->id;
  2060. }
  2061. if ((!empty($CFG->calendar_adminseesall) || $canmanage) &&
  2062. has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
  2063. // Add a c. prefix to every field as expected by get_courses function.
  2064. $fieldlist = explode(',', $fields);
  2065. $prefixedfields = array_map(function($value) {
  2066. return 'c.' . trim(strtolower($value));
  2067. }, $fieldlist);
  2068. if (!in_array('c.visible', $prefixedfields) && !in_array('c.*', $prefixedfields)) {
  2069. $prefixedfields[] = 'c.visible';
  2070. }
  2071. $courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields));
  2072. } else {
  2073. $courses = enrol_get_users_courses($userid, true, $fields);
  2074. }
  2075. if ($courseid && $courseid != SITEID) {
  2076. if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
  2077. // Allow a site admin to see calendars from courses he is not enrolled in.
  2078. // This will come from $COURSE.
  2079. $courses[$courseid] = get_course($courseid);
  2080. }
  2081. }
  2082. return $courses;
  2083. }
  2084. /**
  2085. * Get event format time.
  2086. *
  2087. * @param calendar_event $event event object
  2088. * @param int $now current time in gmt
  2089. * @param array $linkparams list of params for event link
  2090. * @param bool $usecommonwords the words as formatted date/time.
  2091. * @param int $showtime determine the show time GMT timestamp
  2092. * @return string $eventtime link/string for event time
  2093. */
  2094. function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) {
  2095. $starttime = $event->timestart;
  2096. $endtime = $event->timestart + $event->timeduration;
  2097. if (empty($linkparams) || !is_array($linkparams)) {
  2098. $linkparams = array();
  2099. }
  2100. $linkparams['view'] = 'day';
  2101. // OK, now to get a meaningful display.
  2102. // Check if there is a duration for this event.
  2103. if ($event->timeduration) {
  2104. // Get the midnight of the day the event will start.
  2105. $usermidnightstart = usergetmidnight($starttime);
  2106. // Get the midnight of the day the event will end.
  2107. $usermidnightend = usergetmidnight($endtime);
  2108. // Check if we will still be on the same day.
  2109. if ($usermidnightstart == $usermidnightend) {
  2110. // Check if we are running all day.
  2111. if ($event->timeduration == DAYSECS) {
  2112. $time = get_string('allday', 'calendar');
  2113. } else { // Specify the time we will be running this from.
  2114. $datestart = calendar_time_representation($starttime);
  2115. $dateend = calendar_time_representation($endtime);
  2116. $time = $datestart . ' <strong>&raquo;</strong> ' . $dateend;
  2117. }
  2118. // Set printable representation.
  2119. if (!$showtime) {
  2120. $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
  2121. $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
  2122. $eventtime = \html_writer::link($url, $day) . ', ' . $time;
  2123. } else {
  2124. $eventtime = $time;
  2125. }
  2126. } else { // It must spans two or more days.
  2127. $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', ';
  2128. if ($showtime == $usermidnightstart) {
  2129. $daystart = '';
  2130. }
  2131. $timestart = calendar_time_representation($event->timestart);
  2132. $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', ';
  2133. if ($showtime == $usermidnightend) {
  2134. $dayend = '';
  2135. }
  2136. $timeend = calendar_time_representation($event->timestart + $event->timeduration);
  2137. // Set printable representation.
  2138. if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) {
  2139. $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
  2140. $eventtime = $timestart . ' <strong>&raquo;</strong> ' . \html_writer::link($url, $dayend) . $timeend;
  2141. } else {
  2142. // The event is in the future, print start and end links.
  2143. $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime);
  2144. $eventtime = \html_writer::link($url, $daystart) . $timestart . ' <strong>&raquo;</strong> ';
  2145. $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
  2146. $eventtime .= \html_writer::link($url, $dayend) . $timeend;
  2147. }
  2148. }
  2149. } else { // There is no time duration.
  2150. $time = calendar_time_representation($event->timestart);
  2151. // Set printable representation.
  2152. if (!$showtime) {
  2153. $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
  2154. $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime);
  2155. $eventtime = \html_writer::link($url, $day) . ', ' . trim($time);
  2156. } else {
  2157. $eventtime = $time;
  2158. }
  2159. }
  2160. // Check if It has expired.
  2161. if ($event->timestart + $event->timeduration < $now) {
  2162. $eventtime = '<span class="dimmed_text">' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . '</span>';
  2163. }
  2164. return $eventtime;
  2165. }
  2166. /**
  2167. * Checks to see if the requested type of event should be shown for the given user.
  2168. *
  2169. * @param int $type The type to check the display for (default is to display all)
  2170. * @param stdClass|int|null $user The user to check for - by default the current user
  2171. * @return bool True if the tyep should be displayed false otherwise
  2172. */
  2173. function calendar_show_event_type($type, $user = null) {
  2174. $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
  2175. if (get_user_preferences('calendar_persistflt', 0, $user) === 0) {
  2176. global $SESSION;
  2177. if (!isset($SESSION->calendarshoweventtype)) {
  2178. $SESSION->calendarshoweventtype = $default;
  2179. }
  2180. return $SESSION->calendarshoweventtype & $type;
  2181. } else {
  2182. return get_user_preferences('calendar_savedflt', $default, $user) & $type;
  2183. }
  2184. }
  2185. /**
  2186. * Sets the display of the event type given $display.
  2187. *
  2188. * If $display = true the event type will be shown.
  2189. * If $display = false the event type will NOT be shown.
  2190. * If $display = null the current value will be toggled and saved.
  2191. *
  2192. * @param int $type object of CALENDAR_EVENT_XXX
  2193. * @param bool $display option to display event type
  2194. * @param stdClass|int $user moodle user object or id, null means current user
  2195. */
  2196. function calendar_set_event_type_display($type, $display = null, $user = null) {
  2197. $persist = get_user_preferences('calendar_persistflt', 0, $user);
  2198. $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
  2199. + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
  2200. if ($persist === 0) {
  2201. global $SESSION;
  2202. if (!isset($SESSION->calendarshoweventtype)) {
  2203. $SESSION->calendarshoweventtype = $default;
  2204. }
  2205. $preference = $SESSION->calendarshoweventtype;
  2206. } else {
  2207. $preference = get_user_preferences('calendar_savedflt', $default, $user);
  2208. }
  2209. $current = $preference & $type;
  2210. if ($display === null) {
  2211. $display = !$current;
  2212. }
  2213. if ($display && !$current) {
  2214. $preference += $type;
  2215. } else if (!$display && $current) {
  2216. $preference -= $type;
  2217. }
  2218. if ($persist === 0) {
  2219. $SESSION->calendarshoweventtype = $preference;
  2220. } else {
  2221. if ($preference == $default) {
  2222. unset_user_preference('calendar_savedflt', $user);
  2223. } else {
  2224. set_user_preference('calendar_savedflt', $preference, $user);
  2225. }
  2226. }
  2227. }
  2228. /**
  2229. * Get calendar's allowed types.
  2230. *
  2231. * @param stdClass $allowed list of allowed edit for event type
  2232. * @param stdClass|int $course object of a course or course id
  2233. * @param array $groups array of groups for the given course
  2234. * @param stdClass|int $category object of a category
  2235. */
  2236. function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) {
  2237. global $USER, $DB;
  2238. $allowed = new \stdClass();
  2239. $allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance());
  2240. $allowed->groups = false;
  2241. $allowed->courses = false;
  2242. $allowed->categories = false;
  2243. $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
  2244. $getgroupsfunc = function($course, $context, $user) use ($groups) {
  2245. if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
  2246. if (has_capability('moodle/site:accessallgroups', $context)) {
  2247. return is_null($groups) ? groups_get_all_groups($course->id) : $groups;
  2248. } else {
  2249. if (is_null($groups)) {
  2250. return groups_get_all_groups($course->id, $user->id);
  2251. } else {
  2252. return array_filter($groups, function($group) use ($user) {
  2253. return isset($group->members[$user->id]);
  2254. });
  2255. }
  2256. }
  2257. }
  2258. return false;
  2259. };
  2260. if (!empty($course)) {
  2261. if (!is_object($course)) {
  2262. $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST);
  2263. }
  2264. if ($course->id != SITEID) {
  2265. $coursecontext = \context_course::instance($course->id);
  2266. $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext);
  2267. if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
  2268. $allowed->courses = array($course->id => 1);
  2269. $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
  2270. } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
  2271. $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
  2272. }
  2273. }
  2274. }
  2275. if (!empty($category)) {
  2276. $catcontext = \context_coursecat::instance($category->id);
  2277. if (has_capability('moodle/category:manage', $catcontext)) {
  2278. $allowed->categories = [$category->id => 1];
  2279. }
  2280. }
  2281. }
  2282. /**
  2283. * See if user can add calendar entries at all used to print the "New Event" button.
  2284. *
  2285. * @param stdClass $course object of a course or course id
  2286. * @return bool has the capability to add at least one event type
  2287. */
  2288. function calendar_user_can_add_event($course) {
  2289. if (!isloggedin() || isguestuser()) {
  2290. return false;
  2291. }
  2292. calendar_get_allowed_types($allowed, $course);
  2293. return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->categories || $allowed->site);
  2294. }
  2295. /**
  2296. * Check wether the current user is permitted to add events.
  2297. *
  2298. * @param stdClass $event object of event
  2299. * @return bool has the capability to add event
  2300. */
  2301. function calendar_add_event_allowed($event) {
  2302. global $USER, $DB;
  2303. // Can not be using guest account.
  2304. if (!isloggedin() or isguestuser()) {
  2305. return false;
  2306. }
  2307. $sitecontext = \context_system::instance();
  2308. // If user has manageentries at site level, always return true.
  2309. if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
  2310. return true;
  2311. }
  2312. switch ($event->eventtype) {
  2313. case 'category':
  2314. return has_capability('moodle/category:manage', $event->context);
  2315. case 'course':
  2316. return has_capability('moodle/calendar:manageentries', $event->context);
  2317. case 'group':
  2318. // Allow users to add/edit group events if -
  2319. // 1) They have manageentries (= entries for whole course).
  2320. // 2) They have managegroupentries AND are in the group.
  2321. $group = $DB->get_record('groups', array('id' => $event->groupid));
  2322. return $group && (
  2323. has_capability('moodle/calendar:manageentries', $event->context) ||
  2324. (has_capability('moodle/calendar:managegroupentries', $event->context)
  2325. && groups_is_member($event->groupid)));
  2326. case 'user':
  2327. if ($event->userid == $USER->id) {
  2328. return (has_capability('moodle/calendar:manageownentries', $event->context));
  2329. }
  2330. // There is intentionally no 'break'.
  2331. case 'site':
  2332. return has_capability('moodle/calendar:manageentries', $event->context);
  2333. default:
  2334. return has_capability('moodle/calendar:manageentries', $event->context);
  2335. }
  2336. }
  2337. /**
  2338. * Returns option list for the poll interval setting.
  2339. *
  2340. * @return array An array of poll interval options. Interval => description.
  2341. */
  2342. function calendar_get_pollinterval_choices() {
  2343. return array(
  2344. '0' => new \lang_string('never', 'calendar'),
  2345. HOURSECS => new \lang_string('hourly', 'calendar'),
  2346. DAYSECS => new \lang_string('daily', 'calendar'),
  2347. WEEKSECS => new \lang_string('weekly', 'calendar'),
  2348. '2628000' => new \lang_string('monthly', 'calendar'),
  2349. YEARSECS => new \lang_string('annually', 'calendar')
  2350. );
  2351. }
  2352. /**
  2353. * Returns option list of available options for the calendar event type, given the current user and course.
  2354. *
  2355. * @param int $courseid The id of the course
  2356. * @return array An array containing the event types the user can create.
  2357. */
  2358. function calendar_get_eventtype_choices($courseid) {
  2359. $choices = array();
  2360. $allowed = new \stdClass;
  2361. calendar_get_allowed_types($allowed, $courseid);
  2362. if ($allowed->user) {
  2363. $choices['user'] = get_string('userevents', 'calendar');
  2364. }
  2365. if ($allowed->site) {
  2366. $choices['site'] = get_string('siteevents', 'calendar');
  2367. }
  2368. if (!empty($allowed->courses)) {
  2369. $choices['course'] = get_string('courseevents', 'calendar');
  2370. }
  2371. if (!empty($allowed->categories)) {
  2372. $choices['category'] = get_string('categoryevents', 'calendar');
  2373. }
  2374. if (!empty($allowed->groups) and is_array($allowed->groups)) {
  2375. $choices['group'] = get_string('group');
  2376. }
  2377. return array($choices, $allowed->groups);
  2378. }
  2379. /**
  2380. * Add an iCalendar subscription to the database.
  2381. *
  2382. * @param stdClass $sub The subscription object (e.g. from the form)
  2383. * @return int The insert ID, if any.
  2384. */
  2385. function calendar_add_subscription($sub) {
  2386. global $DB, $USER, $SITE;
  2387. // Undo the form definition work around to allow us to have two different
  2388. // course selectors present depending on which event type the user selects.
  2389. if (!empty($sub->groupcourseid)) {
  2390. $sub->courseid = $sub->groupcourseid;
  2391. unset($sub->groupcourseid);
  2392. }
  2393. // Default course id if none is set.
  2394. if (empty($sub->courseid)) {
  2395. if ($sub->eventtype === 'site') {
  2396. $sub->courseid = SITEID;
  2397. } else {
  2398. $sub->courseid = 0;
  2399. }
  2400. }
  2401. if ($sub->eventtype === 'site') {
  2402. $sub->courseid = $SITE->id;
  2403. } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
  2404. $sub->courseid = $sub->courseid;
  2405. } else if ($sub->eventtype === 'category') {
  2406. $sub->categoryid = $sub->categoryid;
  2407. } else {
  2408. // User events.
  2409. $sub->courseid = 0;
  2410. }
  2411. $sub->userid = $USER->id;
  2412. // File subscriptions never update.
  2413. if (empty($sub->url)) {
  2414. $sub->pollinterval = 0;
  2415. }
  2416. if (!empty($sub->name)) {
  2417. if (empty($sub->id)) {
  2418. $id = $DB->insert_record('event_subscriptions', $sub);
  2419. // We cannot cache the data here because $sub is not complete.
  2420. $sub->id = $id;
  2421. // Trigger event, calendar subscription added.
  2422. $eventparams = array('objectid' => $sub->id,
  2423. 'context' => calendar_get_calendar_context($sub),
  2424. 'other' => array(
  2425. 'eventtype' => $sub->eventtype,
  2426. )
  2427. );
  2428. switch ($sub->eventtype) {
  2429. case 'category':
  2430. $eventparams['other']['categoryid'] = $sub->categoryid;
  2431. break;
  2432. case 'course':
  2433. $eventparams['other']['courseid'] = $sub->courseid;
  2434. break;
  2435. case 'group':
  2436. $eventparams['other']['courseid'] = $sub->courseid;
  2437. $eventparams['other']['groupid'] = $sub->groupid;
  2438. break;
  2439. default:
  2440. $eventparams['other']['courseid'] = $sub->courseid;
  2441. }
  2442. $event = \core\event\calendar_subscription_created::create($eventparams);
  2443. $event->trigger();
  2444. return $id;
  2445. } else {
  2446. // Why are we doing an update here?
  2447. calendar_update_subscription($sub);
  2448. return $sub->id;
  2449. }
  2450. } else {
  2451. print_error('errorbadsubscription', 'importcalendar');
  2452. }
  2453. }
  2454. /**
  2455. * Add an iCalendar event to the Moodle calendar.
  2456. *
  2457. * @param stdClass $event The RFC-2445 iCalendar event
  2458. * @param int $unused Deprecated
  2459. * @param int $subscriptionid The iCalendar subscription ID
  2460. * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
  2461. * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
  2462. * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated, CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
  2463. */
  2464. function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $timezone='UTC') {
  2465. global $DB;
  2466. // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
  2467. if (empty($event->properties['SUMMARY'])) {
  2468. return 0;
  2469. }
  2470. $name = $event->properties['SUMMARY'][0]->value;
  2471. $name = str_replace('\n', '<br />', $name);
  2472. $name = str_replace('\\', '', $name);
  2473. $name = preg_replace('/\s+/u', ' ', $name);
  2474. $eventrecord = new \stdClass;
  2475. $eventrecord->name = clean_param($name, PARAM_NOTAGS);
  2476. if (empty($event->properties['DESCRIPTION'][0]->value)) {
  2477. $description = '';
  2478. } else {
  2479. $description = $event->properties['DESCRIPTION'][0]->value;
  2480. $description = clean_param($description, PARAM_NOTAGS);
  2481. $description = str_replace('\n', '<br />', $description);
  2482. $description = str_replace('\\', '', $description);
  2483. $description = preg_replace('/\s+/u', ' ', $description);
  2484. }
  2485. $eventrecord->description = $description;
  2486. // Probably a repeating event with RRULE etc. TODO: skip for now.
  2487. if (empty($event->properties['DTSTART'][0]->value)) {
  2488. return 0;
  2489. }
  2490. if (isset($event->properties['DTSTART'][0]->parameters['TZID'])) {
  2491. $tz = $event->properties['DTSTART'][0]->parameters['TZID'];
  2492. } else {
  2493. $tz = $timezone;
  2494. }
  2495. $tz = \core_date::normalise_timezone($tz);
  2496. $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
  2497. if (empty($event->properties['DTEND'])) {
  2498. $eventrecord->timeduration = 0; // No duration if no end time specified.
  2499. } else {
  2500. if (isset($event->properties['DTEND'][0]->parameters['TZID'])) {
  2501. $endtz = $event->properties['DTEND'][0]->parameters['TZID'];
  2502. } else {
  2503. $endtz = $timezone;
  2504. }
  2505. $endtz = \core_date::normalise_timezone($endtz);
  2506. $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart;
  2507. }
  2508. // Check to see if it should be treated as an all day event.
  2509. if ($eventrecord->timeduration == DAYSECS) {
  2510. // Check to see if the event started at Midnight on the imported calendar.
  2511. date_default_timezone_set($timezone);
  2512. if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
  2513. // This event should be an all day event. This is not correct, we don't do anything differently for all day events.
  2514. // See MDL-56227.
  2515. $eventrecord->timeduration = 0;
  2516. }
  2517. \core_date::set_default_server_timezone();
  2518. }
  2519. $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
  2520. trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value));
  2521. $eventrecord->uuid = $event->properties['UID'][0]->value;
  2522. $eventrecord->timemodified = time();
  2523. // Add the iCal subscription details if required.
  2524. // We should never do anything with an event without a subscription reference.
  2525. $sub = calendar_get_subscription($subscriptionid);
  2526. $eventrecord->subscriptionid = $subscriptionid;
  2527. $eventrecord->userid = $sub->userid;
  2528. $eventrecord->groupid = $sub->groupid;
  2529. $eventrecord->courseid = $sub->courseid;
  2530. $eventrecord->categoryid = $sub->categoryid;
  2531. $eventrecord->eventtype = $sub->eventtype;
  2532. if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
  2533. 'subscriptionid' => $eventrecord->subscriptionid))) {
  2534. // Compare iCal event data against the moodle event to see if something has changed.
  2535. $result = array_diff((array) $eventrecord, (array) $updaterecord);
  2536. // Unset timemodified field because it's always going to be different.
  2537. unset($result['timemodified']);
  2538. if (count($result)) {
  2539. $eventrecord->id = $updaterecord->id;
  2540. $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update.
  2541. } else {
  2542. return CALENDAR_IMPORT_EVENT_SKIPPED;
  2543. }
  2544. } else {
  2545. $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert.
  2546. }
  2547. if ($createdevent = \calendar_event::create($eventrecord, false)) {
  2548. if (!empty($event->properties['RRULE'])) {
  2549. // Repeating events.
  2550. date_default_timezone_set($tz); // Change time zone to parse all events.
  2551. $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value);
  2552. $rrule->parse_rrule();
  2553. $rrule->create_events($createdevent);
  2554. \core_date::set_default_server_timezone(); // Change time zone back to what it was.
  2555. }
  2556. return $return;
  2557. } else {
  2558. return 0;
  2559. }
  2560. }
  2561. /**
  2562. * Update a subscription from the form data in one of the rows in the existing subscriptions table.
  2563. *
  2564. * @param int $subscriptionid The ID of the subscription we are acting upon.
  2565. * @param int $pollinterval The poll interval to use.
  2566. * @param int $action The action to be performed. One of update or remove.
  2567. * @throws dml_exception if invalid subscriptionid is provided
  2568. * @return string A log of the import progress, including errors
  2569. */
  2570. function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
  2571. // Fetch the subscription from the database making sure it exists.
  2572. $sub = calendar_get_subscription($subscriptionid);
  2573. // Update or remove the subscription, based on action.
  2574. switch ($action) {
  2575. case CALENDAR_SUBSCRIPTION_UPDATE:
  2576. // Skip updating file subscriptions.
  2577. if (empty($sub->url)) {
  2578. break;
  2579. }
  2580. $sub->pollinterval = $pollinterval;
  2581. calendar_update_subscription($sub);
  2582. // Update the events.
  2583. return "<p>" . get_string('subscriptionupdated', 'calendar', $sub->name) . "</p>" .
  2584. calendar_update_subscription_events($subscriptionid);
  2585. case CALENDAR_SUBSCRIPTION_REMOVE:
  2586. calendar_delete_subscription($subscriptionid);
  2587. return get_string('subscriptionremoved', 'calendar', $sub->name);
  2588. break;
  2589. default:
  2590. break;
  2591. }
  2592. return '';
  2593. }
  2594. /**
  2595. * Delete subscription and all related events.
  2596. *
  2597. * @param int|stdClass $subscription subscription or it's id, which needs to be deleted.
  2598. */
  2599. function calendar_delete_subscription($subscription) {
  2600. global $DB;
  2601. if (!is_object($subscription)) {
  2602. $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST);
  2603. }
  2604. // Delete subscription and related events.
  2605. $DB->delete_records('event', array('subscriptionid' => $subscription->id));
  2606. $DB->delete_records('event_subscriptions', array('id' => $subscription->id));
  2607. \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id));
  2608. // Trigger event, calendar subscription deleted.
  2609. $eventparams = array('objectid' => $subscription->id,
  2610. 'context' => calendar_get_calendar_context($subscription),
  2611. 'other' => array(
  2612. 'eventtype' => $subscription->eventtype,
  2613. )
  2614. );
  2615. switch ($subscription->eventtype) {
  2616. case 'category':
  2617. $eventparams['other']['categoryid'] = $subscription->categoryid;
  2618. break;
  2619. case 'course':
  2620. $eventparams['other']['courseid'] = $subscription->courseid;
  2621. break;
  2622. case 'group':
  2623. $eventparams['other']['courseid'] = $subscription->courseid;
  2624. $eventparams['other']['groupid'] = $subscription->groupid;
  2625. break;
  2626. default:
  2627. $eventparams['other']['courseid'] = $subscription->courseid;
  2628. }
  2629. $event = \core\event\calendar_subscription_deleted::create($eventparams);
  2630. $event->trigger();
  2631. }
  2632. /**
  2633. * From a URL, fetch the calendar and return an iCalendar object.
  2634. *
  2635. * @param string $url The iCalendar URL
  2636. * @return iCalendar The iCalendar object
  2637. */
  2638. function calendar_get_icalendar($url) {
  2639. global $CFG;
  2640. require_once($CFG->libdir . '/filelib.php');
  2641. $curl = new \curl();
  2642. $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5));
  2643. $calendar = $curl->get($url);
  2644. // Http code validation should actually be the job of curl class.
  2645. if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) {
  2646. throw new \moodle_exception('errorinvalidicalurl', 'calendar');
  2647. }
  2648. $ical = new \iCalendar();
  2649. $ical->unserialize($calendar);
  2650. return $ical;
  2651. }
  2652. /**
  2653. * Import events from an iCalendar object into a course calendar.
  2654. *
  2655. * @param iCalendar $ical The iCalendar object.
  2656. * @param int $unused Deprecated
  2657. * @param int $subscriptionid The subscription ID.
  2658. * @return string A log of the import progress, including errors.
  2659. */
  2660. function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) {
  2661. global $DB;
  2662. $return = '';
  2663. $eventcount = 0;
  2664. $updatecount = 0;
  2665. $skippedcount = 0;
  2666. // Large calendars take a while...
  2667. if (!CLI_SCRIPT) {
  2668. \core_php_time_limit::raise(300);
  2669. }
  2670. // Grab the timezone from the iCalendar file to be used later.
  2671. if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) {
  2672. $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value;
  2673. } else {
  2674. $timezone = 'UTC';
  2675. }
  2676. $icaluuids = [];
  2677. foreach ($ical->components['VEVENT'] as $event) {
  2678. $icaluuids[] = $event->properties['UID'][0]->value;
  2679. $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
  2680. switch ($res) {
  2681. case CALENDAR_IMPORT_EVENT_UPDATED:
  2682. $updatecount++;
  2683. break;
  2684. case CALENDAR_IMPORT_EVENT_INSERTED:
  2685. $eventcount++;
  2686. break;
  2687. case CALENDAR_IMPORT_EVENT_SKIPPED:
  2688. $skippedcount++;
  2689. break;
  2690. case 0:
  2691. $return .= '<p>' . get_string('erroraddingevent', 'calendar') . ': ';
  2692. if (empty($event->properties['SUMMARY'])) {
  2693. $return .= '(' . get_string('notitle', 'calendar') . ')';
  2694. } else {
  2695. $return .= $event->properties['SUMMARY'][0]->value;
  2696. }
  2697. $return .= "</p>\n";
  2698. break;
  2699. }
  2700. }
  2701. $existing = $DB->get_field('event_subscriptions', 'lastupdated', ['id' => $subscriptionid]);
  2702. if (!empty($existing)) {
  2703. $eventsuuids = $DB->get_records_menu('event', ['subscriptionid' => $subscriptionid], '', 'id, uuid');
  2704. $icaleventscount = count($icaluuids);
  2705. $tobedeleted = [];
  2706. if (count($eventsuuids) > $icaleventscount) {
  2707. foreach ($eventsuuids as $eventid => $eventuuid) {
  2708. if (!in_array($eventuuid, $icaluuids)) {
  2709. $tobedeleted[] = $eventid;
  2710. }
  2711. }
  2712. if (!empty($tobedeleted)) {
  2713. $DB->delete_records_list('event', 'id', $tobedeleted);
  2714. $return .= "<p> " . get_string('eventsdeleted', 'calendar') . ": " . count($tobedeleted) . "</p> ";
  2715. }
  2716. }
  2717. }
  2718. $return .= "<p>" . get_string('eventsimported', 'calendar', $eventcount) . "</p> ";
  2719. $return .= "<p>" . get_string('eventsskipped', 'calendar', $skippedcount) . "</p> ";
  2720. $return .= "<p>" . get_string('eventsupdated', 'calendar', $updatecount) . "</p>";
  2721. return $return;
  2722. }
  2723. /**
  2724. * Fetch a calendar subscription and update the events in the calendar.
  2725. *
  2726. * @param int $subscriptionid The course ID for the calendar.
  2727. * @return string A log of the import progress, including errors.
  2728. */
  2729. function calendar_update_subscription_events($subscriptionid) {
  2730. $sub = calendar_get_subscription($subscriptionid);
  2731. // Don't update a file subscription.
  2732. if (empty($sub->url)) {
  2733. return 'File subscription not updated.';
  2734. }
  2735. $ical = calendar_get_icalendar($sub->url);
  2736. $return = calendar_import_icalendar_events($ical, null, $subscriptionid);
  2737. $sub->lastupdated = time();
  2738. calendar_update_subscription($sub);
  2739. return $return;
  2740. }
  2741. /**
  2742. * Update a calendar subscription. Also updates the associated cache.
  2743. *
  2744. * @param stdClass|array $subscription Subscription record.
  2745. * @throws coding_exception If something goes wrong
  2746. * @since Moodle 2.5
  2747. */
  2748. function calendar_update_subscription($subscription) {
  2749. global $DB;
  2750. if (is_array($subscription)) {
  2751. $subscription = (object)$subscription;
  2752. }
  2753. if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) {
  2754. throw new \coding_exception('Cannot update a subscription without a valid id');
  2755. }
  2756. $DB->update_record('event_subscriptions', $subscription);
  2757. // Update cache.
  2758. $cache = \cache::make('core', 'calendar_subscriptions');
  2759. $cache->set($subscription->id, $subscription);
  2760. // Trigger event, calendar subscription updated.
  2761. $eventparams = array('userid' => $subscription->userid,
  2762. 'objectid' => $subscription->id,
  2763. 'context' => calendar_get_calendar_context($subscription),
  2764. 'other' => array(
  2765. 'eventtype' => $subscription->eventtype,
  2766. )
  2767. );
  2768. switch ($subscription->eventtype) {
  2769. case 'category':
  2770. $eventparams['other']['categoryid'] = $subscription->categoryid;
  2771. break;
  2772. case 'course':
  2773. $eventparams['other']['courseid'] = $subscription->courseid;
  2774. break;
  2775. case 'group':
  2776. $eventparams['other']['courseid'] = $subscription->courseid;
  2777. $eventparams['other']['groupid'] = $subscription->groupid;
  2778. break;
  2779. default:
  2780. $eventparams['other']['courseid'] = $subscription->courseid;
  2781. }
  2782. $event = \core\event\calendar_subscription_updated::create($eventparams);
  2783. $event->trigger();
  2784. }
  2785. /**
  2786. * Checks to see if the user can edit a given subscription feed.
  2787. *
  2788. * @param mixed $subscriptionorid Subscription object or id
  2789. * @return bool true if current user can edit the subscription else false
  2790. */
  2791. function calendar_can_edit_subscription($subscriptionorid) {
  2792. if (is_array($subscriptionorid)) {
  2793. $subscription = (object)$subscriptionorid;
  2794. } else if (is_object($subscriptionorid)) {
  2795. $subscription = $subscriptionorid;
  2796. } else {
  2797. $subscription = calendar_get_subscription($subscriptionorid);
  2798. }
  2799. $allowed = new \stdClass;
  2800. $courseid = $subscription->courseid;
  2801. $categoryid = $subscription->categoryid;
  2802. $groupid = $subscription->groupid;
  2803. $category = null;
  2804. if (!empty($categoryid)) {
  2805. $category = \core_course_category::get($categoryid);
  2806. }
  2807. calendar_get_allowed_types($allowed, $courseid, null, $category);
  2808. switch ($subscription->eventtype) {
  2809. case 'user':
  2810. return $allowed->user;
  2811. case 'course':
  2812. if (isset($allowed->courses[$courseid])) {
  2813. return $allowed->courses[$courseid];
  2814. } else {
  2815. return false;
  2816. }
  2817. case 'category':
  2818. if (isset($allowed->categories[$categoryid])) {
  2819. return $allowed->categories[$categoryid];
  2820. } else {
  2821. return false;
  2822. }
  2823. case 'site':
  2824. return $allowed->site;
  2825. case 'group':
  2826. if (isset($allowed->groups[$groupid])) {
  2827. return $allowed->groups[$groupid];
  2828. } else {
  2829. return false;
  2830. }
  2831. default:
  2832. return false;
  2833. }
  2834. }
  2835. /**
  2836. * Helper function to determine the context of a calendar subscription.
  2837. * Subscriptions can be created in two contexts COURSE, or USER.
  2838. *
  2839. * @param stdClass $subscription
  2840. * @return context instance
  2841. */
  2842. function calendar_get_calendar_context($subscription) {
  2843. // Determine context based on calendar type.
  2844. if ($subscription->eventtype === 'site') {
  2845. $context = \context_course::instance(SITEID);
  2846. } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
  2847. $context = \context_course::instance($subscription->courseid);
  2848. } else {
  2849. $context = \context_user::instance($subscription->userid);
  2850. }
  2851. return $context;
  2852. }
  2853. /**
  2854. * Implements callback user_preferences, whitelists preferences that users are allowed to update directly
  2855. *
  2856. * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
  2857. *
  2858. * @return array
  2859. */
  2860. function core_calendar_user_preferences() {
  2861. $preferences = [];
  2862. $preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0',
  2863. 'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24)
  2864. );
  2865. $preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
  2866. 'choices' => array(0, 1, 2, 3, 4, 5, 6));
  2867. $preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20));
  2868. $preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365,
  2869. 'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1));
  2870. $preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
  2871. 'choices' => array(0, 1));
  2872. return $preferences;
  2873. }
  2874. /**
  2875. * Get legacy calendar events
  2876. *
  2877. * @param int $tstart Start time of time range for events
  2878. * @param int $tend End time of time range for events
  2879. * @param array|int|boolean $users array of users, user id or boolean for all/no user events
  2880. * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
  2881. * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
  2882. * @param boolean $withduration whether only events starting within time range selected
  2883. * or events in progress/already started selected as well
  2884. * @param boolean $ignorehidden whether to select only visible events or all events
  2885. * @param array $categories array of category ids and/or objects.
  2886. * @param int $limitnum Number of events to fetch or zero to fetch all.
  2887. *
  2888. * @return array $events of selected events or an empty array if there aren't any (or there was an error)
  2889. */
  2890. function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
  2891. $withduration = true, $ignorehidden = true, $categories = [], $limitnum = 0) {
  2892. // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
  2893. // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
  2894. // parameters, but with the new API method, only null and arrays are accepted.
  2895. list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
  2896. // If parameter is true, return null.
  2897. if ($param === true) {
  2898. return null;
  2899. }
  2900. // If parameter is false, return an empty array.
  2901. if ($param === false) {
  2902. return [];
  2903. }
  2904. // If the parameter is a scalar value, enclose it in an array.
  2905. if (!is_array($param)) {
  2906. return [$param];
  2907. }
  2908. // No normalisation required.
  2909. return $param;
  2910. }, [$users, $groups, $courses, $categories]);
  2911. // If a single user is provided, we can use that for capability checks.
  2912. // Otherwise current logged in user is used - See MDL-58768.
  2913. if (is_array($userparam) && count($userparam) == 1) {
  2914. \core_calendar\local\event\container::set_requesting_user($userparam[0]);
  2915. }
  2916. $mapper = \core_calendar\local\event\container::get_event_mapper();
  2917. $events = \core_calendar\local\api::get_events(
  2918. $tstart,
  2919. $tend,
  2920. null,
  2921. null,
  2922. null,
  2923. null,
  2924. $limitnum,
  2925. null,
  2926. $userparam,
  2927. $groupparam,
  2928. $courseparam,
  2929. $categoryparam,
  2930. $withduration,
  2931. $ignorehidden
  2932. );
  2933. return array_reduce($events, function($carry, $event) use ($mapper) {
  2934. return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
  2935. }, []);
  2936. }
  2937. /**
  2938. * Get the calendar view output.
  2939. *
  2940. * @param \calendar_information $calendar The calendar being represented
  2941. * @param string $view The type of calendar to have displayed
  2942. * @param bool $includenavigation Whether to include navigation
  2943. * @param bool $skipevents Whether to load the events or not
  2944. * @param int $lookahead Overwrites site and users's lookahead setting.
  2945. * @return array[array, string]
  2946. */
  2947. function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true, bool $skipevents = false,
  2948. ?int $lookahead = null) {
  2949. global $PAGE, $CFG;
  2950. $renderer = $PAGE->get_renderer('core_calendar');
  2951. $type = \core_calendar\type_factory::get_calendar_instance();
  2952. // Calculate the bounds of the month.
  2953. $calendardate = $type->timestamp_to_date_array($calendar->time);
  2954. $date = new \DateTime('now', core_date::get_user_timezone_object(99));
  2955. $eventlimit = 0;
  2956. if ($view === 'day') {
  2957. $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday']);
  2958. $date->setTimestamp($tstart);
  2959. $date->modify('+1 day');
  2960. } else if ($view === 'upcoming' || $view === 'upcoming_mini') {
  2961. // Number of days in the future that will be used to fetch events.
  2962. if (!$lookahead) {
  2963. if (isset($CFG->calendar_lookahead)) {
  2964. $defaultlookahead = intval($CFG->calendar_lookahead);
  2965. } else {
  2966. $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
  2967. }
  2968. $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead);
  2969. }
  2970. // Maximum number of events to be displayed on upcoming view.
  2971. $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
  2972. if (isset($CFG->calendar_maxevents)) {
  2973. $defaultmaxevents = intval($CFG->calendar_maxevents);
  2974. }
  2975. $eventlimit = get_user_preferences('calendar_maxevents', $defaultmaxevents);
  2976. $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday'],
  2977. $calendardate['hours']);
  2978. $date->setTimestamp($tstart);
  2979. $date->modify('+' . $lookahead . ' days');
  2980. } else {
  2981. $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], 1);
  2982. $monthdays = $type->get_num_days_in_month($calendardate['year'], $calendardate['mon']);
  2983. $date->setTimestamp($tstart);
  2984. $date->modify('+' . $monthdays . ' days');
  2985. if ($view === 'mini' || $view === 'minithree') {
  2986. $template = 'core_calendar/calendar_mini';
  2987. } else {
  2988. $template = 'core_calendar/calendar_month';
  2989. }
  2990. }
  2991. // We need to extract 1 second to ensure that we don't get into the next day.
  2992. $date->modify('-1 second');
  2993. $tend = $date->getTimestamp();
  2994. list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
  2995. // If parameter is true, return null.
  2996. if ($param === true) {
  2997. return null;
  2998. }
  2999. // If parameter is false, return an empty array.
  3000. if ($param === false) {
  3001. return [];
  3002. }
  3003. // If the parameter is a scalar value, enclose it in an array.
  3004. if (!is_array($param)) {
  3005. return [$param];
  3006. }
  3007. // No normalisation required.
  3008. return $param;
  3009. }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
  3010. if ($skipevents) {
  3011. $events = [];
  3012. } else {
  3013. $events = \core_calendar\local\api::get_events(
  3014. $tstart,
  3015. $tend,
  3016. null,
  3017. null,
  3018. null,
  3019. null,
  3020. $eventlimit,
  3021. null,
  3022. $userparam,
  3023. $groupparam,
  3024. $courseparam,
  3025. $categoryparam,
  3026. true,
  3027. true,
  3028. function ($event) {
  3029. if ($proxy = $event->get_course_module()) {
  3030. $cminfo = $proxy->get_proxied_instance();
  3031. return $cminfo->uservisible;
  3032. }
  3033. if ($proxy = $event->get_category()) {
  3034. $category = $proxy->get_proxied_instance();
  3035. return $category->is_uservisible();
  3036. }
  3037. return true;
  3038. }
  3039. );
  3040. }
  3041. $related = [
  3042. 'events' => $events,
  3043. 'cache' => new \core_calendar\external\events_related_objects_cache($events),
  3044. 'type' => $type,
  3045. ];
  3046. $data = [];
  3047. if ($view == "month" || $view == "mini" || $view == "minithree") {
  3048. $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
  3049. $month->set_includenavigation($includenavigation);
  3050. $month->set_initialeventsloaded(!$skipevents);
  3051. $month->set_showcoursefilter($view == "month");
  3052. $data = $month->export($renderer);
  3053. $data->viewingmonth = true;
  3054. } else if ($view == "day") {
  3055. $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
  3056. $data = $day->export($renderer);
  3057. $data->viewingday = true;
  3058. $template = 'core_calendar/calendar_day';
  3059. } else if ($view == "upcoming" || $view == "upcoming_mini") {
  3060. $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related);
  3061. $data = $upcoming->export($renderer);
  3062. if ($view == "upcoming") {
  3063. $template = 'core_calendar/calendar_upcoming';
  3064. $data->viewingupcoming = true;
  3065. } else if ($view == "upcoming_mini") {
  3066. $template = 'core_calendar/calendar_upcoming_mini';
  3067. }
  3068. }
  3069. return [$data, $template];
  3070. }
  3071. /**
  3072. * Request and render event form fragment.
  3073. *
  3074. * @param array $args The fragment arguments.
  3075. * @return string The rendered mform fragment.
  3076. */
  3077. function calendar_output_fragment_event_form($args) {
  3078. global $CFG, $OUTPUT, $USER;
  3079. require_once($CFG->libdir . '/grouplib.php');
  3080. $html = '';
  3081. $data = [];
  3082. $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
  3083. $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
  3084. $courseid = (isset($args['courseid']) && $args['courseid'] != SITEID) ? clean_param($args['courseid'], PARAM_INT) : null;
  3085. $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
  3086. $event = null;
  3087. $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
  3088. $context = \context_user::instance($USER->id);
  3089. $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
  3090. $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
  3091. $draftitemid = 0;
  3092. if ($hasformdata) {
  3093. parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
  3094. if (isset($data['description']['itemid'])) {
  3095. $draftitemid = $data['description']['itemid'];
  3096. }
  3097. }
  3098. if ($starttime) {
  3099. $formoptions['starttime'] = $starttime;
  3100. }
  3101. // Let's check first which event types user can add.
  3102. $eventtypes = calendar_get_allowed_event_types($courseid);
  3103. $formoptions['eventtypes'] = $eventtypes;
  3104. if (is_null($eventid)) {
  3105. if (!empty($courseid)) {
  3106. $groupcoursedata = groups_get_course_data($courseid);
  3107. $formoptions['groups'] = [];
  3108. foreach ($groupcoursedata->groups as $groupid => $groupdata) {
  3109. $formoptions['groups'][$groupid] = $groupdata->name;
  3110. }
  3111. }
  3112. $mform = new \core_calendar\local\event\forms\create(
  3113. null,
  3114. $formoptions,
  3115. 'post',
  3116. '',
  3117. null,
  3118. true,
  3119. $data
  3120. );
  3121. // If the user is on course context and is allowed to add course events set the event type default to course.
  3122. if (!empty($courseid) && !empty($eventtypes['course'])) {
  3123. $data['eventtype'] = 'course';
  3124. $data['courseid'] = $courseid;
  3125. $data['groupcourseid'] = $courseid;
  3126. } else if (!empty($categoryid) && !empty($eventtypes['category'])) {
  3127. $data['eventtype'] = 'category';
  3128. $data['categoryid'] = $categoryid;
  3129. } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) {
  3130. $data['groupcourseid'] = $courseid;
  3131. $data['groups'] = $groupcoursedata->groups;
  3132. }
  3133. $mform->set_data($data);
  3134. } else {
  3135. $event = calendar_event::load($eventid);
  3136. if (!calendar_edit_event_allowed($event)) {
  3137. print_error('nopermissiontoupdatecalendar');
  3138. }
  3139. $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
  3140. $eventdata = $mapper->from_legacy_event_to_data($event);
  3141. $data = array_merge((array) $eventdata, $data);
  3142. $event->count_repeats();
  3143. $formoptions['event'] = $event;
  3144. if (!empty($event->courseid)) {
  3145. $groupcoursedata = groups_get_course_data($event->courseid);
  3146. $formoptions['groups'] = [];
  3147. foreach ($groupcoursedata->groups as $groupid => $groupdata) {
  3148. $formoptions['groups'][$groupid] = $groupdata->name;
  3149. }
  3150. }
  3151. $data['description']['text'] = file_prepare_draft_area(
  3152. $draftitemid,
  3153. $event->context->id,
  3154. 'calendar',
  3155. 'event_description',
  3156. $event->id,
  3157. null,
  3158. $data['description']['text']
  3159. );
  3160. $data['description']['itemid'] = $draftitemid;
  3161. $mform = new \core_calendar\local\event\forms\update(
  3162. null,
  3163. $formoptions,
  3164. 'post',
  3165. '',
  3166. null,
  3167. true,
  3168. $data
  3169. );
  3170. $mform->set_data($data);
  3171. // Check to see if this event is part of a subscription or import.
  3172. // If so display a warning on edit.
  3173. if (isset($event->subscriptionid) && ($event->subscriptionid != null)) {
  3174. $renderable = new \core\output\notification(
  3175. get_string('eventsubscriptioneditwarning', 'calendar'),
  3176. \core\output\notification::NOTIFY_INFO
  3177. );
  3178. $html .= $OUTPUT->render($renderable);
  3179. }
  3180. }
  3181. if ($hasformdata) {
  3182. $mform->is_validated();
  3183. }
  3184. $html .= $mform->render();
  3185. return $html;
  3186. }
  3187. /**
  3188. * Calculate the timestamp from the supplied Gregorian Year, Month, and Day.
  3189. *
  3190. * @param int $d The day
  3191. * @param int $m The month
  3192. * @param int $y The year
  3193. * @param int $time The timestamp to use instead of a separate y/m/d.
  3194. * @return int The timestamp
  3195. */
  3196. function calendar_get_timestamp($d, $m, $y, $time = 0) {
  3197. // If a day, month and year were passed then convert it to a timestamp. If these were passed
  3198. // then we can assume the day, month and year are passed as Gregorian, as no where in core
  3199. // should we be passing these values rather than the time.
  3200. if (!empty($d) && !empty($m) && !empty($y)) {
  3201. if (checkdate($m, $d, $y)) {
  3202. $time = make_timestamp($y, $m, $d);
  3203. } else {
  3204. $time = time();
  3205. }
  3206. } else if (empty($time)) {
  3207. $time = time();
  3208. }
  3209. return $time;
  3210. }
  3211. /**
  3212. * Get the calendar footer options.
  3213. *
  3214. * @param calendar_information $calendar The calendar information object.
  3215. * @return array The data for template and template name.
  3216. */
  3217. function calendar_get_footer_options($calendar) {
  3218. global $CFG, $USER, $DB, $PAGE;
  3219. // Generate hash for iCal link.
  3220. $rawhash = $USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt;
  3221. $authtoken = sha1($rawhash);
  3222. $renderer = $PAGE->get_renderer('core_calendar');
  3223. $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken);
  3224. $data = $footer->export($renderer);
  3225. $template = 'core_calendar/footer_options';
  3226. return [$data, $template];
  3227. }
  3228. /**
  3229. * Get the list of potential calendar filter types as a type => name
  3230. * combination.
  3231. *
  3232. * @return array
  3233. */
  3234. function calendar_get_filter_types() {
  3235. $types = [
  3236. 'site',
  3237. 'category',
  3238. 'course',
  3239. 'group',
  3240. 'user',
  3241. ];
  3242. return array_map(function($type) {
  3243. return [
  3244. 'eventtype' => $type,
  3245. 'name' => get_string("eventtype{$type}", "calendar"),
  3246. 'icon' => true,
  3247. 'key' => 'i/' . $type . 'event',
  3248. 'component' => 'core'
  3249. ];
  3250. }, $types);
  3251. }
  3252. /**
  3253. * Check whether the specified event type is valid.
  3254. *
  3255. * @param string $type
  3256. * @return bool
  3257. */
  3258. function calendar_is_valid_eventtype($type) {
  3259. $validtypes = [
  3260. 'user',
  3261. 'group',
  3262. 'course',
  3263. 'category',
  3264. 'site',
  3265. ];
  3266. return in_array($type, $validtypes);
  3267. }
  3268. /**
  3269. * Get event types the user can create event based on categories, courses and groups
  3270. * the logged in user belongs to.
  3271. *
  3272. * @param int|null $courseid The course id.
  3273. * @return array The array of allowed types.
  3274. */
  3275. function calendar_get_allowed_event_types(int $courseid = null) {
  3276. global $DB, $CFG, $USER;
  3277. $types = [
  3278. 'user' => false,
  3279. 'site' => false,
  3280. 'course' => false,
  3281. 'group' => false,
  3282. 'category' => false
  3283. ];
  3284. if (!empty($courseid) && $courseid != SITEID) {
  3285. $context = \context_course::instance($courseid);
  3286. $types['user'] = has_capability('moodle/calendar:manageownentries', $context);
  3287. calendar_internal_update_course_and_group_permission($courseid, $context, $types);
  3288. }
  3289. if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) {
  3290. $types['site'] = true;
  3291. }
  3292. if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) {
  3293. $types['user'] = true;
  3294. }
  3295. if (core_course_category::has_manage_capability_on_any()) {
  3296. $types['category'] = true;
  3297. }
  3298. // We still don't know if the user can create group and course events, so iterate over the courses to find out
  3299. // if the user has capabilities in one of the courses.
  3300. if ($types['course'] == false || $types['group'] == false) {
  3301. if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) {
  3302. $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
  3303. FROM {course} c
  3304. JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id
  3305. WHERE c.id IN (
  3306. SELECT DISTINCT courseid FROM {groups}
  3307. )";
  3308. $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]);
  3309. foreach ($courseswithgroups as $course) {
  3310. context_helper::preload_from_record($course);
  3311. $context = context_course::instance($course->id);
  3312. if (has_capability('moodle/calendar:manageentries', $context)) {
  3313. if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) {
  3314. // The user can manage group entries or access any group.
  3315. $types['group'] = true;
  3316. $types['course'] = true;
  3317. break;
  3318. }
  3319. }
  3320. }
  3321. $courseswithgroups->close();
  3322. if (false === $types['course']) {
  3323. // Course is still not confirmed. There may have been no courses with a group in them.
  3324. $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
  3325. $sql = "SELECT
  3326. c.id, c.visible, {$ctxfields}
  3327. FROM {course} c
  3328. JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
  3329. $params = [
  3330. 'contextlevel' => CONTEXT_COURSE,
  3331. ];
  3332. $courses = $DB->get_recordset_sql($sql, $params);
  3333. foreach ($courses as $course) {
  3334. context_helper::preload_from_record($course);
  3335. $context = context_course::instance($course->id);
  3336. if (has_capability('moodle/calendar:manageentries', $context)) {
  3337. $types['course'] = true;
  3338. break;
  3339. }
  3340. }
  3341. $courses->close();
  3342. }
  3343. } else {
  3344. $courses = calendar_get_default_courses(null, 'id');
  3345. if (empty($courses)) {
  3346. return $types;
  3347. }
  3348. $courseids = array_map(function($c) {
  3349. return $c->id;
  3350. }, $courses);
  3351. // Check whether the user has access to create events within courses which have groups.
  3352. list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
  3353. $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
  3354. FROM {course} c
  3355. JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
  3356. WHERE c.id $insql
  3357. AND c.id IN (SELECT DISTINCT courseid FROM {groups})";
  3358. $params['contextlevel'] = CONTEXT_COURSE;
  3359. $courseswithgroups = $DB->get_recordset_sql($sql, $params);
  3360. foreach ($courseswithgroups as $coursewithgroup) {
  3361. context_helper::preload_from_record($coursewithgroup);
  3362. $context = context_course::instance($coursewithgroup->id);
  3363. calendar_internal_update_course_and_group_permission($coursewithgroup->id, $context, $types);
  3364. // Okay, course and group event types are allowed, no need to keep the loop iteration.
  3365. if ($types['course'] == true && $types['group'] == true) {
  3366. break;
  3367. }
  3368. }
  3369. $courseswithgroups->close();
  3370. if (false === $types['course']) {
  3371. list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
  3372. $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
  3373. FROM {course} c
  3374. JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
  3375. WHERE c.id $insql";
  3376. $params['contextlevel'] = CONTEXT_COURSE;
  3377. $contextrecords = $DB->get_recordset_sql($contextsql, $params);
  3378. foreach ($contextrecords as $course) {
  3379. context_helper::preload_from_record($course);
  3380. $coursecontext = context_course::instance($course->id);
  3381. if (has_capability('moodle/calendar:manageentries', $coursecontext)
  3382. && ($courseid == $course->id || empty($courseid))) {
  3383. $types['course'] = true;
  3384. break;
  3385. }
  3386. }
  3387. $contextrecords->close();
  3388. }
  3389. }
  3390. }
  3391. return $types;
  3392. }
  3393. /**
  3394. * Given a course id, and context, updates the permission types array to add the 'course' or 'group'
  3395. * permission if it is relevant for that course.
  3396. *
  3397. * For efficiency, if they already have 'course' or 'group' then it skips checks.
  3398. *
  3399. * Do not call this function directly, it is only for use by calendar_get_allowed_event_types().
  3400. *
  3401. * @param int $courseid Course id
  3402. * @param context $context Context for that course
  3403. * @param array $types Current permissions
  3404. */
  3405. function calendar_internal_update_course_and_group_permission(int $courseid, context $context, array &$types) {
  3406. if (!$types['course']) {
  3407. // If they have manageentries permission on the course, then they can update this course.
  3408. if (has_capability('moodle/calendar:manageentries', $context)) {
  3409. $types['course'] = true;
  3410. }
  3411. }
  3412. // To update group events they must have EITHER manageentries OR managegroupentries.
  3413. if (!$types['group'] && (has_capability('moodle/calendar:manageentries', $context) ||
  3414. has_capability('moodle/calendar:managegroupentries', $context))) {
  3415. // And they also need for a group to exist on the course.
  3416. $groups = groups_get_all_groups($courseid);
  3417. if (!empty($groups)) {
  3418. // And either accessallgroups, or belong to one of the groups.
  3419. if (has_capability('moodle/site:accessallgroups', $context)) {
  3420. $types['group'] = true;
  3421. } else {
  3422. foreach ($groups as $group) {
  3423. if (groups_is_member($group->id)) {
  3424. $types['group'] = true;
  3425. break;
  3426. }
  3427. }
  3428. }
  3429. }
  3430. }
  3431. }