PageRenderTime 40ms CodeModel.GetById 5ms RepoModel.GetById 1ms app.codeStats 1ms

/calendar/lib.php

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