Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

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