929e82ae81afe40499345e8be9f0b3268b6aa1aa
[moodle.git] / calendar / lib.php
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/>.
17 /**
18  * Calendar extension
19  *
20  * @package    core_calendar
21  * @copyright  2004 Greek School Network (http://www.sch.gr), Jon Papaioannou,
22  *             Avgoustos Tsinakos
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 if (!defined('MOODLE_INTERNAL')) {
27     die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
28 }
30 /**
31  *  These are read by the administration component to provide default values
32  */
34 /**
35  * CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD - default value of upcoming event preference
36  */
37 define('CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD', 21);
39 /**
40  * CALENDAR_DEFAULT_UPCOMING_MAXEVENTS - default value to display the maximum number of upcoming event
41  */
42 define('CALENDAR_DEFAULT_UPCOMING_MAXEVENTS', 10);
44 /**
45  * CALENDAR_DEFAULT_STARTING_WEEKDAY - default value to display the starting weekday
46  */
47 define('CALENDAR_DEFAULT_STARTING_WEEKDAY', 1);
49 // This is a packed bitfield: day X is "weekend" if $field & (1 << X) is true
50 // Default value = 65 = 64 + 1 = 2^6 + 2^0 = Saturday & Sunday
52 /**
53  * CALENDAR_DEFAULT_WEEKEND - default value for weekend (Saturday & Sunday)
54  */
55 define('CALENDAR_DEFAULT_WEEKEND', 65);
57 /**
58  * CALENDAR_URL - path to calendar's folder
59  */
60 define('CALENDAR_URL', $CFG->wwwroot.'/calendar/');
62 /**
63  * CALENDAR_TF_24 - Calendar time in 24 hours format
64  */
65 define('CALENDAR_TF_24', '%H:%M');
67 /**
68  * CALENDAR_TF_12 - Calendar time in 12 hours format
69  */
70 define('CALENDAR_TF_12', '%I:%M %p');
72 /**
73  * CALENDAR_EVENT_GLOBAL - Global calendar event types
74  */
75 define('CALENDAR_EVENT_GLOBAL', 1);
77 /**
78  * CALENDAR_EVENT_COURSE - Course calendar event types
79  */
80 define('CALENDAR_EVENT_COURSE', 2);
82 /**
83  * CALENDAR_EVENT_GROUP - group calendar event types
84  */
85 define('CALENDAR_EVENT_GROUP', 4);
87 /**
88  * CALENDAR_EVENT_USER - user calendar event types
89  */
90 define('CALENDAR_EVENT_USER', 8);
93 /**
94  * CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
95  */
96 define('CALENDAR_IMPORT_FROM_FILE', 0);
98 /**
99  * CALENDAR_IMPORT_FROM_URL - import the calendar from a URL
100  */
101 define('CALENDAR_IMPORT_FROM_URL',  1);
103 /**
104  * CALENDAR_IMPORT_EVENT_UPDATED - imported event was updated
105  */
106 define('CALENDAR_IMPORT_EVENT_UPDATED',  1);
108 /**
109  * CALENDAR_IMPORT_EVENT_INSERTED - imported event was added by insert
110  */
111 define('CALENDAR_IMPORT_EVENT_INSERTED', 2);
113 /**
114  * CALENDAR_SUBSCRIPTION_UPDATE - Used to represent update action for subscriptions in various forms.
115  */
116 define('CALENDAR_SUBSCRIPTION_UPDATE', 1);
118 /**
119  * CALENDAR_SUBSCRIPTION_REMOVE - Used to represent remove action for subscriptions in various forms.
120  */
121 define('CALENDAR_SUBSCRIPTION_REMOVE', 2);
123 /**
124  * CALENDAR_EVENT_USER_OVERRIDE_PRIORITY - Constant for the user override priority.
125  */
126 define('CALENDAR_EVENT_USER_OVERRIDE_PRIORITY', 9999999);
128 /**
129  * Return the days of the week
130  *
131  * @return array array of days
132  */
133 function calendar_get_days() {
134     $calendartype = \core_calendar\type_factory::get_calendar_instance();
135     return $calendartype->get_weekdays();
138 /**
139  * Get the subscription from a given id
140  *
141  * @since Moodle 2.5
142  * @param int $id id of the subscription
143  * @return stdClass Subscription record from DB
144  * @throws moodle_exception for an invalid id
145  */
146 function calendar_get_subscription($id) {
147     global $DB;
149     $cache = cache::make('core', 'calendar_subscriptions');
150     $subscription = $cache->get($id);
151     if (empty($subscription)) {
152         $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST);
153         // cache the data.
154         $cache->set($id, $subscription);
155     }
156     return $subscription;
159 /**
160  * Gets the first day of the week
161  *
162  * Used to be define('CALENDAR_STARTING_WEEKDAY', blah);
163  *
164  * @return int
165  */
166 function calendar_get_starting_weekday() {
167     $calendartype = \core_calendar\type_factory::get_calendar_instance();
168     return $calendartype->get_starting_weekday();
171 /**
172  * Generates the HTML for a miniature calendar
173  *
174  * @param array $courses list of course to list events from
175  * @param array $groups list of group
176  * @param array $users user's info
177  * @param int|bool $calmonth calendar month in numeric, default is set to false
178  * @param int|bool $calyear calendar month in numeric, default is set to false
179  * @param string|bool $placement the place/page the calendar is set to appear - passed on the the controls function
180  * @param int|bool $courseid id of the course the calendar is displayed on - passed on the the controls function
181  * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
182  *     and $calyear to support multiple calendars
183  * @return string $content return html table for mini calendar
184  */
185 function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false,
186     $courseid = false, $time = 0) {
187     global $CFG, $OUTPUT, $PAGE;
189     // Get the calendar type we are using.
190     $calendartype = \core_calendar\type_factory::get_calendar_instance();
192     $display = new stdClass;
194     // Assume we are not displaying this month for now.
195     $display->thismonth = false;
197     $content = '';
199     // Do this check for backwards compatibility. The core should be passing a timestamp rather than month and year.
200     // If a month and year are passed they will be in Gregorian.
201     if (!empty($calmonth) && !empty($calyear)) {
202         // Ensure it is a valid date, else we will just set it to the current timestamp.
203         if (checkdate($calmonth, 1, $calyear)) {
204             $time = make_timestamp($calyear, $calmonth, 1);
205         } else {
206             $time = time();
207         }
208         $date = usergetdate($time);
209         if ($calmonth == $date['mon'] && $calyear == $date['year']) {
210             $display->thismonth = true;
211         }
212         // We can overwrite date now with the date used by the calendar type, if it is not Gregorian, otherwise
213         // there is no need as it is already in Gregorian.
214         if ($calendartype->get_name() != 'gregorian') {
215             $date = $calendartype->timestamp_to_date_array($time);
216         }
217     } else if (!empty($time)) {
218         // Get the specified date in the calendar type being used.
219         $date = $calendartype->timestamp_to_date_array($time);
220         $thisdate = $calendartype->timestamp_to_date_array(time());
221         if ($date['month'] == $thisdate['month'] && $date['year'] == $thisdate['year']) {
222             $display->thismonth = true;
223             // If we are the current month we want to set the date to the current date, not the start of the month.
224             $date = $thisdate;
225         }
226     } else {
227         // Get the current date in the calendar type being used.
228         $time = time();
229         $date = $calendartype->timestamp_to_date_array($time);
230         $display->thismonth = true;
231     }
233     list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display.
235     // Get Gregorian date for the start of the month.
236     $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1);
238     // Store the gregorian date values to be used later.
239     list($gy, $gm, $gd, $gh, $gmin) = array($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'],
240         $gregoriandate['hour'], $gregoriandate['minute']);
242     // Get the max number of days in this month for this calendar type.
243     $display->maxdays = calendar_days_in_month($m, $y);
244     // Get the starting week day for this month.
245     $startwday = dayofweek(1, $m, $y);
246     // Get the days in a week.
247     $daynames = calendar_get_days();
248     // Store the number of days in a week.
249     $numberofdaysinweek = $calendartype->get_num_weekdays();
251     // Set the min and max weekday.
252     $display->minwday = calendar_get_starting_weekday();
253     $display->maxwday = $display->minwday + ($numberofdaysinweek - 1);
255     // These are used for DB queries, so we want unixtime, so we need to use Gregorian dates.
256     $display->tstart = make_timestamp($gy, $gm, $gd, $gh, $gmin, 0);
257     $display->tend = $display->tstart + ($display->maxdays * DAYSECS) - 1;
259     // Align the starting weekday to fall in our display range
260     // This is simple, not foolproof.
261     if ($startwday < $display->minwday) {
262         $startwday += $numberofdaysinweek;
263     }
265     // Get the events matching our criteria. Don't forget to offset the timestamps for the user's TZ!
266     $events = calendar_get_events($display->tstart, $display->tend, $users, $groups, $courses);
268     // Set event course class for course events
269     if (!empty($events)) {
270         foreach ($events as $eventid => $event) {
271             if (!empty($event->modulename)) {
272                 $cm = get_coursemodule_from_instance($event->modulename, $event->instance);
273                 if (!\core_availability\info_module::is_user_visible($cm, 0, false)) {
274                     unset($events[$eventid]);
275                 }
276             }
277         }
278     }
280     // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after
281     // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month
282     // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra
283     // arguments to this function.
284     $hrefparams = array();
285     if(!empty($courses)) {
286         $courses = array_diff($courses, array(SITEID));
287         if(count($courses) == 1) {
288             $hrefparams['course'] = reset($courses);
289         }
290     }
292     // We want to have easy access by day, since the display is on a per-day basis.
293     calendar_events_by_day($events, $m, $y, $eventsbyday, $durationbyday, $typesbyday, $courses);
295     // Accessibility: added summary and <abbr> elements.
296     $summary = get_string('calendarheading', 'calendar', userdate($display->tstart, get_string('strftimemonthyear')));
297     // Begin table.
298     $content .= '<table class="minicalendar calendartable" summary="' . $summary . '">';
299     if (($placement !== false) && ($courseid !== false)) {
300         $content .= '<caption>'. calendar_top_controls($placement, array('id' => $courseid, 'time' => $time)) .'</caption>';
301     }
302     $content .= '<tr class="weekdays">'; // Header row: day names
304     // Print out the names of the weekdays.
305     for ($i = $display->minwday; $i <= $display->maxwday; ++$i) {
306         $pos = $i % $numberofdaysinweek;
307         $content .= '<th scope="col"><abbr title="'. $daynames[$pos]['fullname'] .'">'.
308             $daynames[$pos]['shortname'] ."</abbr></th>\n";
309     }
311     $content .= '</tr><tr>'; // End of day names; prepare for day numbers
313     // For the table display. $week is the row; $dayweek is the column.
314     $dayweek = $startwday;
316     // Paddding (the first week may have blank days in the beginning)
317     for($i = $display->minwday; $i < $startwday; ++$i) {
318         $content .= '<td class="dayblank">&nbsp;</td>'."\n";
319     }
321     $weekend = CALENDAR_DEFAULT_WEEKEND;
322     if (isset($CFG->calendar_weekend)) {
323         $weekend = intval($CFG->calendar_weekend);
324     }
326     // Now display all the calendar
327     $daytime = strtotime('-1 day', $display->tstart);
328     for($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) {
329         $cellattributes = array();
330         $daytime = strtotime('+1 day', $daytime);
331         if($dayweek > $display->maxwday) {
332             // We need to change week (table row)
333             $content .= '</tr><tr>';
334             $dayweek = $display->minwday;
335         }
337         // Reset vars.
338         if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) {
339             // Weekend. This is true no matter what the exact range is.
340             $class = 'weekend day';
341         } else {
342             // Normal working day.
343             $class = 'day';
344         }
346         $eventids = array();
347         if (!empty($eventsbyday[$day])) {
348             $eventids = $eventsbyday[$day];
349         }
351         if (!empty($durationbyday[$day])) {
352             $eventids = array_unique(array_merge($eventids, $durationbyday[$day]));
353         }
355         $finishclass = false;
357         if (!empty($eventids)) {
358             // There is at least one event on this day.
360             $class .= ' hasevent';
361             $hrefparams['view'] = 'day';
362             $dayhref = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $hrefparams), 0, 0, 0, $daytime);
364             $popupcontent = '';
365             foreach ($eventids as $eventid) {
366                 if (!isset($events[$eventid])) {
367                     continue;
368                 }
369                 $event = new calendar_event($events[$eventid]);
370                 $popupalt  = '';
371                 $component = 'moodle';
372                 if (!empty($event->modulename)) {
373                     $popupicon = 'icon';
374                     $popupalt  = $event->modulename;
375                     $component = $event->modulename;
376                 } else if ($event->courseid == SITEID) { // Site event.
377                     $popupicon = 'i/siteevent';
378                 } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event.
379                     $popupicon = 'i/courseevent';
380                 } else if ($event->groupid) { // Group event.
381                     $popupicon = 'i/groupevent';
382                 } else { // Must be a user event.
383                     $popupicon = 'i/userevent';
384                 }
386                 if ($event->timeduration) {
387                     $startdate = $calendartype->timestamp_to_date_array($event->timestart);
388                     $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1);
389                     if ($enddate['mon'] == $m && $enddate['year'] == $y && $enddate['mday'] == $day) {
390                         $finishclass = true;
391                     }
392                 }
394                 $dayhref->set_anchor('event_'.$event->id);
396                 $popupcontent .= html_writer::start_tag('div');
397                 $popupcontent .= $OUTPUT->pix_icon($popupicon, $popupalt, $component);
398                 // Show ical source if needed.
399                 if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
400                     $a = new stdClass();
401                     $a->name = format_string($event->name, true);
402                     $a->source = $event->subscription->name;
403                     $name = get_string('namewithsource', 'calendar', $a);
404                 } else {
405                     if ($finishclass) {
406                         $samedate = $startdate['mon'] == $enddate['mon'] &&
407                                     $startdate['year'] == $enddate['year'] &&
408                                     $startdate['mday'] == $enddate['mday'];
410                         if ($samedate) {
411                             $name = format_string($event->name, true);
412                         } else {
413                             $name = format_string($event->name, true) . ' (' . get_string('eventendtime', 'calendar') . ')';
414                         }
415                     } else {
416                         $name = format_string($event->name, true);
417                     }
418                 }
419                 $popupcontent .= html_writer::link($dayhref, $name);
420                 $popupcontent .= html_writer::end_tag('div');
421             }
423             if ($display->thismonth && $day == $d) {
424                 $popupdata = calendar_get_popup(true, $daytime, $popupcontent);
425             } else {
426                 $popupdata = calendar_get_popup(false, $daytime, $popupcontent);
427             }
428             $cellattributes = array_merge($cellattributes, $popupdata);
430             // Class and cell content
431             if(isset($typesbyday[$day]['startglobal'])) {
432                 $class .= ' calendar_event_global';
433             } else if(isset($typesbyday[$day]['startcourse'])) {
434                 $class .= ' calendar_event_course';
435             } else if(isset($typesbyday[$day]['startgroup'])) {
436                 $class .= ' calendar_event_group';
437             } else if(isset($typesbyday[$day]['startuser'])) {
438                 $class .= ' calendar_event_user';
439             }
440             if ($finishclass) {
441                 $class .= ' duration_finish';
442             }
443             $cell = html_writer::link($dayhref, $day);
444         } else {
445             $cell = $day;
446         }
448         $durationclass = false;
449         if (isset($typesbyday[$day]['durationglobal'])) {
450             $durationclass = ' duration_global';
451         } else if(isset($typesbyday[$day]['durationcourse'])) {
452             $durationclass = ' duration_course';
453         } else if(isset($typesbyday[$day]['durationgroup'])) {
454             $durationclass = ' duration_group';
455         } else if(isset($typesbyday[$day]['durationuser'])) {
456             $durationclass = ' duration_user';
457         }
458         if ($durationclass) {
459             $class .= ' duration '.$durationclass;
460         }
462         // If event has a class set then add it to the table day <td> tag
463         // Note: only one colour for minicalendar
464         if(isset($eventsbyday[$day])) {
465             foreach($eventsbyday[$day] as $eventid) {
466                 if (!isset($events[$eventid])) {
467                     continue;
468                 }
469                 $event = $events[$eventid];
470                 if (!empty($event->class)) {
471                     $class .= ' '.$event->class;
472                 }
473                 break;
474             }
475         }
477         if ($display->thismonth && $day == $d) {
478             // The current cell is for today - add appropriate classes and additional information for styling.
479             $class .= ' today';
480             $today = get_string('today', 'calendar').' '.userdate(time(), get_string('strftimedayshort'));
482             if (!isset($eventsbyday[$day]) && !isset($durationbyday[$day])) {
483                 $class .= ' eventnone';
484                 $popupdata = calendar_get_popup(true, false);
485                 $cellattributes = array_merge($cellattributes, $popupdata);
486                 $cell = html_writer::link('#', $day);
487             }
488             $cell = get_accesshide($today . ' ') . $cell;
489         }
491         // Just display it
492         $cellattributes['class'] = $class;
493         $content .= html_writer::tag('td', $cell, $cellattributes);
494     }
496     // Paddding (the last week may have blank days at the end)
497     for($i = $dayweek; $i <= $display->maxwday; ++$i) {
498         $content .= '<td class="dayblank">&nbsp;</td>';
499     }
500     $content .= '</tr>'; // Last row ends
502     $content .= '</table>'; // Tabular display of days ends
504     static $jsincluded = false;
505     if (!$jsincluded) {
506         $PAGE->requires->yui_module('moodle-calendar-info', 'Y.M.core_calendar.info.init');
507         $jsincluded = true;
508     }
509     return $content;
512 /**
513  * Gets the calendar popup
514  *
515  * It called at multiple points in from calendar_get_mini.
516  * Copied and modified from calendar_get_mini.
517  *
518  * @param bool $is_today false except when called on the current day.
519  * @param mixed $event_timestart $events[$eventid]->timestart, OR false if there are no events.
520  * @param string $popupcontent content for the popup window/layout.
521  * @return string eventid for the calendar_tooltip popup window/layout.
522  */
523 function calendar_get_popup($today = false, $timestart, $popupcontent = '') {
524     global $PAGE;
526     $popupcaption = '';
527     if ($today) {
528         $popupcaption = get_string('today', 'calendar') . ' ';
529     }
531     if (false === $timestart) {
532         $popupcaption .= userdate(time(), get_string('strftimedayshort'));
533         $popupcontent = get_string('eventnone', 'calendar');
535     } else {
536         $popupcaption .= get_string('eventsfor', 'calendar', userdate($timestart, get_string('strftimedayshort')));
537     }
539     return array(
540         'data-core_calendar-title' => $popupcaption,
541         'data-core_calendar-popupcontent' => $popupcontent,
542     );
545 /**
546  * Gets the calendar upcoming event
547  *
548  * @param array $courses array of courses
549  * @param array|int|bool $groups array of groups, group id or boolean for all/no group events
550  * @param array|int|bool $users array of users, user id or boolean for all/no user events
551  * @param int $daysinfuture number of days in the future we 'll look
552  * @param int $maxevents maximum number of events
553  * @param int $fromtime start time
554  * @return array $output array of upcoming events
555  */
556 function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxevents, $fromtime=0) {
557     global $CFG, $COURSE, $DB;
559     $display = new stdClass;
560     $display->range = $daysinfuture; // How many days in the future we 'll look
561     $display->maxevents = $maxevents;
563     $output = array();
565     // Prepare "course caching", since it may save us a lot of queries
566     $coursecache = array();
568     $processed = 0;
569     $now = time(); // We 'll need this later
570     $usermidnighttoday = usergetmidnight($now);
572     if ($fromtime) {
573         $display->tstart = $fromtime;
574     } else {
575         $display->tstart = $usermidnighttoday;
576     }
578     // This works correctly with respect to the user's DST, but it is accurate
579     // only because $fromtime is always the exact midnight of some day!
580     $display->tend = usergetmidnight($display->tstart + DAYSECS * $display->range + 3 * HOURSECS) - 1;
582     // Get the events matching our criteria
583     $events = calendar_get_events($display->tstart, $display->tend, $users, $groups, $courses);
585     // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after
586     // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month
587     // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra
588     // arguments to this function.
590     $hrefparams = array();
591     if(!empty($courses)) {
592         $courses = array_diff($courses, array(SITEID));
593         if(count($courses) == 1) {
594             $hrefparams['course'] = reset($courses);
595         }
596     }
598     if ($events !== false) {
600         $modinfo = get_fast_modinfo($COURSE);
602         foreach($events as $event) {
605             if (!empty($event->modulename)) {
606                 if ($event->courseid == $COURSE->id) {
607                     if (isset($modinfo->instances[$event->modulename][$event->instance])) {
608                         $cm = $modinfo->instances[$event->modulename][$event->instance];
609                         if (!$cm->uservisible) {
610                             continue;
611                         }
612                     }
613                 } else {
614                     if (!$cm = get_coursemodule_from_instance($event->modulename, $event->instance)) {
615                         continue;
616                     }
617                     if (!\core_availability\info_module::is_user_visible($cm, 0, false)) {
618                         continue;
619                     }
620                 }
621             }
623             if ($processed >= $display->maxevents) {
624                 break;
625             }
627             $event->time = calendar_format_event_time($event, $now, $hrefparams);
628             $output[] = $event;
629             ++$processed;
630         }
631     }
632     return $output;
636 /**
637  * Get a HTML link to a course.
638  *
639  * @param int $courseid the course id
640  * @return string a link to the course (as HTML); empty if the course id is invalid
641  */
642 function calendar_get_courselink($courseid) {
644     if (!$courseid) {
645         return '';
646     }
648     calendar_get_course_cached($coursecache, $courseid);
649     $context = context_course::instance($courseid);
650     $fullname = format_string($coursecache[$courseid]->fullname, true, array('context' => $context));
651     $url = new moodle_url('/course/view.php', array('id' => $courseid));
652     $link = html_writer::link($url, $fullname);
654     return $link;
658 /**
659  * Add calendar event metadata
660  *
661  * @param stdClass $event event info
662  * @return stdClass $event metadata
663  */
664 function calendar_add_event_metadata($event) {
665     global $CFG, $OUTPUT;
667     //Support multilang in event->name
668     $event->name = format_string($event->name,true);
670     if(!empty($event->modulename)) {                                // Activity event
671         // The module name is set. I will assume that it has to be displayed, and
672         // also that it is an automatically-generated event. And of course that the
673         // fields for get_coursemodule_from_instance are set correctly.
674         $module = calendar_get_module_cached($coursecache, $event->modulename, $event->instance);
676         if ($module === false) {
677             return;
678         }
680         $modulename = get_string('modulename', $event->modulename);
681         if (get_string_manager()->string_exists($event->eventtype, $event->modulename)) {
682             // will be used as alt text if the event icon
683             $eventtype = get_string($event->eventtype, $event->modulename);
684         } else {
685             $eventtype = '';
686         }
687         $icon = $OUTPUT->pix_url('icon', $event->modulename) . '';
689         $event->icon = '<img src="'.$icon.'" alt="'.$eventtype.'" title="'.$modulename.'" class="icon" />';
690         $event->referer = '<a href="'.$CFG->wwwroot.'/mod/'.$event->modulename.'/view.php?id='.$module->id.'">'.$event->name.'</a>';
691         $event->courselink = calendar_get_courselink($module->course);
692         $event->cmid = $module->id;
694     } else if($event->courseid == SITEID) {                              // Site event
695         $event->icon = '<img src="'.$OUTPUT->pix_url('i/siteevent') . '" alt="'.get_string('globalevent', 'calendar').'" class="icon" />';
696         $event->cssclass = 'calendar_event_global';
697     } else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {          // Course event
698         $event->icon = '<img src="'.$OUTPUT->pix_url('i/courseevent') . '" alt="'.get_string('courseevent', 'calendar').'" class="icon" />';
699         $event->courselink = calendar_get_courselink($event->courseid);
700         $event->cssclass = 'calendar_event_course';
701     } else if ($event->groupid) {                                    // Group event
702         if ($group = calendar_get_group_cached($event->groupid)) {
703             $groupname = format_string($group->name, true, context_course::instance($group->courseid));
704         } else {
705             $groupname = '';
706         }
707         $event->icon = html_writer::empty_tag('image', array('src' => $OUTPUT->pix_url('i/groupevent'),
708             'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon'));
709         $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname;
710         $event->cssclass = 'calendar_event_group';
711     } else if($event->userid) {                                      // User event
712         $event->icon = '<img src="'.$OUTPUT->pix_url('i/userevent') . '" alt="'.get_string('userevent', 'calendar').'" class="icon" />';
713         $event->cssclass = 'calendar_event_user';
714     }
715     return $event;
718 /**
719  * Get calendar events
720  *
721  * @param int $tstart Start time of time range for events
722  * @param int $tend End time of time range for events
723  * @param array|int|boolean $users array of users, user id or boolean for all/no user events
724  * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
725  * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
726  * @param boolean $withduration whether only events starting within time range selected
727  *                              or events in progress/already started selected as well
728  * @param boolean $ignorehidden whether to select only visible events or all events
729  * @return array $events of selected events or an empty array if there aren't any (or there was an error)
730  */
731 function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) {
732     global $DB;
734     $params = array();
735     // Quick test.
736     if (empty($users) && empty($groups) && empty($courses)) {
737         return array();
738     }
740     // Array of filter conditions. To be concatenated by the OR operator.
741     $filters = [];
743     // User filter.
744     if ((is_array($users) && !empty($users)) or is_numeric($users)) {
745         // Events from a number of users
746         list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
747         $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
748         $params = array_merge($params, $inparamsusers);
749     } else if ($users === true) {
750         // Events from ALL users
751         $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)";
752     }
753     // Boolean false (no users at all): We don't need to do anything.
755     // Group filter.
756     if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) {
757         // Events from a number of groups
758         list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
759         $filters[] = "e.groupid $insqlgroups";
760         $params = array_merge($params, $inparamsgroups);
761     } else if ($groups === true) {
762         // Events from ALL groups
763         $filters[] = "e.groupid != 0";
764     }
765     // Boolean false (no groups at all): We don't need to do anything.
767     // Course filter.
768     if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) {
769         list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
770         $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)";
771         $params = array_merge($params, $inparamscourses);
772     } else if ($courses === true) {
773         // Events from ALL courses
774         $filters[] = "(e.groupid = 0 AND e.courseid != 0)";
775     }
777     // Security check: if, by now, we have NOTHING in $whereclause, then it means
778     // that NO event-selecting clauses were defined. Thus, we won't be returning ANY
779     // events no matter what. Allowing the code to proceed might return a completely
780     // valid query with only time constraints, thus selecting ALL events in that time frame!
781     if (empty($filters)) {
782         return array();
783     }
785     // Build our clause for the filters.
786     $filterclause = implode(' OR ', $filters);
788     // Array of where conditions for our query. To be concatenated by the AND operator.
789     $whereconditions = ["($filterclause)"];
791     // Time clause.
792     if ($withduration) {
793         $timeclause = "((e.timestart >= :tstart1 OR e.timestart + e.timeduration > :tstart2) AND e.timestart <= :tend)";
794         $params['tstart1'] = $tstart;
795         $params['tstart2'] = $tstart;
796         $params['tend'] = $tend;
797     } else {
798         $timeclause = "(e.timestart >= :tstart AND e.timestart <= :tend)";
799         $params['tstart'] = $tstart;
800         $params['tend'] = $tend;
801     }
802     $whereconditions[] = $timeclause;
804     // Show visible only.
805     if ($ignorehidden) {
806         $whereconditions[] = "(e.visible = 1)";
807     }
809     // Build the main query's WHERE clause.
810     $whereclause = implode(' AND ', $whereconditions);
812     // Build SQL subquery and conditions for filtered events based on priorities.
813     $subquerywhere = '';
814     $subqueryconditions = [];
816     // Get the user's courses. Otherwise, get the default courses being shown by the calendar.
817     $usercourses = calendar_get_default_courses();
819     // Set calendar filters.
820     list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true);
821     $subqueryparams = [];
823     // Flag to indicate whether the query needs to exclude group overrides.
824     $viewgroupsonly = false;
826     if ($user) {
827         // Set filter condition for the user's events.
828         $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)";
829         $subqueryparams['user'] = $user;
831         foreach ($usercourses as $courseid) {
832             if (has_capability('moodle/site:accessallgroups', context_course::instance($courseid))) {
833                 $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id');
834                 if (count($usergroupmembership) == 0) {
835                     $viewgroupsonly = true;
836                     break;
837                 }
838             }
839         }
840     }
842     // Set filter condition for the user's group events.
843     if ($usergroups === true || $viewgroupsonly) {
844         // Fetch group events, but not group overrides.
845         $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')";
846     } else if (!empty($usergroups)) {
847         // Fetch group events and group overrides.
848         list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
849         $subqueryconditions[] = "(ev.groupid $inusergroups)";
850         $subqueryparams = array_merge($subqueryparams, $inusergroupparams);
851     }
853     // Set filter condition for the user's courses.
854     if (!empty($usercourses)) {
855         list($inusercourses, $inusercoursesparams) = $DB->get_in_or_equal($usercourses, SQL_PARAMS_NAMED);
856         $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $inusercourses)";
857         $subqueryparams = array_merge($subqueryparams, $inusercoursesparams);
858     }
860     // Build the WHERE condition for the sub-query.
861     if (!empty($subqueryconditions)) {
862         $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);
863     }
865     // Merge subquery parameters to the parameters of the main query.
866     if (!empty($subqueryparams)) {
867         $params = array_merge($params, $subqueryparams);
868     }
870     // Sub-query that fetches the list of unique events that were filtered based on priority.
871     $subquery = "SELECT ev.modulename,
872                         ev.instance,
873                         ev.eventtype,
874                         MAX(ev.priority) as priority
875                    FROM {event} ev
876                   $subquerywhere
877                GROUP BY ev.modulename, ev.instance, ev.eventtype";
879     // Build the main query.
880     $sql = "SELECT e.*
881               FROM {event} e
882         INNER JOIN ($subquery) fe
883                 ON e.modulename = fe.modulename
884                    AND e.instance = fe.instance
885                    AND e.eventtype = fe.eventtype
886                    AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL))
887          LEFT JOIN {modules} m
888                 ON e.modulename = m.name
889              WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause
890           ORDER BY e.timestart";
891     $events = $DB->get_records_sql($sql, $params);
893     if ($events === false) {
894         $events = array();
895     }
896     return $events;
899 /** Get calendar events by id
900  *
901  * @since Moodle 2.5
902  * @param array $eventids list of event ids
903  * @return array Array of event entries, empty array if nothing found
904  */
906 function calendar_get_events_by_id($eventids) {
907     global $DB;
909     if (!is_array($eventids) || empty($eventids)) {
910         return array();
911     }
912     list($wheresql, $params) = $DB->get_in_or_equal($eventids);
913     $wheresql = "id $wheresql";
915     return $DB->get_records_select('event', $wheresql, $params);
918 /**
919  * Get control options for Calendar
920  *
921  * @param string $type of calendar
922  * @param array $data calendar information
923  * @return string $content return available control for the calender in html
924  */
925 function calendar_top_controls($type, $data) {
926     global $PAGE, $OUTPUT;
928     // Get the calendar type we are using.
929     $calendartype = \core_calendar\type_factory::get_calendar_instance();
931     $content = '';
933     // Ensure course id passed if relevant.
934     $courseid = '';
935     if (!empty($data['id'])) {
936         $courseid = '&amp;course='.$data['id'];
937     }
939     // If we are passing a month and year then we need to convert this to a timestamp to
940     // support multiple calendars. No where in core should these be passed, this logic
941     // here is for third party plugins that may use this function.
942     if (!empty($data['m']) && !empty($date['y'])) {
943         if (!isset($data['d'])) {
944             $data['d'] = 1;
945         }
946         if (!checkdate($data['m'], $data['d'], $data['y'])) {
947             $time = time();
948         } else {
949             $time = make_timestamp($data['y'], $data['m'], $data['d']);
950         }
951     } else if (!empty($data['time'])) {
952         $time = $data['time'];
953     } else {
954         $time = time();
955     }
957     // Get the date for the calendar type.
958     $date = $calendartype->timestamp_to_date_array($time);
960     $urlbase = $PAGE->url;
962     // We need to get the previous and next months in certain cases.
963     if ($type == 'frontpage' || $type == 'course' || $type == 'month') {
964         $prevmonth = calendar_sub_month($date['mon'], $date['year']);
965         $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1);
966         $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'],
967             $prevmonthtime['hour'], $prevmonthtime['minute']);
969         $nextmonth = calendar_add_month($date['mon'], $date['year']);
970         $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1);
971         $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'],
972             $nextmonthtime['hour'], $nextmonthtime['minute']);
973     }
975     switch ($type) {
976         case 'frontpage':
977             $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, true, $prevmonthtime);
978             $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, true, $nextmonthtime);
979             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time);
981             if (!empty($data['id'])) {
982                 $calendarlink->param('course', $data['id']);
983             }
985             $prevlink = $prevlink;
986             $right = $nextlink;
988             $content .= html_writer::start_tag('div', array('class'=>'calendar-controls'));
989             $content .= $prevlink.'<span class="hide"> | </span>';
990             $content .= html_writer::tag('span', html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')), array('title'=>get_string('monththis','calendar'))), array('class'=>'current'));
991             $content .= '<span class="hide"> | </span>'. $right;
992             $content .= "<span class=\"clearer\"><!-- --></span>\n";
993             $content .= html_writer::end_tag('div');
995             break;
996         case 'course':
997             $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, true, $prevmonthtime);
998             $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, true, $nextmonthtime);
999             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time);
1001             if (!empty($data['id'])) {
1002                 $calendarlink->param('course', $data['id']);
1003             }
1005             $content .= html_writer::start_tag('div', array('class'=>'calendar-controls'));
1006             $content .= $prevlink.'<span class="hide"> | </span>';
1007             $content .= html_writer::tag('span', html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')), array('title'=>get_string('monththis','calendar'))), array('class'=>'current'));
1008             $content .= '<span class="hide"> | </span>'. $nextlink;
1009             $content .= "<span class=\"clearer\"><!-- --></span>";
1010             $content .= html_writer::end_tag('div');
1011             break;
1012         case 'upcoming':
1013             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'upcoming')), false, false, false, $time);
1014             if (!empty($data['id'])) {
1015                 $calendarlink->param('course', $data['id']);
1016             }
1017             $calendarlink = html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
1018             $content .= html_writer::tag('div', $calendarlink, array('class'=>'centered'));
1019             break;
1020         case 'display':
1021             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time);
1022             if (!empty($data['id'])) {
1023                 $calendarlink->param('course', $data['id']);
1024             }
1025             $calendarlink = html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
1026             $content .= html_writer::tag('h3', $calendarlink);
1027             break;
1028         case 'month':
1029             $prevlink = calendar_get_link_previous(userdate($prevmonthtime, get_string('strftimemonthyear')), 'view.php?view=month'.$courseid.'&amp;', false, false, false, false, $prevmonthtime);
1030             $nextlink = calendar_get_link_next(userdate($nextmonthtime, get_string('strftimemonthyear')), 'view.php?view=month'.$courseid.'&amp;', false, false, false, false, $nextmonthtime);
1032             $content .= html_writer::start_tag('div', array('class'=>'calendar-controls'));
1033             $content .= $prevlink . '<span class="hide"> | </span>';
1034             $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current');
1035             $content .= '<span class="hide"> | </span>' . $nextlink;
1036             $content .= '<span class="clearer"><!-- --></span>';
1037             $content .= html_writer::end_tag('div')."\n";
1038             break;
1039         case 'day':
1040             $days = calendar_get_days();
1042             $prevtimestamp = strtotime('-1 day', $time);
1043             $nexttimestamp = strtotime('+1 day', $time);
1045             $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp);
1046             $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp);
1048             $prevname = $days[$prevdate['wday']]['fullname'];
1049             $nextname = $days[$nextdate['wday']]['fullname'];
1050             $prevlink = calendar_get_link_previous($prevname, 'view.php?view=day'.$courseid.'&amp;', false, false, false, false, $prevtimestamp);
1051             $nextlink = calendar_get_link_next($nextname, 'view.php?view=day'.$courseid.'&amp;', false, false, false, false, $nexttimestamp);
1053             $content .= html_writer::start_tag('div', array('class'=>'calendar-controls'));
1054             $content .= $prevlink;
1055             $content .= '<span class="hide"> | </span><span class="current">'.userdate($time, get_string('strftimedaydate')).'</span>';
1056             $content .= '<span class="hide"> | </span>'. $nextlink;
1057             $content .= "<span class=\"clearer\"><!-- --></span>";
1058             $content .= html_writer::end_tag('div')."\n";
1060             break;
1061     }
1062     return $content;
1065 /**
1066  * Formats a filter control element.
1067  *
1068  * @param moodle_url $url of the filter
1069  * @param int $type constant defining the type filter
1070  * @return string html content of the element
1071  */
1072 function calendar_filter_controls_element(moodle_url $url, $type) {
1073     global $OUTPUT;
1074     switch ($type) {
1075         case CALENDAR_EVENT_GLOBAL:
1076             $typeforhumans = 'global';
1077             $class = 'calendar_event_global';
1078             break;
1079         case CALENDAR_EVENT_COURSE:
1080             $typeforhumans = 'course';
1081             $class = 'calendar_event_course';
1082             break;
1083         case CALENDAR_EVENT_GROUP:
1084             $typeforhumans = 'groups';
1085             $class = 'calendar_event_group';
1086             break;
1087         case CALENDAR_EVENT_USER:
1088             $typeforhumans = 'user';
1089             $class = 'calendar_event_user';
1090             break;
1091     }
1092     if (calendar_show_event_type($type)) {
1093         $icon = $OUTPUT->pix_icon('t/hide', get_string('hide'));
1094         $str = get_string('hide'.$typeforhumans.'events', 'calendar');
1095     } else {
1096         $icon = $OUTPUT->pix_icon('t/show', get_string('show'));
1097         $str = get_string('show'.$typeforhumans.'events', 'calendar');
1098     }
1099     $content = html_writer::start_tag('li', array('class' => 'calendar_event'));
1100     $content .= html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow'));
1101     $content .= html_writer::tag('span', $icon, array('class' => $class));
1102     $content .= html_writer::tag('span', $str, array('class' => 'eventname'));
1103     $content .= html_writer::end_tag('a');
1104     $content .= html_writer::end_tag('li');
1105     return $content;
1108 /**
1109  * Get the controls filter for calendar.
1110  *
1111  * Filter is used to hide calendar info from the display page
1112  *
1113  * @param moodle_url $returnurl return-url for filter controls
1114  * @return string $content return filter controls in html
1115  */
1116 function calendar_filter_controls(moodle_url $returnurl) {
1117     global $CFG, $USER, $OUTPUT;
1119     $groupevents = true;
1120     $id = optional_param( 'id',0,PARAM_INT );
1121     $seturl = new moodle_url('/calendar/set.php', array('return' => base64_encode($returnurl->out_as_local_url(false)), 'sesskey'=>sesskey()));
1122     $content = html_writer::start_tag('ul');
1124     $seturl->param('var', 'showglobal');
1125     $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GLOBAL);
1127     $seturl->param('var', 'showcourses');
1128     $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_COURSE);
1130     if (isloggedin() && !isguestuser()) {
1131         if ($groupevents) {
1132             // This course MIGHT have group events defined, so show the filter
1133             $seturl->param('var', 'showgroups');
1134             $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GROUP);
1135         } else {
1136             // This course CANNOT have group events, so lose the filter
1137         }
1138         $seturl->param('var', 'showuser');
1139         $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER);
1140     }
1141     $content .= html_writer::end_tag('ul');
1143     return $content;
1146 /**
1147  * Return the representation day
1148  *
1149  * @param int $tstamp Timestamp in GMT
1150  * @param int $now current Unix timestamp
1151  * @param bool $usecommonwords
1152  * @return string the formatted date/time
1153  */
1154 function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) {
1156     static $shortformat;
1157     if(empty($shortformat)) {
1158         $shortformat = get_string('strftimedayshort');
1159     }
1161     if($now === false) {
1162         $now = time();
1163     }
1165     // To have it in one place, if a change is needed
1166     $formal = userdate($tstamp, $shortformat);
1168     $datestamp = usergetdate($tstamp);
1169     $datenow   = usergetdate($now);
1171     if($usecommonwords == false) {
1172         // We don't want words, just a date
1173         return $formal;
1174     }
1175     else if($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) {
1176         // Today
1177         return get_string('today', 'calendar');
1178     }
1179     else if(
1180         ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) ||
1181         ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12 && $datenow['yday'] == 1)
1182         ) {
1183         // Yesterday
1184         return get_string('yesterday', 'calendar');
1185     }
1186     else if(
1187         ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) ||
1188         ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12 && $datestamp['yday'] == 1)
1189         ) {
1190         // Tomorrow
1191         return get_string('tomorrow', 'calendar');
1192     }
1193     else {
1194         return $formal;
1195     }
1198 /**
1199  * return the formatted representation time
1200  *
1201  * @param int $time the timestamp in UTC, as obtained from the database
1202  * @return string the formatted date/time
1203  */
1204 function calendar_time_representation($time) {
1205     static $langtimeformat = NULL;
1206     if($langtimeformat === NULL) {
1207         $langtimeformat = get_string('strftimetime');
1208     }
1209     $timeformat = get_user_preferences('calendar_timeformat');
1210     if(empty($timeformat)){
1211         $timeformat = get_config(NULL,'calendar_site_timeformat');
1212     }
1213     // The ? is needed because the preference might be present, but empty
1214     return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat);
1217 /**
1218  * Adds day, month, year arguments to a URL and returns a moodle_url object.
1219  *
1220  * @param string|moodle_url $linkbase
1221  * @param int $d The number of the day.
1222  * @param int $m The number of the month.
1223  * @param int $y The number of the year.
1224  * @param int $time the unixtime, used for multiple calendar support. The values $d,
1225  *     $m and $y are kept for backwards compatibility.
1226  * @return moodle_url|null $linkbase
1227  */
1228 function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) {
1229     if (empty($linkbase)) {
1230         return '';
1231     }
1232     if (!($linkbase instanceof moodle_url)) {
1233         $linkbase = new moodle_url($linkbase);
1234     }
1236     // If a day, month and year were passed then convert it to a timestamp. If these were passed
1237     // then we can assume the day, month and year are passed as Gregorian, as no where in core
1238     // should we be passing these values rather than the time.
1239     if (!empty($d) && !empty($m) && !empty($y)) {
1240         if (checkdate($m, $d, $y)) {
1241             $time = make_timestamp($y, $m, $d);
1242         } else {
1243             $time = time();
1244         }
1245     } else if (empty($time)) {
1246         $time = time();
1247     }
1249     $linkbase->param('time', $time);
1251     return $linkbase;
1254 /**
1255  * Build and return a previous month HTML link, with an arrow.
1256  *
1257  * @param string $text The text label.
1258  * @param string|moodle_url $linkbase The URL stub.
1259  * @param int $d The number of the date.
1260  * @param int $m The number of the month.
1261  * @param int $y year The number of the year.
1262  * @param bool $accesshide Default visible, or hide from all except screenreaders.
1263  * @param int $time the unixtime, used for multiple calendar support. The values $d,
1264  *     $m and $y are kept for backwards compatibility.
1265  * @return string HTML string.
1266  */
1267 function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
1268     $href = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y, $time);
1269     if (empty($href)) {
1270         return $text;
1271     }
1272     return link_arrow_left($text, (string)$href, $accesshide, 'previous');
1275 /**
1276  * Build and return a next month HTML link, with an arrow.
1277  *
1278  * @param string $text The text label.
1279  * @param string|moodle_url $linkbase The URL stub.
1280  * @param int $d the number of the Day
1281  * @param int $m The number of the month.
1282  * @param int $y The number of the year.
1283  * @param bool $accesshide Default visible, or hide from all except screenreaders.
1284  * @param int $time the unixtime, used for multiple calendar support. The values $d,
1285  *     $m and $y are kept for backwards compatibility.
1286  * @return string HTML string.
1287  */
1288 function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
1289     $href = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y, $time);
1290     if (empty($href)) {
1291         return $text;
1292     }
1293     return link_arrow_right($text, (string)$href, $accesshide, 'next');
1296 /**
1297  * Return the name of the weekday
1298  *
1299  * @param string $englishname
1300  * @return string of the weekeday
1301  */
1302 function calendar_wday_name($englishname) {
1303     return get_string(strtolower($englishname), 'calendar');
1306 /**
1307  * Return the number of days in month
1308  *
1309  * @param int $month the number of the month.
1310  * @param int $year the number of the year
1311  * @return int
1312  */
1313 function calendar_days_in_month($month, $year) {
1314     $calendartype = \core_calendar\type_factory::get_calendar_instance();
1315     return $calendartype->get_num_days_in_month($year, $month);
1318 /**
1319  * Get the upcoming event block
1320  *
1321  * @param array $events list of events
1322  * @param moodle_url|string $linkhref link to event referer
1323  * @param boolean $showcourselink whether links to courses should be shown
1324  * @return string|null $content html block content
1325  */
1326 function calendar_get_block_upcoming($events, $linkhref = NULL, $showcourselink = false) {
1327     $content = '';
1328     $lines = count($events);
1329     if (!$lines) {
1330         return $content;
1331     }
1333     for ($i = 0; $i < $lines; ++$i) {
1334         if (!isset($events[$i]->time)) {   // Just for robustness
1335             continue;
1336         }
1337         $events[$i] = calendar_add_event_metadata($events[$i]);
1338         $content .= '<div class="event"><span class="icon c0">'.$events[$i]->icon.'</span>';
1339         if (!empty($events[$i]->referer)) {
1340             // That's an activity event, so let's provide the hyperlink
1341             $content .= $events[$i]->referer;
1342         } else {
1343             if(!empty($linkhref)) {
1344                 $href = calendar_get_link_href(new moodle_url(CALENDAR_URL . $linkhref), 0, 0, 0, $events[$i]->timestart);
1345                 $href->set_anchor('event_'.$events[$i]->id);
1346                 $content .= html_writer::link($href, $events[$i]->name);
1347             }
1348             else {
1349                 $content .= $events[$i]->name;
1350             }
1351         }
1352         $events[$i]->time = str_replace('&raquo;', '<br />&raquo;', $events[$i]->time);
1353         if ($showcourselink && !empty($events[$i]->courselink)) {
1354             $content .= html_writer::div($events[$i]->courselink, 'course');
1355         }
1356         $content .= '<div class="date">'.$events[$i]->time.'</div></div>';
1357         if ($i < $lines - 1) $content .= '<hr />';
1358     }
1360     return $content;
1363 /**
1364  * Get the next following month
1365  *
1366  * @param int $month the number of the month.
1367  * @param int $year the number of the year.
1368  * @return array the following month
1369  */
1370 function calendar_add_month($month, $year) {
1371     // Get the calendar type we are using.
1372     $calendartype = \core_calendar\type_factory::get_calendar_instance();
1373     return $calendartype->get_next_month($year, $month);
1376 /**
1377  * Get the previous month.
1378  *
1379  * @param int $month the number of the month.
1380  * @param int $year the number of the year.
1381  * @return array previous month
1382  */
1383 function calendar_sub_month($month, $year) {
1384     // Get the calendar type we are using.
1385     $calendartype = \core_calendar\type_factory::get_calendar_instance();
1386     return $calendartype->get_prev_month($year, $month);
1389 /**
1390  * Get per-day basis events
1391  *
1392  * @param array $events list of events
1393  * @param int $month the number of the month
1394  * @param int $year the number of the year
1395  * @param array $eventsbyday event on specific day
1396  * @param array $durationbyday duration of the event in days
1397  * @param array $typesbyday event type (eg: global, course, user, or group)
1398  * @param array $courses list of courses
1399  * @return void
1400  */
1401 function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) {
1402     // Get the calendar type we are using.
1403     $calendartype = \core_calendar\type_factory::get_calendar_instance();
1405     $eventsbyday = array();
1406     $typesbyday = array();
1407     $durationbyday = array();
1409     if($events === false) {
1410         return;
1411     }
1413     foreach ($events as $event) {
1414         $startdate = $calendartype->timestamp_to_date_array($event->timestart);
1415         // Set end date = start date if no duration
1416         if ($event->timeduration) {
1417             $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1);
1418         } else {
1419             $enddate = $startdate;
1420         }
1422         // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair
1423         if(!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) && ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) {
1424             // Out of bounds
1425             continue;
1426         }
1428         $eventdaystart = intval($startdate['mday']);
1430         if($startdate['mon'] == $month && $startdate['year'] == $year) {
1431             // Give the event to its day
1432             $eventsbyday[$eventdaystart][] = $event->id;
1434             // Mark the day as having such an event
1435             if($event->courseid == SITEID && $event->groupid == 0) {
1436                 $typesbyday[$eventdaystart]['startglobal'] = true;
1437                 // Set event class for global event
1438                 $events[$event->id]->class = 'calendar_event_global';
1439             }
1440             else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
1441                 $typesbyday[$eventdaystart]['startcourse'] = true;
1442                 // Set event class for course event
1443                 $events[$event->id]->class = 'calendar_event_course';
1444             }
1445             else if($event->groupid) {
1446                 $typesbyday[$eventdaystart]['startgroup'] = true;
1447                 // Set event class for group event
1448                 $events[$event->id]->class = 'calendar_event_group';
1449             }
1450             else if($event->userid) {
1451                 $typesbyday[$eventdaystart]['startuser'] = true;
1452                 // Set event class for user event
1453                 $events[$event->id]->class = 'calendar_event_user';
1454             }
1455         }
1457         if($event->timeduration == 0) {
1458             // Proceed with the next
1459             continue;
1460         }
1462         // The event starts on $month $year or before. So...
1463         $lowerbound = $startdate['mon'] == $month && $startdate['year'] == $year ? intval($startdate['mday']) : 0;
1465         // Also, it ends on $month $year or later...
1466         $upperbound = $enddate['mon'] == $month && $enddate['year'] == $year ? intval($enddate['mday']) : calendar_days_in_month($month, $year);
1468         // Mark all days between $lowerbound and $upperbound (inclusive) as duration
1469         for($i = $lowerbound + 1; $i <= $upperbound; ++$i) {
1470             $durationbyday[$i][] = $event->id;
1471             if($event->courseid == SITEID && $event->groupid == 0) {
1472                 $typesbyday[$i]['durationglobal'] = true;
1473             }
1474             else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
1475                 $typesbyday[$i]['durationcourse'] = true;
1476             }
1477             else if($event->groupid) {
1478                 $typesbyday[$i]['durationgroup'] = true;
1479             }
1480             else if($event->userid) {
1481                 $typesbyday[$i]['durationuser'] = true;
1482             }
1483         }
1485     }
1486     return;
1489 /**
1490  * Get current module cache
1491  *
1492  * @param array $coursecache list of course cache
1493  * @param string $modulename name of the module
1494  * @param int $instance module instance number
1495  * @return stdClass|bool $module information
1496  */
1497 function calendar_get_module_cached(&$coursecache, $modulename, $instance) {
1498     $module = get_coursemodule_from_instance($modulename, $instance);
1500     if($module === false) return false;
1501     if(!calendar_get_course_cached($coursecache, $module->course)) {
1502         return false;
1503     }
1504     return $module;
1507 /**
1508  * Get current course cache
1509  *
1510  * @param array $coursecache list of course cache
1511  * @param int $courseid id of the course
1512  * @return stdClass $coursecache[$courseid] return the specific course cache
1513  */
1514 function calendar_get_course_cached(&$coursecache, $courseid) {
1515     if (!isset($coursecache[$courseid])) {
1516         $coursecache[$courseid] = get_course($courseid);
1517     }
1518     return $coursecache[$courseid];
1521 /**
1522  * Get group from groupid for calendar display
1523  *
1524  * @param int $groupid
1525  * @return stdClass group object with fields 'id', 'name' and 'courseid'
1526  */
1527 function calendar_get_group_cached($groupid) {
1528     static $groupscache = array();
1529     if (!isset($groupscache[$groupid])) {
1530         $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid');
1531     }
1532     return $groupscache[$groupid];
1535 /**
1536  * Returns the courses to load events for, the
1537  *
1538  * @param array $courseeventsfrom An array of courses to load calendar events for
1539  * @param bool $ignorefilters specify the use of filters, false is set as default
1540  * @return array An array of courses, groups, and user to load calendar events for based upon filters
1541  */
1542 function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
1543     global $USER, $CFG, $DB;
1545     // For backwards compatability we have to check whether the courses array contains
1546     // just id's in which case we need to load course objects.
1547     $coursestoload = array();
1548     foreach ($courseeventsfrom as $id => $something) {
1549         if (!is_object($something)) {
1550             $coursestoload[] = $id;
1551             unset($courseeventsfrom[$id]);
1552         }
1553     }
1554     if (!empty($coursestoload)) {
1555         // TODO remove this in 2.2
1556         debugging('calendar_set_filters now preferes an array of course objects with preloaded contexts', DEBUG_DEVELOPER);
1557         $courseeventsfrom = array_merge($courseeventsfrom, $DB->get_records_list('course', 'id', $coursestoload));
1558     }
1560     $courses = array();
1561     $user = false;
1562     $group = false;
1564     // capabilities that allow seeing group events from all groups
1565     // TODO: rewrite so that moodle/calendar:manageentries is not necessary here
1566     $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries');
1568     $isloggedin = isloggedin();
1570     if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE)) {
1571         $courses = array_keys($courseeventsfrom);
1572     }
1573     if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL)) {
1574         $courses[] = SITEID;
1575     }
1576     $courses = array_unique($courses);
1577     sort($courses);
1579     if (!empty($courses) && in_array(SITEID, $courses)) {
1580         // Sort courses for consistent colour highlighting
1581         // Effectively ignoring SITEID as setting as last course id
1582         $key = array_search(SITEID, $courses);
1583         unset($courses[$key]);
1584         $courses[] = SITEID;
1585     }
1587     if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) {
1588         $user = $USER->id;
1589     }
1591     if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) {
1593         if (count($courseeventsfrom)==1) {
1594             $course = reset($courseeventsfrom);
1595             if (has_any_capability($allgroupscaps, context_course::instance($course->id))) {
1596                 $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id');
1597                 $group = array_keys($coursegroups);
1598             }
1599         }
1600         if ($group === false) {
1601             if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, context_system::instance())) {
1602                 $group = true;
1603             } else if ($isloggedin) {
1604                 $groupids = array();
1606                 // We already have the courses to examine in $courses
1607                 // For each course...
1608                 foreach ($courseeventsfrom as $courseid => $course) {
1609                     // If the user is an editing teacher in there,
1610                     if (!empty($USER->groupmember[$course->id])) {
1611                         // We've already cached the users groups for this course so we can just use that
1612                         $groupids = array_merge($groupids, $USER->groupmember[$course->id]);
1613                     } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
1614                         // If this course has groups, show events from all of those related to the current user
1615                         $coursegroups = groups_get_user_groups($course->id, $USER->id);
1616                         $groupids = array_merge($groupids, $coursegroups['0']);
1617                     }
1618                 }
1619                 if (!empty($groupids)) {
1620                     $group = $groupids;
1621                 }
1622             }
1623         }
1624     }
1625     if (empty($courses)) {
1626         $courses = false;
1627     }
1629     return array($courses, $group, $user);
1632 /**
1633  * Return the capability for editing calendar event
1634  *
1635  * @param calendar_event $event event object
1636  * @return bool capability to edit event
1637  */
1638 function calendar_edit_event_allowed($event) {
1639     global $USER, $DB;
1641     // Must be logged in
1642     if (!isloggedin()) {
1643         return false;
1644     }
1646     // can not be using guest account
1647     if (isguestuser()) {
1648         return false;
1649     }
1651     // You cannot edit calendar subscription events presently.
1652     if (!empty($event->subscriptionid)) {
1653         return false;
1654     }
1656     $sitecontext = context_system::instance();
1657     // if user has manageentries at site level, return true
1658     if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
1659         return true;
1660     }
1662     // if groupid is set, it's definitely a group event
1663     if (!empty($event->groupid)) {
1664         // Allow users to add/edit group events if:
1665         // 1) They have manageentries (= entries for whole course)
1666         // 2) They have managegroupentries AND are in the group
1667         $group = $DB->get_record('groups', array('id'=>$event->groupid));
1668         return $group && (
1669             has_capability('moodle/calendar:manageentries', $event->context) ||
1670             (has_capability('moodle/calendar:managegroupentries', $event->context)
1671                 && groups_is_member($event->groupid)));
1672     } else if (!empty($event->courseid)) {
1673     // if groupid is not set, but course is set,
1674     // it's definiely a course event
1675         return has_capability('moodle/calendar:manageentries', $event->context);
1676     } else if (!empty($event->userid) && $event->userid == $USER->id) {
1677     // if course is not set, but userid id set, it's a user event
1678         return (has_capability('moodle/calendar:manageownentries', $event->context));
1679     } else if (!empty($event->userid)) {
1680         return (has_capability('moodle/calendar:manageentries', $event->context));
1681     }
1682     return false;
1685 /**
1686  * Returns the default courses to display on the calendar when there isn't a specific
1687  * course to display.
1688  *
1689  * @return array $courses Array of courses to display
1690  */
1691 function calendar_get_default_courses() {
1692     global $CFG, $DB;
1694     if (!isloggedin()) {
1695         return array();
1696     }
1698     $courses = array();
1699     if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', context_system::instance())) {
1700         $select = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1701         $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1702         $sql = "SELECT c.* $select
1703                   FROM {course} c
1704                   $join
1705                   WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id)
1706                   ";
1707         $courses = $DB->get_records_sql($sql, array('contextlevel' => CONTEXT_COURSE), 0, 20);
1708         foreach ($courses as $course) {
1709             context_helper::preload_from_record($course);
1710         }
1711         return $courses;
1712     }
1714     $courses = enrol_get_my_courses();
1716     return $courses;
1719 /**
1720  * Display calendar preference button
1721  *
1722  * @param stdClass $course course object
1723  * @deprecated since Moodle 3.2
1724  * @todo MDL-55875 This will be deleted in Moodle 3.6.
1725  * @return string return preference button in html
1726  */
1727 function calendar_preferences_button(stdClass $course) {
1728     global $OUTPUT;
1730     // Guests have no preferences
1731     if (!isloggedin() || isguestuser()) {
1732         return '';
1733     }
1734     debugging('This should no longer be used, the calendar preferences are now linked to the user preferences page');
1736     return $OUTPUT->single_button(new moodle_url('/user/calendar.php'), get_string("preferences", "calendar"));
1739 /**
1740  * Get event format time
1741  *
1742  * @param calendar_event $event event object
1743  * @param int $now current time in gmt
1744  * @param array $linkparams list of params for event link
1745  * @param bool $usecommonwords the words as formatted date/time.
1746  * @param int $showtime determine the show time GMT timestamp
1747  * @return string $eventtime link/string for event time
1748  */
1749 function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) {
1750     $starttime = $event->timestart;
1751     $endtime = $event->timestart + $event->timeduration;
1753     if (empty($linkparams) || !is_array($linkparams)) {
1754         $linkparams = array();
1755     }
1757     $linkparams['view'] = 'day';
1759     // OK, now to get a meaningful display...
1760     // Check if there is a duration for this event.
1761     if ($event->timeduration) {
1762         // Get the midnight of the day the event will start.
1763         $usermidnightstart = usergetmidnight($starttime);
1764         // Get the midnight of the day the event will end.
1765         $usermidnightend = usergetmidnight($endtime);
1766         // Check if we will still be on the same day.
1767         if ($usermidnightstart == $usermidnightend) {
1768             // Check if we are running all day.
1769             if ($event->timeduration == DAYSECS) {
1770                 $time = get_string('allday', 'calendar');
1771             } else { // Specify the time we will be running this from.
1772                 $datestart = calendar_time_representation($starttime);
1773                 $dateend = calendar_time_representation($endtime);
1774                 $time = $datestart . ' <strong>&raquo;</strong> ' . $dateend;
1775             }
1777             // Set printable representation.
1778             if (!$showtime) {
1779                 $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
1780                 $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
1781                 $eventtime = html_writer::link($url, $day) . ', ' . $time;
1782             } else {
1783                 $eventtime = $time;
1784             }
1785         } else { // It must spans two or more days.
1786             $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', ';
1787             if ($showtime == $usermidnightstart) {
1788                 $daystart = '';
1789             }
1790             $timestart = calendar_time_representation($event->timestart);
1791             $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', ';
1792             if ($showtime == $usermidnightend) {
1793                 $dayend = '';
1794             }
1795             $timeend = calendar_time_representation($event->timestart + $event->timeduration);
1797             // Set printable representation.
1798             if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) {
1799                 $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
1800                 $eventtime = $timestart . ' <strong>&raquo;</strong> ' . html_writer::link($url, $dayend) . $timeend;
1801             } else {
1802                 // The event is in the future, print start and end  links.
1803                 $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime);
1804                 $eventtime  = html_writer::link($url, $daystart) . $timestart . ' <strong>&raquo;</strong> ';
1806                 $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams),  0, 0, 0, $endtime);
1807                 $eventtime .= html_writer::link($url, $dayend) . $timeend;
1808             }
1809         }
1810     } else { // There is no time duration.
1811         $time = calendar_time_representation($event->timestart);
1812         // Set printable representation.
1813         if (!$showtime) {
1814             $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
1815             $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams),  0, 0, 0, $starttime);
1816             $eventtime = html_writer::link($url, $day) . ', ' . trim($time);
1817         } else {
1818             $eventtime = $time;
1819         }
1820     }
1822     // Check if It has expired.
1823     if ($event->timestart + $event->timeduration < $now) {
1824         $eventtime = '<span class="dimmed_text">' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . '</span>';
1825     }
1827     return $eventtime;
1830 /**
1831  * Display month selector options
1832  *
1833  * @param string $name for the select element
1834  * @param string|array $selected options for select elements
1835  */
1836 function calendar_print_month_selector($name, $selected) {
1837     $months = array();
1838     for ($i=1; $i<=12; $i++) {
1839         $months[$i] = userdate(gmmktime(12, 0, 0, $i, 15, 2000), '%B');
1840     }
1841     echo html_writer::label(get_string('months'), 'menu'. $name, false, array('class' => 'accesshide'));
1842     echo html_writer::select($months, $name, $selected, false);
1845 /**
1846  * Checks to see if the requested type of event should be shown for the given user.
1847  *
1848  * @param CALENDAR_EVENT_GLOBAL|CALENDAR_EVENT_COURSE|CALENDAR_EVENT_GROUP|CALENDAR_EVENT_USER $type
1849  *          The type to check the display for (default is to display all)
1850  * @param stdClass|int|null $user The user to check for - by default the current user
1851  * @return bool True if the tyep should be displayed false otherwise
1852  */
1853 function calendar_show_event_type($type, $user = null) {
1854     $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
1855     if (get_user_preferences('calendar_persistflt', 0, $user) === 0) {
1856         global $SESSION;
1857         if (!isset($SESSION->calendarshoweventtype)) {
1858             $SESSION->calendarshoweventtype = $default;
1859         }
1860         return $SESSION->calendarshoweventtype & $type;
1861     } else {
1862         return get_user_preferences('calendar_savedflt', $default, $user) & $type;
1863     }
1866 /**
1867  * Sets the display of the event type given $display.
1868  *
1869  * If $display = true the event type will be shown.
1870  * If $display = false the event type will NOT be shown.
1871  * If $display = null the current value will be toggled and saved.
1872  *
1873  * @param CALENDAR_EVENT_GLOBAL|CALENDAR_EVENT_COURSE|CALENDAR_EVENT_GROUP|CALENDAR_EVENT_USER $type object of CALENDAR_EVENT_XXX
1874  * @param bool $display option to display event type
1875  * @param stdClass|int $user moodle user object or id, null means current user
1876  */
1877 function calendar_set_event_type_display($type, $display = null, $user = null) {
1878     $persist = get_user_preferences('calendar_persistflt', 0, $user);
1879     $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
1880     if ($persist === 0) {
1881         global $SESSION;
1882         if (!isset($SESSION->calendarshoweventtype)) {
1883             $SESSION->calendarshoweventtype = $default;
1884         }
1885         $preference = $SESSION->calendarshoweventtype;
1886     } else {
1887         $preference = get_user_preferences('calendar_savedflt', $default, $user);
1888     }
1889     $current = $preference & $type;
1890     if ($display === null) {
1891         $display = !$current;
1892     }
1893     if ($display && !$current) {
1894         $preference += $type;
1895     } else if (!$display && $current) {
1896         $preference -= $type;
1897     }
1898     if ($persist === 0) {
1899         $SESSION->calendarshoweventtype = $preference;
1900     } else {
1901         if ($preference == $default) {
1902             unset_user_preference('calendar_savedflt', $user);
1903         } else {
1904             set_user_preference('calendar_savedflt', $preference, $user);
1905         }
1906     }
1909 /**
1910  * Get calendar's allowed types
1911  *
1912  * @param stdClass $allowed list of allowed edit for event  type
1913  * @param stdClass|int $course object of a course or course id
1914  */
1915 function calendar_get_allowed_types(&$allowed, $course = null) {
1916     global $USER, $CFG, $DB;
1917     $allowed = new stdClass();
1918     $allowed->user = has_capability('moodle/calendar:manageownentries', context_system::instance());
1919     $allowed->groups = false; // This may change just below
1920     $allowed->courses = false; // This may change just below
1921     $allowed->site = has_capability('moodle/calendar:manageentries', context_course::instance(SITEID));
1923     if (!empty($course)) {
1924         if (!is_object($course)) {
1925             $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST);
1926         }
1927         if ($course->id != SITEID) {
1928             $coursecontext = context_course::instance($course->id);
1929             $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext);
1931             if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
1932                 $allowed->courses = array($course->id => 1);
1934                 if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
1935                     if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
1936                         $allowed->groups = groups_get_all_groups($course->id);
1937                     } else {
1938                         $allowed->groups = groups_get_all_groups($course->id, $USER->id);
1939                     }
1940                 }
1941             } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
1942                 if($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
1943                     if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
1944                         $allowed->groups = groups_get_all_groups($course->id);
1945                     } else {
1946                         $allowed->groups = groups_get_all_groups($course->id, $USER->id);
1947                     }
1948                 }
1949             }
1950         }
1951     }
1954 /**
1955  * See if user can add calendar entries at all
1956  * used to print the "New Event" button
1957  *
1958  * @param stdClass $course object of a course or course id
1959  * @return bool has the capability to add at least one event type
1960  */
1961 function calendar_user_can_add_event($course) {
1962     if (!isloggedin() || isguestuser()) {
1963         return false;
1964     }
1965     calendar_get_allowed_types($allowed, $course);
1966     return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site);
1969 /**
1970  * Check wether the current user is permitted to add events
1971  *
1972  * @param stdClass $event object of event
1973  * @return bool has the capability to add event
1974  */
1975 function calendar_add_event_allowed($event) {
1976     global $USER, $DB;
1978     // can not be using guest account
1979     if (!isloggedin() or isguestuser()) {
1980         return false;
1981     }
1983     $sitecontext = context_system::instance();
1984     // if user has manageentries at site level, always return true
1985     if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
1986         return true;
1987     }
1989     switch ($event->eventtype) {
1990         case 'course':
1991             return has_capability('moodle/calendar:manageentries', $event->context);
1993         case 'group':
1994             // Allow users to add/edit group events if:
1995             // 1) They have manageentries (= entries for whole course)
1996             // 2) They have managegroupentries AND are in the group
1997             $group = $DB->get_record('groups', array('id'=>$event->groupid));
1998             return $group && (
1999                 has_capability('moodle/calendar:manageentries', $event->context) ||
2000                 (has_capability('moodle/calendar:managegroupentries', $event->context)
2001                     && groups_is_member($event->groupid)));
2003         case 'user':
2004             if ($event->userid == $USER->id) {
2005                 return (has_capability('moodle/calendar:manageownentries', $event->context));
2006             }
2007             //there is no 'break;' intentionally
2009         case 'site':
2010             return has_capability('moodle/calendar:manageentries', $event->context);
2012         default:
2013             return has_capability('moodle/calendar:manageentries', $event->context);
2014     }
2017 /**
2018  * Manage calendar events
2019  *
2020  * This class provides the required functionality in order to manage calendar events.
2021  * It was introduced as part of Moodle 2.0 and was created in order to provide a
2022  * better framework for dealing with calendar events in particular regard to file
2023  * handling through the new file API
2024  *
2025  * @package    core_calendar
2026  * @category   calendar
2027  * @copyright  2009 Sam Hemelryk
2028  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2029  *
2030  * @property int $id The id within the event table
2031  * @property string $name The name of the event
2032  * @property string $description The description of the event
2033  * @property int $format The format of the description FORMAT_?
2034  * @property int $courseid The course the event is associated with (0 if none)
2035  * @property int $groupid The group the event is associated with (0 if none)
2036  * @property int $userid The user the event is associated with (0 if none)
2037  * @property int $repeatid If this is a repeated event this will be set to the
2038  *                          id of the original
2039  * @property string $modulename If added by a module this will be the module name
2040  * @property int $instance If added by a module this will be the module instance
2041  * @property string $eventtype The event type
2042  * @property int $timestart The start time as a timestamp
2043  * @property int $timeduration The duration of the event in seconds
2044  * @property int $visible 1 if the event is visible
2045  * @property int $uuid ?
2046  * @property int $sequence ?
2047  * @property int $timemodified The time last modified as a timestamp
2048  */
2049 class calendar_event {
2051     /** @var array An object containing the event properties can be accessed via the magic __get/set methods */
2052     protected $properties = null;
2054     /**
2055      * @var string The converted event discription with file paths resolved. This gets populated when someone requests description for the first time */
2056     protected $_description = null;
2058     /** @var array The options to use with this description editor */
2059     protected $editoroptions = array(
2060             'subdirs'=>false,
2061             'forcehttps'=>false,
2062             'maxfiles'=>-1,
2063             'maxbytes'=>null,
2064             'trusttext'=>false);
2066     /** @var object The context to use with the description editor */
2067     protected $editorcontext = null;
2069     /**
2070      * Instantiates a new event and optionally populates its properties with the
2071      * data provided
2072      *
2073      * @param stdClass $data Optional. An object containing the properties to for
2074      *                  an event
2075      */
2076     public function __construct($data=null) {
2077         global $CFG, $USER;
2079         // First convert to object if it is not already (should either be object or assoc array)
2080         if (!is_object($data)) {
2081             $data = (object)$data;
2082         }
2084         $this->editoroptions['maxbytes'] = $CFG->maxbytes;
2086         $data->eventrepeats = 0;
2088         if (empty($data->id)) {
2089             $data->id = null;
2090         }
2092         if (!empty($data->subscriptionid)) {
2093             $data->subscription = calendar_get_subscription($data->subscriptionid);
2094         }
2096         // Default to a user event
2097         if (empty($data->eventtype)) {
2098             $data->eventtype = 'user';
2099         }
2101         // Default to the current user
2102         if (empty($data->userid)) {
2103             $data->userid = $USER->id;
2104         }
2106         if (!empty($data->timeduration) && is_array($data->timeduration)) {
2107             $data->timeduration = make_timestamp($data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'], $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart;
2108         }
2109         if (!empty($data->description) && is_array($data->description)) {
2110             $data->format = $data->description['format'];
2111             $data->description = $data->description['text'];
2112         } else if (empty($data->description)) {
2113             $data->description = '';
2114             $data->format = editors_get_preferred_format();
2115         }
2116         // Ensure form is defaulted correctly
2117         if (empty($data->format)) {
2118             $data->format = editors_get_preferred_format();
2119         }
2121         if (empty($data->context)) {
2122             $data->context = $this->calculate_context($data);
2123         }
2124         $this->properties = $data;
2125     }
2127     /**
2128      * Magic property method
2129      *
2130      * Attempts to call a set_$key method if one exists otherwise falls back
2131      * to simply set the property
2132      *
2133      * @param string $key property name
2134      * @param mixed $value value of the property
2135      */
2136     public function __set($key, $value) {
2137         if (method_exists($this, 'set_'.$key)) {
2138             $this->{'set_'.$key}($value);
2139         }
2140         $this->properties->{$key} = $value;
2141     }
2143     /**
2144      * Magic get method
2145      *
2146      * Attempts to call a get_$key method to return the property and ralls over
2147      * to return the raw property
2148      *
2149      * @param string $key property name
2150      * @return mixed property value
2151      */
2152     public function __get($key) {
2153         if (method_exists($this, 'get_'.$key)) {
2154             return $this->{'get_'.$key}();
2155         }
2156         if (!isset($this->properties->{$key})) {
2157             throw new coding_exception('Undefined property requested');
2158         }
2159         return $this->properties->{$key};
2160     }
2162     /**
2163      * Stupid PHP needs an isset magic method if you use the get magic method and
2164      * still want empty calls to work.... blah ~!
2165      *
2166      * @param string $key $key property name
2167      * @return bool|mixed property value, false if property is not exist
2168      */
2169     public function __isset($key) {
2170         return !empty($this->properties->{$key});
2171     }
2173     /**
2174      * Calculate the context value needed for calendar_event.
2175      * Event's type can be determine by the available value store in $data
2176      * It is important to check for the existence of course/courseid to determine
2177      * the course event.
2178      * Default value is set to CONTEXT_USER
2179      *
2180      * @param stdClass $data information about event
2181      * @return stdClass The context object.
2182      */
2183     protected function calculate_context(stdClass $data) {
2184         global $USER, $DB;
2186         $context = null;
2187         if (isset($data->courseid) && $data->courseid > 0) {
2188             $context =  context_course::instance($data->courseid);
2189         } else if (isset($data->course) && $data->course > 0) {
2190             $context =  context_course::instance($data->course);
2191         } else if (isset($data->groupid) && $data->groupid > 0) {
2192             $group = $DB->get_record('groups', array('id'=>$data->groupid));
2193             $context = context_course::instance($group->courseid);
2194         } else if (isset($data->userid) && $data->userid > 0 && $data->userid == $USER->id) {
2195             $context =  context_user::instance($data->userid);
2196         } else if (isset($data->userid) && $data->userid > 0 && $data->userid != $USER->id &&
2197                    isset($data->instance) && $data->instance > 0) {
2198             $cm = get_coursemodule_from_instance($data->modulename, $data->instance, 0, false, MUST_EXIST);
2199             $context =  context_course::instance($cm->course);
2200         } else {
2201             $context =  context_user::instance($data->userid);
2202         }
2204         return $context;
2205     }
2207     /**
2208      * Returns an array of editoroptions for this event: Called by __get
2209      * Please use $blah = $event->editoroptions;
2210      *
2211      * @return array event editor options
2212      */
2213     protected function get_editoroptions() {
2214         return $this->editoroptions;
2215     }
2217     /**
2218      * Returns an event description: Called by __get
2219      * Please use $blah = $event->description;
2220      *
2221      * @return string event description
2222      */
2223     protected function get_description() {
2224         global $CFG;
2226         require_once($CFG->libdir . '/filelib.php');
2228         if ($this->_description === null) {
2229             // Check if we have already resolved the context for this event
2230             if ($this->editorcontext === null) {
2231                 // Switch on the event type to decide upon the appropriate context
2232                 // to use for this event
2233                 $this->editorcontext = $this->properties->context;
2234                 if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
2235                         && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
2236                     return clean_text($this->properties->description, $this->properties->format);
2237                 }
2238             }
2240             // Work out the item id for the editor, if this is a repeated event then the files will
2241             // be associated with the original
2242             if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
2243                 $itemid = $this->properties->repeatid;
2244             } else {
2245                 $itemid = $this->properties->id;
2246             }
2248             // Convert file paths in the description so that things display correctly
2249             $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php', $this->editorcontext->id, 'calendar', 'event_description', $itemid);
2250             // Clean the text so no nasties get through
2251             $this->_description = clean_text($this->_description, $this->properties->format);
2252         }
2253         // Finally return the description
2254         return $this->_description;
2255     }
2257     /**
2258      * Return the number of repeat events there are in this events series
2259      *
2260      * @return int number of event repeated
2261      */
2262     public function count_repeats() {
2263         global $DB;
2264         if (!empty($this->properties->repeatid)) {
2265             $this->properties->eventrepeats = $DB->count_records('event', array('repeatid'=>$this->properties->repeatid));
2266             // We don't want to count ourselves
2267             $this->properties->eventrepeats--;
2268         }
2269         return $this->properties->eventrepeats;
2270     }
2272     /**
2273      * Update or create an event within the database
2274      *
2275      * Pass in a object containing the event properties and this function will
2276      * insert it into the database and deal with any associated files
2277      *
2278      * @see self::create()
2279      * @see self::update()
2280      *
2281      * @param stdClass $data object of event
2282      * @param bool $checkcapability if moodle should check calendar managing capability or not
2283      * @return bool event updated
2284      */
2285     public function update($data, $checkcapability=true) {
2286         global $DB, $USER;
2288         foreach ($data as $key=>$value) {
2289             $this->properties->$key = $value;
2290         }
2292         $this->properties->timemodified = time();
2293         $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description));
2295         // Prepare event data.
2296         $eventargs = array(
2297             'context' => $this->properties->context,
2298             'objectid' => $this->properties->id,
2299             'other' => array(
2300                 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
2301                 'timestart' => $this->properties->timestart,
2302                 'name' => $this->properties->name
2303             )
2304         );
2306         if (empty($this->properties->id) || $this->properties->id < 1) {
2308             if ($checkcapability) {
2309                 if (!calendar_add_event_allowed($this->properties)) {
2310                     print_error('nopermissiontoupdatecalendar');
2311                 }
2312             }
2314             if ($usingeditor) {
2315                 switch ($this->properties->eventtype) {
2316                     case 'user':
2317                         $this->properties->courseid = 0;
2318                         $this->properties->course = 0;
2319                         $this->properties->groupid = 0;
2320                         $this->properties->userid = $USER->id;
2321                         break;
2322                     case 'site':
2323                         $this->properties->courseid = SITEID;
2324                         $this->properties->course = SITEID;
2325                         $this->properties->groupid = 0;
2326                         $this->properties->userid = $USER->id;
2327                         break;
2328                     case 'course':
2329                         $this->properties->groupid = 0;
2330                         $this->properties->userid = $USER->id;
2331                         break;
2332                     case 'group':
2333                         $this->properties->userid = $USER->id;
2334                         break;
2335                     default:
2336                         // Ewww we should NEVER get here, but just incase we do lets
2337                         // fail gracefully
2338                         $usingeditor = false;
2339                         break;
2340                 }
2342                 // If we are actually using the editor, we recalculate the context because some default values
2343                 // were set when calculate_context() was called from the constructor.
2344                 if ($usingeditor) {
2345                     $this->properties->context = $this->calculate_context($this->properties);
2346                     $this->editorcontext = $this->properties->context;
2347                 }
2349                 $editor = $this->properties->description;
2350                 $this->properties->format = $this->properties->description['format'];
2351                 $this->properties->description = $this->properties->description['text'];
2352             }
2354             // Insert the event into the database
2355             $this->properties->id = $DB->insert_record('event', $this->properties);
2357             if ($usingeditor) {
2358                 $this->properties->description = file_save_draft_area_files(
2359                                                 $editor['itemid'],
2360                                                 $this->editorcontext->id,
2361                                                 'calendar',
2362                                                 'event_description',
2363                                                 $this->properties->id,
2364                                                 $this->editoroptions,
2365                                                 $editor['text'],
2366                                                 $this->editoroptions['forcehttps']);
2367                 $DB->set_field('event', 'description', $this->properties->description, array('id'=>$this->properties->id));
2368             }
2370             // Log the event entry.
2371             $eventargs['objectid'] = $this->properties->id;
2372             $eventargs['context'] = $this->properties->context;
2373             $event = \core\event\calendar_event_created::create($eventargs);
2374             $event->trigger();
2376             $repeatedids = array();
2378             if (!empty($this->properties->repeat)) {
2379                 $this->properties->repeatid = $this->properties->id;
2380                 $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id'=>$this->properties->id));
2382                 $eventcopy = clone($this->properties);
2383                 unset($eventcopy->id);
2385                 $timestart = new DateTime('@' . $eventcopy->timestart);
2386                 $timestart->setTimezone(core_date::get_user_timezone_object());
2388                 for($i = 1; $i < $eventcopy->repeats; $i++) {
2390                     $timestart->add(new DateInterval('P7D'));
2391                     $eventcopy->timestart = $timestart->getTimestamp();
2393                     // Get the event id for the log record.
2394                     $eventcopyid = $DB->insert_record('event', $eventcopy);
2396                     // If the context has been set delete all associated files
2397                     if ($usingeditor) {
2398                         $fs = get_file_storage();
2399                         $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id);
2400                         foreach ($files as $file) {
2401                             $fs->create_file_from_storedfile(array('itemid'=>$eventcopyid), $file);
2402                         }
2403                     }
2405                     $repeatedids[] = $eventcopyid;
2407                     // Trigger an event.
2408                     $eventargs['objectid'] = $eventcopyid;
2409                     $eventargs['other']['timestart'] = $eventcopy->timestart;
2410                     $event = \core\event\calendar_event_created::create($eventargs);
2411                     $event->trigger();
2412                 }
2413             }
2415             // Hook for tracking added events
2416             self::calendar_event_hook('add_event', array($this->properties, $repeatedids));
2417             return true;
2418         } else {
2420             if ($checkcapability) {
2421                 if(!calendar_edit_event_allowed($this->properties)) {
2422                     print_error('nopermissiontoupdatecalendar');
2423                 }
2424             }
2426             if ($usingeditor) {
2427                 if ($this->editorcontext !== null) {
2428                     $this->properties->description = file_save_draft_area_files(
2429                                                     $this->properties->description['itemid'],
2430                                                     $this->editorcontext->id,
2431                                                     'calendar',
2432                                                     'event_description',
2433                                                     $this->properties->id,
2434                                                     $this->editoroptions,
2435                                                     $this->properties->description['text'],
2436                                                     $this->editoroptions['forcehttps']);
2437                 } else {
2438                     $this->properties->format = $this->properties->description['format'];
2439                     $this->properties->description = $this->properties->description['text'];
2440                 }
2441             }
2443             $event = $DB->get_record('event', array('id'=>$this->properties->id));
2445             $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
2447             if ($updaterepeated) {
2448                 // Update all
2449                 if ($this->properties->timestart != $event->timestart) {
2450                     $timestartoffset = $this->properties->timestart - $event->timestart;
2451                     $sql = "UPDATE {event}
2452                                SET name = ?,
2453                                    description = ?,
2454                                    timestart = timestart + ?,
2455                                    timeduration = ?,
2456                                    timemodified = ?
2457                              WHERE repeatid = ?";
2458                     $params = array($this->properties->name, $this->properties->description, $timestartoffset, $this->properties->timeduration, time(), $event->repeatid);
2459                 } else {
2460                     $sql = "UPDATE {event} SET name = ?, description = ?, timeduration = ?, timemodified = ? WHERE repeatid = ?";
2461                     $params = array($this->properties->name, $this->properties->description, $this->properties->timeduration, time(), $event->repeatid);
2462                 }
2463                 $DB->execute($sql, $params);
2465                 // Trigger an update event for each of the calendar event.
2466                 $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', 'id,timestart');
2467                 foreach ($events as $event) {
2468                     $eventargs['objectid'] = $event->id;
2469                     $eventargs['other']['timestart'] = $event->timestart;
2470                     $event = \core\event\calendar_event_updated::create($eventargs);
2471                     $event->trigger();
2472                 }
2473             } else {
2474                 $DB->update_record('event', $this->properties);
2475                 $event = calendar_event::load($this->properties->id);
2476                 $this->properties = $event->properties();
2478                 // Trigger an update event.
2479                 $event = \core\event\calendar_event_updated::create($eventargs);
2480                 $event->trigger();
2481             }
2483             // Hook for tracking event updates
2484             self::calendar_event_hook('update_event', array($this->properties, $updaterepeated));
2485             return true;
2486         }
2487     }
2489     /**
2490      * Deletes an event and if selected an repeated events in the same series
2491      *
2492      * This function deletes an event, any associated events if $deleterepeated=true,
2493      * and cleans up any files associated with the events.
2494      *
2495      * @see self::delete()
2496      *
2497      * @param bool $deleterepeated  delete event repeatedly
2498      * @return bool succession of deleting event
2499      */
2500     public function delete($deleterepeated=false) {
2501         global $DB;
2503         // If $this->properties->id is not set then something is wrong
2504         if (empty($this->properties->id)) {
2505             debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER);
2506             return false;
2507         }
2508         $calevent = $DB->get_record('event',  array('id' => $this->properties->id), '*', MUST_EXIST);
2509         // Delete the event
2510         $DB->delete_records('event', array('id'=>$this->properties->id));
2512         // Trigger an event for the delete action.
2513         $eventargs = array(
2514             'context' => $this->properties->context,
2515             'objectid' => $this->properties->id,
2516             'other' => array(
2517                 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
2518                 'timestart' => $this->properties->timestart,
2519                 'name' => $this->properties->name
2520             ));
2521         $event = \core\event\calendar_event_deleted::create($eventargs);
2522         $event->add_record_snapshot('event', $calevent);
2523         $event->trigger();
2525         // If we are deleting parent of a repeated event series, promote the next event in the series as parent
2526         if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) {
2527             $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC", array($this->properties->id), IGNORE_MULTIPLE);
2528             if (!empty($newparent)) {
2529                 $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?", array($newparent, $this->properties->id));
2530                 // Get all records where the repeatid is the same as the event being removed
2531                 $events = $DB->get_records('event', array('repeatid' => $newparent));
2532                 // For each of the returned events trigger the event_update hook and an update event.
2533                 foreach ($events as $event) {
2534                     // Trigger an event for the update.
2535                     $eventargs['objectid'] = $event->id;
2536                     $eventargs['other']['timestart'] = $event->timestart;
2537                     $event = \core\event\calendar_event_updated::create($eventargs);
2538                     $event->trigger();
2540                     self::calendar_event_hook('update_event', array($event, false));
2541                 }
2542             }
2543         }
2545         // If the editor context hasn't already been set then set it now
2546         if ($this->editorcontext === null) {
2547             $this->editorcontext = $this->properties->context;
2548         }
2550         // If the context has been set delete all associated files
2551         if ($this->editorcontext !== null) {
2552             $fs = get_file_storage();
2553             $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id);
2554             foreach ($files as $file) {
2555                 $file->delete();
2556             }
2557         }
2559         // Fire the event deleted hook
2560         self::calendar_event_hook('delete_event', array($this->properties->id, $deleterepeated));
2562         // If we need to delete repeated events then we will fetch them all and delete one by one
2563         if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
2564             // Get all records where the repeatid is the same as the event being removed
2565             $events = $DB->get_records('event', array('repeatid'=>$this->properties->repeatid));
2566             // For each of the returned events populate a calendar_event object and call delete
2567             // make sure the arg passed is false as we are already deleting all repeats
2568             foreach ($events as $event) {
2569                 $event = new calendar_event($event);
2570                 $event->delete(false);
2571             }
2572         }
2574         return true;
2575     }
2577     /**
2578      * Fetch all event properties
2579      *
2580      * This function returns all of the events properties as an object and optionally
2581      * can prepare an editor for the description field at the same time. This is
2582      * designed to work when the properties are going to be used to set the default
2583      * values of a moodle forms form.
2584      *
2585      * @param bool $prepareeditor If set to true a editor is prepared for use with
2586      *              the mforms editor element. (for description)
2587      * @return stdClass Object containing event properties
2588      */
2589     public function properties($prepareeditor=false) {
2590         global $USER, $CFG, $DB;
2592         // First take a copy of the properties. We don't want to actually change the
2593         // properties or we'd forever be converting back and forwards between an
2594         // editor formatted description and not
2595         $properties = clone($this->properties);
2596         // Clean the description here
2597         $properties->description = clean_text($properties->description, $properties->format);
2599         // If set to true we need to prepare the properties for use with an editor
2600         // and prepare the file area
2601         if ($prepareeditor) {
2603             // We may or may not have a property id. If we do then we need to work
2604             // out the context so we can copy the existing files to the draft area
2605             if (!empty($properties->id)) {
2607                 if ($properties->eventtype === 'site') {
2608                     // Site context
2609                     $this->editorcontext = $this->properties->context;
2610                 } else if ($properties->eventtype === 'user') {
2611                     // User context
2612                     $this->editorcontext = $this->properties->context;
2613                 } else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') {
2614                     // First check the course is valid
2615                     $course = $DB->get_record('course', array('id'=>$properties->courseid));
2616                     if (!$course) {
2617                         print_error('invalidcourse');
2618                     }
2619                     // Course context
2620                     $this->editorcontext = $this->properties->context;
2621                     // We have a course and are within the course context so we had
2622                     // better use the courses max bytes value
2623                     $this->editoroptions['maxbytes'] = $course->maxbytes;
2624                 } else {
2625                     // If we get here we have a custom event type as used by some
2626                     // modules. In this case the event will have been added by
2627                     // code and we won't need the editor
2628                     $this->editoroptions['maxbytes'] = 0;
2629                     $this->editoroptions['maxfiles'] = 0;
2630                 }
2632                 if (empty($this->editorcontext) || empty($this->editorcontext->id)) {
2633                     $contextid = false;
2634                 } else {
2635                     // Get the context id that is what we really want
2636                     $contextid = $this->editorcontext->id;
2637                 }
2638             } else {
2640                 // If we get here then this is a new event in which case we don't need a
2641                 // context as there is no existing files to copy to the draft area.
2642                 $contextid = null;
2643             }
2645             // If the contextid === false we don't support files so no preparing
2646             // a draft area
2647             if ($contextid !== false) {
2648                 // Just encase it has already been submitted
2649                 $draftiddescription = file_get_submitted_draft_itemid('description');
2650                 // Prepare the draft area, this copies existing files to the draft area as well
2651                 $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar', 'event_description', $properties->id, $this->editoroptions, $properties->description);
2652             } else {
2653                 $draftiddescription = 0;
2654             }
2656             // Structure the description field as the editor requires
2657             $properties->description = array('text'=>$properties->description, 'format'=>$properties->format, 'itemid'=>$draftiddescription);
2658         }
2660         // Finally return the properties
2661         return $properties;
2662     }
2664     /**
2665      * Toggles the visibility of an event
2666      *
2667      * @param null|bool $force If it is left null the events visibility is flipped,
2668      *                   If it is false the event is made hidden, if it is true it
2669      *                   is made visible.
2670      * @return bool if event is successfully updated, toggle will be visible
2671      */
2672     public function toggle_visibility($force=null) {
2673         global $CFG, $DB;
2675         // Set visible to the default if it is not already set
2676         if (empty($this->properties->visible)) {
2677             $this->properties->visible = 1;
2678         }
2680         if ($force === true || ($force !== false && $this->properties->visible == 0)) {
2681             // Make this event visible
2682             $this->properties->visible = 1;
2683             // Fire the hook
2684             self::calendar_event_hook('show_event', array($this->properties));
2685         } else {
2686             // Make this event hidden
2687             $this->properties->visible = 0;
2688             // Fire the hook
2689             self::calendar_event_hook('hide_event', array($this->properties));
2690         }
2692         // Update the database to reflect this change
2693         return $DB->set_field('event', 'visible', $this->properties->visible, array('id'=>$this->properties->id));
2694     }
2696     /**
2697      * Attempts to call the hook for the specified action should a calendar type
2698      * by set $CFG->calendar, and the appopriate function defined
2699      *
2700      * @param string $action One of `update_event`, `add_event`, `delete_event`, `show_event`, `hide_event`
2701      * @param array $args The args to pass to the hook, usually the event is the first element
2702      * @return bool attempts to call event hook
2703      */
2704     public static function calendar_event_hook($action, array $args) {
2705         global $CFG;
2706         static $extcalendarinc;
2707         if ($extcalendarinc === null) {
2708             if (!empty($CFG->calendar)) {
2709                 if (is_readable($CFG->dirroot .'/calendar/'. $CFG->calendar .'/lib.php')) {
2710                     include_once($CFG->dirroot .'/calendar/'. $CFG->calendar .'/lib.php');
2711                     $extcalendarinc = true;
2712                 } else {
2713                     debugging("Calendar lib file missing or not readable at /calendar/{$CFG->calendar}/lib.php.",
2714                         DEBUG_DEVELOPER);
2715                     $extcalendarinc = false;
2716                 }
2717             } else {
2718                 $extcalendarinc = false;
2719             }
2720         }
2721         if($extcalendarinc === false) {
2722             return false;
2723         }
2724         $hook = $CFG->calendar .'_'.$action;
2725         if (function_exists($hook)) {
2726             call_user_func_array($hook, $args);
2727             return true;
2728         }
2729         return false;
2730     }
2732     /**
2733      * Returns a calendar_event object when provided with an event id
2734      *
2735      * This function makes use of MUST_EXIST, if the event id passed in is invalid
2736      * it will result in an exception being thrown
2737      *
2738      * @param int|object $param event object or event id
2739      * @return calendar_event|false status for loading calendar_event
2740      */
2741     public static function load($param) {
2742         global $DB;
2743         if (is_object($param)) {
2744             $event = new calendar_event($param);
2745         } else {
2746             $event = $DB->get_record('event', array('id'=>(int)$param), '*', MUST_EXIST);
2747             $event = new calendar_event($event);
2748         }
2749         return $event;
2750     }
2752     /**
2753      * Creates a new event and returns a calendar_event object
2754      *
2755      * @param stdClass|array $properties An object containing event properties
2756      * @param bool $checkcapability Check caps or not
2757      * @throws coding_exception
2758      *
2759      * @return calendar_event|bool The event object or false if it failed
2760      */
2761     public static function create($properties, $checkcapability = true) {
2762         if (is_array($properties)) {
2763             $properties = (object)$properties;
2764         }
2765         if (!is_object($properties)) {
2766             throw new coding_exception('When creating an event properties should be either an object or an assoc array');
2767         }
2768         $event = new calendar_event($properties);
2769         if ($event->update($properties, $checkcapability)) {
2770             return $event;
2771         } else {
2772             return false;
2773         }
2774     }
2776     /**
2777      * Format the text using the external API.
2778      * This function should we used when text formatting is required in external functions.
2779      *
2780      * @return array an array containing the text formatted and the text format
2781      */
2782     public function format_external_text() {
2784         if ($this->editorcontext === null) {
2785             // Switch on the event type to decide upon the appropriate context to use for this event.
2786             $this->editorcontext = $this->properties->context;
2788             if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
2789                     && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
2790                 // We don't have a context here, do a normal format_text.
2791                 return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
2792             }
2793         }
2795         // Work out the item id for the editor, if this is a repeated event then the files will be associated with the original.
2796         if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
2797             $itemid = $this->properties->repeatid;
2798         } else {
2799             $itemid = $this->properties->id;
2800         }
2802         return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id,
2803                                     'calendar', 'event_description', $itemid);
2804     }
2807 /**
2808  * Calendar information class
2809  *
2810  * This class is used simply to organise the information pertaining to a calendar
2811  * and is used primarily to make information easily available.
2812  *
2813  * @package core_calendar
2814  * @category calendar
2815  * @copyright 2010 Sam Hemelryk
2816  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2817  */
2818 class calendar_information {
2820     /**
2821      * @var int The timestamp
2822      *
2823      * Rather than setting the day, month and year we will set a timestamp which will be able
2824      * to be used by multiple calendars.
2825      */
2826     public $time;
2828     /** @var int A course id */
2829     public $courseid = null;
2831     /** @var array An array of courses */
2832     public $courses = array();
2834     /** @var array An array of groups */
2835     public $groups = array();
2837     /** @var array An array of users */
2838     public $users = array();
2840     /**
2841      * Creates a new instance
2842      *
2843      * @param int $day the number of the day
2844      * @param int $month the number of the month
2845      * @param int $year the number of the year
2846      * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
2847      *     and $calyear to support multiple calendars
2848      */
2849     public function __construct($day = 0, $month = 0, $year = 0, $time = 0) {
2850         // If a day, month and year were passed then convert it to a timestamp. If these were passed
2851         // then we can assume the day, month and year are passed as Gregorian, as no where in core
2852         // should we be passing these values rather than the time. This is done for BC.
2853         if (!empty($day) || !empty($month) || !empty($year)) {
2854             $date = usergetdate(time());
2855             if (empty($day)) {
2856                 $day = $date['mday'];
2857             }
2858             if (empty($month)) {
2859                 $month = $date['mon'];
2860             }
2861             if (empty($year)) {
2862                 $year =  $date['year'];
2863             }
2864             if (checkdate($month, $day, $year)) {
2865                 $this->time = make_timestamp($year, $month, $day);
2866             } else {
2867                 $this->time = time();
2868             }
2869         } else if (!empty($time)) {
2870             $this->time = $time;
2871         } else {
2872             $this->time = time();
2873         }
2874     }
2876     /**
2877      * Initialize calendar information
2878      *
2879      * @param stdClass $course object
2880      * @param array $coursestoload An array of courses [$course->id => $course]
2881      * @param bool $ignorefilters options to use filter
2882      */
2883     public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) {
2884         $this->courseid = $course->id;
2885         $this->course = $course;
2886         list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters);
2887         $this->courses = $courses;
2888         $this->groups = $group;
2889         $this->users = $user;
2890     }
2892     /**
2893      * Ensures the date for the calendar is correct and either sets it to now
2894      * or throws a moodle_exception if not
2895      *
2896      * @param bool $defaultonow use current time
2897      * @throws moodle_exception
2898      * @return bool validation of checkdate
2899      */
2900     public function checkdate($defaultonow = true) {
2901         if (!checkdate($this->month, $this->day, $this->year)) {
2902             if ($defaultonow) {
2903                 $now = usergetdate(time());
2904                 $this->day = intval($now['mday']);
2905                 $this->month = intval($now['mon']);
2906                 $this->year = intval($now['year']);
2907                 return true;
2908             } else {
2909                 throw new moodle_exception('invaliddate');
2910             }
2911         }
2912         return true;
2913     }
2915     /**
2916      * Gets todays timestamp for the calendar
2917      *
2918      * @return int today timestamp
2919      */
2920     public function timestamp_today() {
2921         return $this->time;
2922     }
2923     /**
2924      * Gets tomorrows timestamp for the calendar
2925      *
2926      * @return int tomorrow timestamp
2927      */
2928     public function timestamp_tomorrow() {
2929         return strtotime('+1 day', $this->time);
2930     }
2931     /**
2932      * Adds the pretend blocks for the calendar
2933      *
2934      * @param core_calendar_renderer $renderer
2935      * @param bool $showfilters display filters, false is set as default
2936      * @param string|null $view preference view options (eg: day, month, upcoming)
2937      */
2938     public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) {
2939         if ($showfilters) {
2940             $filters = new block_contents();
2941             $filters->content = $renderer->fake_block_filters($this->courseid, 0, 0, 0, $view, $this->courses);
2942             $filters->footer = '';
2943             $filters->title = get_string('eventskey', 'calendar');
2944             $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT);
2945         }
2946         $block = new block_contents;
2947         $block->content = $renderer->fake_block_threemonths($this);
2948         $block->footer = '';
2949         $block->title = get_string('monthlyview', 'calendar');
2950         $renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT);
2951     }
2954 /**
2955  * Returns option list for the poll interval setting.
2956  *
2957  * @return array An array of poll interval options. Interval => description.
2958  */
2959 function calendar_get_pollinterval_choices() {
2960     return array(
2961         '0' => new lang_string('never', 'calendar'),
2962         HOURSECS => new lang_string('hourly', 'calendar'),
2963         DAYSECS => new lang_string('daily', 'calendar'),
2964         WEEKSECS => new lang_string('weekly', 'calendar'),
2965         '2628000' => new lang_string('monthly', 'calendar'),
2966         YEARSECS => new lang_string('annually', 'calendar')
2967     );
2970 /**
2971  * Returns option list of available options for the calendar event type, given the current user and course.
2972  *
2973  * @param int $courseid The id of the course
2974  * @return array An array containing the event types the user can create.
2975  */
2976 function calendar_get_eventtype_choices($courseid) {
2977     $choices = array();
2978     $allowed = new stdClass;
2979     calendar_get_allowed_types($allowed, $courseid);
2981     if ($allowed->user) {
2982         $choices['user'] = get_string('userevents', 'calendar');
2983     }
2984     if ($allowed->site) {
2985         $choices['site'] = get_string('siteevents', 'calendar');
2986     }
2987     if (!empty($allowed->courses)) {
2988         $choices['course'] = get_string('courseevents', 'calendar');
2989     }
2990     if (!empty($allowed->groups) and is_array($allowed->groups)) {
2991         $choices['group'] = get_string('group');
2992     }
2994     return array($choices, $allowed->groups);
2997 /**
2998  * Add an iCalendar subscription to the database.
2999  *
3000  * @param stdClass $sub The subscription object (e.g. from the form)
3001  * @return int The insert ID, if any.
3002  */
3003 function calendar_add_subscription($sub) {
3004     global $DB, $USER, $SITE;
3006     if ($sub->eventtype === 'site') {
3007         $sub->courseid = $SITE->id;
3008     } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
3009         $sub->courseid = $sub->course;
3010     } else {
3011         // User events.
3012         $sub->courseid = 0;
3013     }
3014     $sub->userid = $USER->id;
3016     // File subscriptions never update.
3017     if (empty($sub->url)) {
3018         $sub->pollinterval = 0;
3019     }
3021     if (!empty($sub->name)) {
3022         if (empty($sub->id)) {
3023             $id = $DB->insert_record('event_subscriptions', $sub);
3024             // we cannot cache the data here because $sub is not complete.
3025             $sub->id = $id;
3026             // Trigger event, calendar subscription added.
3027             $eventparams = array('objectid' => $sub->id,
3028                 'context' => calendar_get_calendar_context($sub),
3029                 'other' => array('eventtype' => $sub->eventtype, 'courseid' => $sub->courseid)
3030             );
3031             $event = \core\event\calendar_subscription_created::create($eventparams);
3032             $event->trigger();
3033             return $id;
3034         } else {
3035             // Why are we doing an update here?
3036             calendar_update_subscription($sub);
3037             return $sub->id;
3038         }
3039     } else {
3040         print_error('errorbadsubscription', 'importcalendar');
3041     }
3044 /**
3045  * Add an iCalendar event to the Moodle calendar.
3046  *
3047  * @param stdClass $event The RFC-2445 iCalendar event
3048  * @param int $courseid The course ID
3049  * @param int $subscriptionid The iCalendar subscription ID
3050  * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
3051  * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
3052  * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated,  CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
3053  */
3054 function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone='UTC') {
3055     global $DB;
3057     // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
3058     if (empty($event->properties['SUMMARY'])) {
3059         return 0;
3060     }
3062     $name = $event->properties['SUMMARY'][0]->value;
3063     $name = str_replace('\n', '<br />', $name);
3064     $name = str_replace('\\', '', $name);
3065     $name = preg_replace('/\s+/u', ' ', $name);
3067     $eventrecord = new stdClass;
3068     $eventrecord->name = clean_param($name, PARAM_NOTAGS);
3070     if (empty($event->properties['DESCRIPTION'][0]->value)) {
3071         $description = '';
3072     } else {
3073         $description = $event->properties['DESCRIPTION'][0]->value;
3074         $description = clean_param($description, PARAM_NOTAGS);
3075         $description = str_replace('\n', '<br />', $description);
3076         $description = str_replace('\\', '', $description);
3077         $description = preg_replace('/\s+/u', ' ', $description);
3078     }
3079     $eventrecord->description = $description;
3081     // Probably a repeating event with RRULE etc. TODO: skip for now.
3082     if (empty($event->properties['DTSTART'][0]->value)) {
3083         return 0;
3084     }
3086     $tz = isset($event->properties['DTSTART'][0]->parameters['TZID']) ? $event->properties['DTSTART'][0]->parameters['TZID'] :
3087             $timezone;
3088     $tz = core_date::normalise_timezone($tz);
3089     $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
3090     if (empty($event->properties['DTEND'])) {
3091         $eventrecord->timeduration = 0; // no duration if no end time specified
3092     } else {
3093         $endtz = isset($event->properties['DTEND'][0]->parameters['TZID']) ? $event->properties['DTEND'][0]->parameters['TZID'] :
3094                 $timezone;
3095         $endtz = core_date::normalise_timezone($endtz);
3096         $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart;
3097     }
3099     // Check to see if it should be treated as an all day event.
3100     if ($eventrecord->timeduration == DAYSECS) {
3101         // Check to see if the event started at Midnight on the imported calendar.
3102         date_default_timezone_set($timezone);
3103         if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
3104             // This event should be an all day event.
3105             $eventrecord->timeduration = 0;
3106         }
3107         core_date::set_default_server_timezone();
3108     }
3110     $eventrecord->uuid = $event->properties['UID'][0]->value;
3111     $eventrecord->timemodified = time();
3113     // Add the iCal subscription details if required.
3114     // We should never do anything with an event without a subscription reference.
3115     $sub = calendar_get_subscription($subscriptionid);
3116     $eventrecord->subscriptionid = $subscriptionid;
3117     $eventrecord->userid = $sub->userid;
3118     $eventrecord->groupid = $sub->groupid;
3119     $eventrecord->courseid = $sub->courseid;
3120     $eventrecord->eventtype = $sub->eventtype;
3122     if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid, 'subscriptionid' => $eventrecord->subscriptionid))) {
3123         $eventrecord->id = $updaterecord->id;
3124         $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update.
3125     } else {
3126         $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert.
3127     }
3128     if ($createdevent = calendar_event::create($eventrecord, false)) {
3129         if (!empty($event->properties['RRULE'])) {
3130             // Repeating events.
3131             date_default_timezone_set($tz); // Change time zone to parse all events.
3132             $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value);
3133             $rrule->parse_rrule();
3134             $rrule->create_events($createdevent);
3135             core_date::set_default_server_timezone(); // Change time zone back to what it was.
3136         }
3137         return $return;
3138     } else {
3139         return 0;
3140     }
3143 /**
3144  * Update a subscription from the form data in one of the rows in the existing subscriptions table.
3145  *
3146  * @param int $subscriptionid The ID of the subscription we are acting upon.
3147  * @param int $pollinterval The poll interval to use.
3148  * @param int $action The action to be performed. One of update or remove.
3149  * @throws dml_exception if invalid subscriptionid is provided
3150  * @return string A log of the import progress, including errors
3151  */
3152 function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
3154     // Fetch the subscription from the database making sure it exists.
3155     $sub = calendar_get_subscription($subscriptionid);
3157     // Update or remove the subscription, based on action.
3158     switch ($action) {
3159         case CALENDAR_SUBSCRIPTION_UPDATE:
3160             // Skip updating file subscriptions.
3161             if (empty($sub->url)) {
3162                 break;
3163             }
3164             $sub->pollinterval = $pollinterval;
3165             calendar_update_subscription($sub);
3167             // Update the events.
3168             return "<p>".get_string('subscriptionupdated', 'calendar', $sub->name)."</p>" . calendar_update_subscription_events($subscriptionid);
3170         case CALENDAR_SUBSCRIPTION_REMOVE:
3171             calendar_delete_subscription($subscriptionid);
3172             return get_string('subscriptionremoved', 'calendar', $sub->name);
3173             break;
3175         default:
3176             break;
3177     }
3178     return '';
3181 /**
3182  * Delete subscription and all related events.
3183  *
3184  * @param int|stdClass $subscription subscription or it's id, which needs to be deleted.
3185  */
3186 function calendar_delete_subscription($subscription) {
3187     global $DB;
3189     if (!is_object($subscription)) {
3190         $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST);
3191     }
3192     // Delete subscription and related events.
3193     $DB->delete_records('event', array('subscriptionid' => $subscription->id));
3194     $DB->delete_records('event_subscriptions', array('id' => $subscription->id));
3195     cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id));
3197     // Trigger event, calendar subscription deleted.
3198     $eventparams = array('objectid' => $subscription->id,
3199         'context' => calendar_get_calendar_context($subscription),
3200         'other' => array('courseid' => $subscription->courseid)
3201     );
3202     $event = \core\event\calendar_subscription_deleted::create($eventparams);
3203     $event->trigger();
3205 /**
3206  * From a URL, fetch the calendar and return an iCalendar object.
3207  *
3208  * @param string $url The iCalendar URL
3209  * @return stdClass The iCalendar object
3210  */
3211 function calendar_get_icalendar($url) {
3212     global $CFG;
3214     require_once($CFG->libdir.'/filelib.php');
3216     $curl = new curl();
3217     $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5));
3218     $calendar = $curl->get($url);
3219     // Http code validation should actually be the job of curl class.
3220     if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) {
3221         throw new moodle_exception('errorinvalidicalurl', 'calendar');
3222     }
3224     $ical = new iCalendar();
3225     $ical->unserialize($calendar);
3226     return $ical;
3229 /**
3230  * Import events from an iCalendar object into a course calendar.
3231  *
3232  * @param stdClass $ical The iCalendar object.
3233  * @param int $courseid The course ID for the calendar.
3234  * @param int $subscriptionid The subscription ID.
3235  * @return string A log of the import progress, including errors.
3236  */
3237 function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = null) {
3238     global $DB;
3239     $return = '';
3240     $eventcount = 0;
3241     $updatecount = 0;
3243     // Large calendars take a while...
3244     if (!CLI_SCRIPT) {
3245         core_php_time_limit::raise(300);
3246     }
3248     // Mark all events in a subscription with a zero timestamp.
3249     if (!empty($subscriptionid)) {
3250         $sql = "UPDATE {event} SET timemodified = :time WHERE subscriptionid = :id";
3251         $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid));
3252     }
3253     // Grab the timezone from the iCalendar file to be used later.
3254     if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) {
3255         $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value;
3256     } else {
3257         $timezone = 'UTC';
3258     }
3259     foreach ($ical->components['VEVENT'] as $event) {
3260         $res = calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone);
3261         switch ($res) {
3262           case CALENDAR_IMPORT_EVENT_UPDATED:
3263             $updatecount++;
3264             break;
3265           case CALENDAR_IMPORT_EVENT_INSERTED:
3266             $eventcount++;
3267             break;
3268           case 0:
3269             $return .= '<p>'.get_string('erroraddingevent', 'calendar').': '.(empty($event->properties['SUMMARY'])?'('.get_string('notitle', 'calendar').')':$event->properties['SUMMARY'][0]->value)." </p>\n";
3270             break;
3271         }
3272     }
3273     $return .= "<p> ".get_string('eventsimported', 'calendar', $eventcount)."</p>";
3274     $return .= "<p> ".get_string('eventsupdated', 'calendar', $updatecount)."</p>";
3276     // Delete remaining zero-marked events since they're not in remote calendar.
3277     if (!empty($subscriptionid)) {
3278         $deletecount = $DB->count_records('event', array('timemodified' => 0, 'subscriptionid' => $subscriptionid));
3279         if (!empty($deletecount)) {
3280             $sql = "DELETE FROM {event} WHERE timemodified = :time AND subscriptionid = :id";
3281             $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid));
3282             $return .= "<p> ".get_string('eventsdeleted', 'calendar').": {$deletecount} </p>\n";
3283         }
3284     }
3286     return $return;
3289 /**
3290  * Fetch a calendar subscription and update the events in the calendar.
3291  *
3292  * @param int $subscriptionid The course ID for the calendar.
3293  * @return string A log of the import progress, including errors.
3294  */
3295 function calendar_update_subscription_events($subscriptionid) {
3296     global $DB;
3298     $sub = calendar_get_subscription($subscriptionid);
3299     // Don't update a file subscription. TODO: Update from a new uploaded file.
3300     if (empty($sub->url)) {
3301         return 'File subscription not updated.';
3302     }
3303     $ical = calendar_get_icalendar($sub->url);
3304     $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid);
3305     $sub->lastupdated = time();
3306     calendar_update_subscription($sub);
3307     return $return;
3310 /**
3311  * Update a calendar subscription. Also updates the associated cache.
3312  *
3313  * @param stdClass|array $subscription Subscription record.
3314  * @throws coding_exception If something goes wrong
3315  * @since Moodle 2.5
3316  */
3317 function calendar_update_subscription($subscription) {
3318     global $DB;
3320     if (is_array($subscription)) {
3321         $subscription = (object)$subscription;
3322     }
3323     if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) {
3324         throw new coding_exception('Cannot update a subscription without a valid id');
3325     }
3327     $DB->update_record('event_subscriptions', $subscription);
3328     // Update cache.
3329     $cache = cache::make('core', 'calendar_subscriptions');
3330     $cache->set($subscription->id, $subscription);
3331     // Trigger event, calendar subscription updated.
3332     $eventparams = array('userid' => $subscription->userid,
3333         'objectid' => $subscription->id,
3334         'context' => calendar_get_calendar_context($subscription),
3335         'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid)
3336         );
3337     $event = \core\event\calendar_subscription_updated::create($eventparams);
3338     $event->trigger();
3341 /**
3342  * Checks to see if the user can edit a given subscription feed.
3343  *
3344  * @param mixed $subscriptionorid Subscription object or id
3345  * @return bool true if current user can edit the subscription else false
3346  */
3347 function calendar_can_edit_subscription($subscriptionorid) {
3348     global $DB;
3350     if (is_array($subscriptionorid)) {
3351         $subscription = (object)$subscriptionorid;
3352     } else if (is_object($subscriptionorid)) {
3353         $subscription = $subscriptionorid;
3354     } else {
3355         $subscription = calendar_get_subscription($subscriptionorid);
3356     }
3357     $allowed = new stdClass;
3358     $courseid = $subscription->courseid;
3359     $groupid = $subscription->groupid;
3360     calendar_get_allowed_types($allowed, $courseid);
3361     switch ($subscription->eventtype) {
3362         case 'user':
3363             return $allowed->user;
3364         case 'course':
3365             if (isset($allowed->courses[$courseid])) {
3366                 return $allowed->courses[$courseid];
3367             } else {
3368                 return false;
3369             }
3370         case 'site':
3371             return $allowed->site;
3372         case 'group':
3373             if (isset($allowed->groups[$groupid])) {
3374                 return $allowed->groups[$groupid];
3375             } else {
3376                 return false;
3377             }
3378         default:
3379             return false;
3380     }
3383 /**
3384  * Update calendar subscriptions.
3385  *
3386  * @return bool
3387  */
3388 function calendar_cron() {
3389     global $CFG, $DB;
3391     // In order to execute this we need bennu.
3392     require_once($CFG->libdir.'/bennu/bennu.inc.php');
3394     mtrace('Updating calendar subscriptions:');
3395     cron_trace_time_and_memory();
3397     $time = time();
3398     $subscriptions = $DB->get_records_sql('SELECT * FROM {event_subscriptions} WHERE pollinterval > 0 AND lastupdated + pollinterval < ?', array($time));
3399     foreach ($subscriptions as $sub) {
3400         mtrace("Updating calendar subscription {$sub->name} in course {$sub->courseid}");
3401         try {
3402             $log = calendar_update_subscription_events($sub->id);
3403             mtrace(trim(strip_tags($log)));
3404         } catch (moodle_exception $ex) {
3405             mtrace('Error updating calendar subscription: ' . $ex->getMessage());
3406         }
3407     }
3409     mtrace('Finished updating calendar subscriptions.');
3411     return true;
3414 /**
3415  * Helper function to determine the context of a calendar subscription.
3416  * Subscriptions can be created in two contexts COURSE, or USER.
3417  *
3418  * @param stdClass $subscription
3419  * @return context instance
3420  */
3421 function calendar_get_calendar_context($subscription) {
3423     // Determine context based on calendar type.
3424     if ($subscription->eventtype === 'site') {
3425         $context = context_course::instance(SITEID);
3426     } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
3427         $context = context_course::instance($subscription->courseid);
3428     } else {
3429         $context = context_user::instance($subscription->userid);
3430     }
3431     return $context;