Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
 * Library of useful functions
19
 *
20
 * @copyright 1999 Martin Dougiamas  http://dougiamas.com
21
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 * @package core_course
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die;
26
 
27
use core\di;
28
use core\hook;
29
use core_course\external\course_summary_exporter;
30
use core_courseformat\base as course_format;
31
use core_courseformat\formatactions;
32
use core\output\local\action_menu\subpanel as action_menu_subpanel;
33
 
34
require_once($CFG->libdir.'/completionlib.php');
35
require_once($CFG->libdir.'/filelib.php');
36
require_once($CFG->libdir.'/datalib.php');
37
require_once($CFG->dirroot.'/course/format/lib.php');
38
 
39
define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // Records.
40
define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds.
41
 
42
/**
43
 * Number of courses to display when summaries are included.
44
 * @var int
45
 * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead.
46
 */
47
define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
48
 
49
// Max courses in log dropdown before switching to optional.
50
define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
51
// Max users in log dropdown before switching to optional.
52
define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
53
define('FRONTPAGENEWS', '0');
54
define('FRONTPAGECATEGORYNAMES', '2');
55
define('FRONTPAGECATEGORYCOMBO', '4');
56
define('FRONTPAGEENROLLEDCOURSELIST', '5');
57
define('FRONTPAGEALLCOURSELIST', '6');
58
define('FRONTPAGECOURSESEARCH', '7');
59
// Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
60
define('EXCELROWS', 65535);
61
define('FIRSTUSEDEXCELROW', 3);
62
 
63
define('MOD_CLASS_ACTIVITY', 0);
64
define('MOD_CLASS_RESOURCE', 1);
65
 
66
define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
67
define('COURSE_TIMELINE_ALL', 'all');
68
define('COURSE_TIMELINE_PAST', 'past');
69
define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
70
define('COURSE_TIMELINE_FUTURE', 'future');
71
define('COURSE_TIMELINE_SEARCH', 'search');
72
define('COURSE_FAVOURITES', 'favourites');
73
define('COURSE_TIMELINE_HIDDEN', 'hidden');
74
define('COURSE_CUSTOMFIELD', 'customfield');
75
define('COURSE_DB_QUERY_LIMIT', 1000);
76
/** Searching for all courses that have no value for the specified custom field. */
77
define('COURSE_CUSTOMFIELD_EMPTY', -1);
78
 
79
// Course activity chooser footer default display option.
80
define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
81
 
82
// Download course content options.
83
define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0);
84
define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1);
85
define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2);
86
 
87
function make_log_url($module, $url) {
88
    switch ($module) {
89
        case 'course':
90
            if (strpos($url, 'report/') === 0) {
91
                // there is only one report type, course reports are deprecated
92
                $url = "/$url";
93
                break;
94
            }
95
        case 'file':
96
        case 'login':
97
        case 'lib':
98
        case 'admin':
99
        case 'category':
100
        case 'mnet course':
101
            if (strpos($url, '../') === 0) {
102
                $url = ltrim($url, '.');
103
            } else {
104
                $url = "/course/$url";
105
            }
106
            break;
107
        case 'calendar':
108
            $url = "/calendar/$url";
109
            break;
110
        case 'user':
111
        case 'blog':
112
            $url = "/$module/$url";
113
            break;
114
        case 'upload':
115
            $url = $url;
116
            break;
117
        case 'coursetags':
118
            $url = '/'.$url;
119
            break;
120
        case 'library':
121
        case '':
122
            $url = '/';
123
            break;
124
        case 'message':
125
            $url = "/message/$url";
126
            break;
127
        case 'notes':
128
            $url = "/notes/$url";
129
            break;
130
        case 'tag':
131
            $url = "/tag/$url";
132
            break;
133
        case 'role':
134
            $url = '/'.$url;
135
            break;
136
        case 'grade':
137
            $url = "/grade/$url";
138
            break;
139
        default:
140
            $url = "/mod/$module/$url";
141
            break;
142
    }
143
 
144
    //now let's sanitise urls - there might be some ugly nasties:-(
145
    $parts = explode('?', $url);
146
    $script = array_shift($parts);
147
    if (strpos($script, 'http') === 0) {
148
        $script = clean_param($script, PARAM_URL);
149
    } else {
150
        $script = clean_param($script, PARAM_PATH);
151
    }
152
 
153
    $query = '';
154
    if ($parts) {
155
        $query = implode('', $parts);
156
        $query = str_replace('&amp;', '&', $query); // both & and &amp; are stored in db :-|
157
        $parts = explode('&', $query);
158
        $eq = urlencode('=');
159
        foreach ($parts as $key=>$part) {
160
            $part = urlencode(urldecode($part));
161
            $part = str_replace($eq, '=', $part);
162
            $parts[$key] = $part;
163
        }
164
        $query = '?'.implode('&amp;', $parts);
165
    }
166
 
167
    return $script.$query;
168
}
169
 
170
 
171
function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='',
172
                   $modname="", $modid=0, $modaction="", $groupid=0) {
173
    global $CFG, $DB;
174
 
175
    // It is assumed that $date is the GMT time of midnight for that day,
176
    // and so the next 86400 seconds worth of logs are printed.
177
 
178
    /// Setup for group handling.
179
 
180
    // TODO: I don't understand group/context/etc. enough to be able to do
181
    // something interesting with it here
182
    // What is the context of a remote course?
183
 
184
    /// If the group mode is separate, and this user does not have editing privileges,
185
    /// then only the user's group can be viewed.
186
    //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
187
    //    $groupid = get_current_group($course->id);
188
    //}
189
    /// If this course doesn't have groups, no groupid can be specified.
190
    //else if (!$course->groupmode) {
191
    //    $groupid = 0;
192
    //}
193
 
194
    $groupid = 0;
195
 
196
    $joins = array();
197
    $where = '';
198
 
199
    $qry = "SELECT l.*, u.firstname, u.lastname, u.picture
200
              FROM {mnet_log} l
201
               LEFT JOIN {user} u ON l.userid = u.id
202
              WHERE ";
203
    $params = array();
204
 
205
    $where .= "l.hostid = :hostid";
206
    $params['hostid'] = $hostid;
207
 
208
    // TODO: Is 1 really a magic number referring to the sitename?
209
    if ($course != SITEID || $modid != 0) {
210
        $where .= " AND l.course=:courseid";
211
        $params['courseid'] = $course;
212
    }
213
 
214
    if ($modname) {
215
        $where .= " AND l.module = :modname";
216
        $params['modname'] = $modname;
217
    }
218
 
219
    if ('site_errors' === $modid) {
220
        $where .= " AND ( l.action='error' OR l.action='infected' )";
221
    } else if ($modid) {
222
        //TODO: This assumes that modids are the same across sites... probably
223
        //not true
224
        $where .= " AND l.cmid = :modid";
225
        $params['modid'] = $modid;
226
    }
227
 
228
    if ($modaction) {
229
        $firstletter = substr($modaction, 0, 1);
230
        if ($firstletter == '-') {
231
            $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true);
232
            $params['modaction'] = '%'.substr($modaction, 1).'%';
233
        } else {
234
            $where .= " AND ".$DB->sql_like('l.action', ':modaction', false);
235
            $params['modaction'] = '%'.$modaction.'%';
236
        }
237
    }
238
 
239
    if ($user) {
240
        $where .= " AND l.userid = :user";
241
        $params['user'] = $user;
242
    }
243
 
244
    if ($date) {
245
        $enddate = $date + 86400;
246
        $where .= " AND l.time > :date AND l.time < :enddate";
247
        $params['date'] = $date;
248
        $params['enddate'] = $enddate;
249
    }
250
 
251
    $result = array();
252
    $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params);
253
    if(!empty($result['totalcount'])) {
254
        $where .= " ORDER BY $order";
255
        $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum);
256
    } else {
257
        $result['logs'] = array();
258
    }
259
    return $result;
260
}
261
 
262
/**
263
 * Checks the integrity of the course data.
264
 *
265
 * In summary - compares course_sections.sequence and course_modules.section.
266
 *
267
 * More detailed, checks that:
268
 * - course_sections.sequence contains each module id not more than once in the course
269
 * - for each moduleid from course_sections.sequence the field course_modules.section
270
 *   refers to the same section id (this means course_sections.sequence is more
271
 *   important if they are different)
272
 * - ($fullcheck only) each module in the course is present in one of
273
 *   course_sections.sequence
274
 * - ($fullcheck only) removes non-existing course modules from section sequences
275
 *
276
 * If there are any mismatches, the changes are made and records are updated in DB.
277
 *
278
 * Course cache is NOT rebuilt if there are any errors!
279
 *
280
 * This function is used each time when course cache is being rebuilt with $fullcheck = false
281
 * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true
282
 *
283
 * @param int $courseid id of the course
284
 * @param array $rawmods result of funciton {@link get_course_mods()} - containst
285
 *     the list of enabled course modules in the course. Retrieved from DB if not specified.
286
 *     Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway.
287
 * @param array $sections records from course_sections table for this course.
288
 *     Retrieved from DB if not specified
289
 * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing
290
 *     course modules from sequences. Only to be used in site maintenance mode when we are
291
 *     sure that another user is not in the middle of the process of moving/removing a module.
292
 * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages.
293
 * @return array|false array of messages with found problems. Empty output means everything is ok
294
 */
295
function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) {
296
    global $DB;
297
    $messages = array();
298
    if ($sections === null) {
299
        $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence');
300
    }
301
    if ($fullcheck) {
302
        // Retrieve all records from course_modules regardless of module type visibility.
303
        $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section');
304
    }
305
    if ($rawmods === null) {
306
        $rawmods = get_course_mods($courseid);
307
    }
308
    if (!$fullcheck && (empty($sections) || empty($rawmods))) {
309
        // If either of the arrays is empty, no modules are displayed anyway.
310
        return true;
311
    }
312
    $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. ';
313
 
314
    // First make sure that each module id appears in section sequences only once.
315
    // If it appears in several section sequences the last section wins.
316
    // If it appears twice in one section sequence, the first occurence wins.
317
    $modsection = array();
318
    foreach ($sections as $sectionid => $section) {
319
        $sections[$sectionid]->newsequence = $section->sequence;
320
        if (!empty($section->sequence)) {
321
            $sequence = explode(",", $section->sequence);
322
            $sequenceunique = array_unique($sequence);
323
            if (count($sequenceunique) != count($sequence)) {
324
                // Some course module id appears in this section sequence more than once.
325
                ksort($sequenceunique); // Preserve initial order of modules.
326
                $sequence = array_values($sequenceunique);
327
                $sections[$sectionid]->newsequence = join(',', $sequence);
328
                $messages[] = $debuggingprefix.'Sequence for course section ['.
329
                        $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"';
330
            }
331
            foreach ($sequence as $cmid) {
332
                if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) {
333
                    // Some course module id appears to be in more than one section's sequences.
334
                    $wrongsectionid = $modsection[$cmid];
335
                    $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ',');
336
                    $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['.
337
                            $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']';
338
                }
339
                $modsection[$cmid] = $sectionid;
340
            }
341
        }
342
    }
343
 
344
    // Add orphaned modules to their sections if they exist or to section 0 otherwise.
345
    if ($fullcheck) {
346
        foreach ($rawmods as $cmid => $mod) {
347
            if (!isset($modsection[$cmid])) {
348
                // This is a module that is not mentioned in course_section.sequence at all.
349
                // Add it to the section $mod->section or to the last available section.
350
                if ($mod->section && isset($sections[$mod->section])) {
351
                    $modsection[$cmid] = $mod->section;
352
                } else {
353
                    $firstsection = reset($sections);
354
                    $modsection[$cmid] = $firstsection->id;
355
                }
356
                $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ',');
357
                $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['.
358
                        $modsection[$cmid].']';
359
            }
360
        }
361
        foreach ($modsection as $cmid => $sectionid) {
362
            if (!isset($rawmods[$cmid])) {
363
                // Section $sectionid refers to module id that does not exist.
364
                $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ',');
365
                $messages[] = $debuggingprefix.'Course module ['.$cmid.
366
                        '] does not exist but is present in the sequence of section ['.$sectionid.']';
367
            }
368
        }
369
    }
370
 
371
    // Update changed sections.
372
    if (!$checkonly && !empty($messages)) {
373
        foreach ($sections as $sectionid => $section) {
374
            if ($section->newsequence !== $section->sequence) {
375
                $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence));
376
            }
377
        }
378
    }
379
 
380
    // Now make sure that all modules point to the correct sections.
381
    foreach ($rawmods as $cmid => $mod) {
382
        if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) {
383
            if (!$checkonly) {
384
                $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid]));
385
            }
386
            $messages[] = $debuggingprefix.'Course module ['.$cmid.
387
                    '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']';
388
        }
389
    }
390
 
391
    return $messages;
392
}
393
 
394
/**
395
 * Returns an array where the key is the module name (component name without 'mod_')
396
 * and the value is a lang_string object with a human-readable string.
397
 *
398
 * @param bool $plural If true, the function returns the plural forms of the names.
399
 * @param bool $resetcache If true, the static cache will be reset
400
 * @return lang_string[] Localised human-readable names of all used modules.
401
 */
402
function get_module_types_names($plural = false, $resetcache = false) {
403
    static $modnames = null;
404
    global $DB, $CFG;
405
    if ($modnames === null || empty($modnames[0]) || $resetcache) {
406
        $modnames = array(0 => array(), 1 => array());
407
        if ($allmods = $DB->get_records("modules")) {
408
            foreach ($allmods as $mod) {
409
                if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) {
410
                    $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true);
411
                    $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true);
412
                }
413
            }
414
        }
415
    }
416
    return $modnames[(int)$plural];
417
}
418
 
419
/**
420
 * Set highlighted section. Only one section can be highlighted at the time.
421
 *
422
 * @param int $courseid course id
423
 * @param int $marker highlight section with this number, 0 means remove higlightin
424
 * @return void
425
 */
426
function course_set_marker($courseid, $marker) {
427
    global $DB, $COURSE;
428
    $DB->set_field("course", "marker", $marker, array('id' => $courseid));
429
    if ($COURSE && $COURSE->id == $courseid) {
430
        $COURSE->marker = $marker;
431
    }
432
    core_courseformat\base::reset_course_cache($courseid);
433
    course_modinfo::clear_instance_cache($courseid);
434
}
435
 
436
/**
437
 * For a given course section, marks it visible or hidden,
438
 * and does the same for every activity in that section
439
 *
440
 * @param int $courseid course id
441
 * @param int $sectionnumber The section number to adjust
442
 * @param int $visibility The new visibility
443
 * @return array A list of resources which were hidden in the section
444
 */
445
function set_section_visible($courseid, $sectionnumber, $visibility) {
446
    global $DB;
447
 
448
    $resourcestotoggle = array();
449
    if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
450
        course_update_section($courseid, $section, array('visible' => $visibility));
451
 
452
        // Determine which modules are visible for AJAX update
453
        $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
454
        if (!empty($modules)) {
455
            list($insql, $params) = $DB->get_in_or_equal($modules);
456
            $select = 'id ' . $insql . ' AND visible = ?';
457
            array_push($params, $visibility);
458
            if (!$visibility) {
459
                $select .= ' AND visibleold = 1';
460
            }
461
            $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
462
        }
463
    }
464
    return $resourcestotoggle;
465
}
466
 
467
/**
468
 * Return the course category context for the category with id $categoryid, except
469
 * that if $categoryid is 0, return the system context.
470
 *
471
 * @param integer $categoryid a category id or 0.
472
 * @return context the corresponding context
473
 */
474
function get_category_or_system_context($categoryid) {
475
    if ($categoryid) {
476
        return context_coursecat::instance($categoryid, IGNORE_MISSING);
477
    } else {
478
        return context_system::instance();
479
    }
480
}
481
 
482
/**
483
 * Print the buttons relating to course requests.
484
 *
485
 * @param context $context current page context.
486
 * @deprecated since Moodle 4.0
487
 * @todo Final deprecation MDL-73976
488
 */
489
function print_course_request_buttons($context) {
490
    global $CFG, $DB, $OUTPUT;
491
    debugging("print_course_request_buttons() is deprecated. " .
492
        "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER);
493
    if (empty($CFG->enablecourserequests)) {
494
        return;
495
    }
496
    if (course_request::can_request($context)) {
497
        // Print a button to request a new course.
498
        $params = [];
499
        if ($context instanceof context_coursecat) {
500
            $params['category'] = $context->instanceid;
501
        }
502
        echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
503
            get_string('requestcourse'), 'get');
504
    }
505
    /// Print a button to manage pending requests
506
    if (has_capability('moodle/site:approvecourse', $context)) {
507
        $disabled = !$DB->record_exists('course_request', array());
508
        echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled));
509
    }
510
}
511
 
512
/**
513
 * Does the user have permission to edit things in this category?
514
 *
515
 * @param integer $categoryid The id of the category we are showing, or 0 for system context.
516
 * @return boolean has_any_capability(array(...), ...); in the appropriate context.
517
 */
518
function can_edit_in_category($categoryid = 0) {
519
    $context = get_category_or_system_context($categoryid);
520
    return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context);
521
}
522
 
523
/// MODULE FUNCTIONS /////////////////////////////////////////////////////////////////
524
 
525
function add_course_module($mod) {
526
    global $DB;
527
 
528
    $mod->added = time();
529
    unset($mod->id);
530
 
531
    $cmid = $DB->insert_record("course_modules", $mod);
532
    rebuild_course_cache($mod->course, true);
533
    return $cmid;
534
}
535
 
536
/**
537
 * Creates a course section and adds it to the specified position
538
 *
539
 * @param int|stdClass $courseorid course id or course object
540
 * @param int $position position to add to, 0 means to the end. If position is greater than
541
 *        number of existing secitons, the section is added to the end. This will become sectionnum of the
542
 *        new section. All existing sections at this or bigger position will be shifted down.
543
 * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
544
 * @return stdClass created section object
545
 */
546
function course_create_section($courseorid, $position = 0, $skipcheck = false) {
547
    return formatactions::section($courseorid)->create($position, $skipcheck);
548
}
549
 
550
/**
551
 * Creates missing course section(s) and rebuilds course cache
552
 *
553
 * @param int|stdClass $courseorid course id or course object
554
 * @param int|array $sections list of relative section numbers to create
555
 * @return bool if there were any sections created
556
 */
557
function course_create_sections_if_missing($courseorid, $sections) {
558
    if (!is_array($sections)) {
559
        $sections = array($sections);
560
    }
561
    return formatactions::section($courseorid)->create_if_missing($sections);
562
}
563
 
564
/**
565
 * Adds an existing module to the section
566
 *
567
 * Updates both tables {course_sections} and {course_modules}
568
 *
569
 * Note: This function does not use modinfo PROVIDED that the section you are
570
 * adding the module to already exists. If the section does not exist, it will
571
 * build modinfo if necessary and create the section.
572
 *
573
 * @param int|stdClass $courseorid course id or course object
574
 * @param int $cmid id of the module already existing in course_modules table
575
 * @param int $sectionnum relative number of the section (field course_sections.section)
576
 *     If section does not exist it will be created
577
 * @param int|stdClass $beforemod id or object with field id corresponding to the module
578
 *     before which the module needs to be included. Null for inserting in the
579
 *     end of the section
580
 * @return int The course_sections ID where the module is inserted
581
 */
582
function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) {
583
    global $DB, $COURSE;
584
    if (is_object($beforemod)) {
585
        $beforemod = $beforemod->id;
586
    }
587
    if (is_object($courseorid)) {
588
        $courseid = $courseorid->id;
589
    } else {
590
        $courseid = $courseorid;
591
    }
592
    // Do not try to use modinfo here, there is no guarantee it is valid!
593
    $section = $DB->get_record('course_sections',
594
            array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING);
595
    if (!$section) {
596
        // This function call requires modinfo.
597
        course_create_sections_if_missing($courseorid, $sectionnum);
598
        $section = $DB->get_record('course_sections',
599
                array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST);
600
    }
601
 
602
    $modarray = explode(",", trim($section->sequence));
603
    if (empty($section->sequence)) {
604
        $newsequence = "$cmid";
605
    } else if ($beforemod && ($key = moodle_array_keys_filter($modarray, $beforemod))) {
606
        $insertarray = array($cmid, $beforemod);
607
        array_splice($modarray, $key[0], 1, $insertarray);
608
        $newsequence = implode(",", $modarray);
609
    } else {
610
        $newsequence = "$section->sequence,$cmid";
611
    }
612
    $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
613
    $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
614
    rebuild_course_cache($courseid, true);
615
    return $section->id;     // Return course_sections ID that was used.
616
}
617
 
618
/**
619
 * Change the group mode of a course module.
620
 *
621
 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
622
 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
623
 *
624
 * @param int $id course module ID.
625
 * @param int $groupmode the new groupmode value.
626
 * @return bool True if the $groupmode was updated.
627
 */
628
function set_coursemodule_groupmode($id, $groupmode) {
629
    global $DB;
630
    $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
631
    if ($cm->groupmode != $groupmode) {
632
        $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
633
        \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
634
        rebuild_course_cache($cm->course, false, true);
635
    }
636
    return ($cm->groupmode != $groupmode);
637
}
638
 
639
function set_coursemodule_idnumber($id, $idnumber) {
640
    global $DB;
641
    $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
642
    if ($cm->idnumber != $idnumber) {
643
        $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
644
        \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
645
        rebuild_course_cache($cm->course, false, true);
646
    }
647
    return ($cm->idnumber != $idnumber);
648
}
649
 
650
/**
651
 * Set downloadcontent value to course module.
652
 *
653
 * @param int $id The id of the module.
654
 * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled.
655
 * @return bool True if downloadcontent has been updated, false otherwise.
656
 */
657
function set_downloadcontent(int $id, bool $downloadcontent): bool {
658
    global $DB;
659
    $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST);
660
    if ($cm->downloadcontent != $downloadcontent) {
661
        $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]);
662
        rebuild_course_cache($cm->course, true);
663
    }
664
    return ($cm->downloadcontent != $downloadcontent);
665
}
666
 
667
/**
668
 * Set the visibility of a module and inherent properties.
669
 *
670
 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
671
 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
672
 *
673
 * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered
674
 * has been moved to {@link set_section_visible()} which was the only place from which
675
 * the parameter was used.
676
 *
677
 * If $rebuildcache is set to false, the calling code is responsible for ensuring the cache is purged
678
 * and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times
679
 * (e.g. in a loop).
680
 *
681
 * @param int $id of the module
682
 * @param int $visible state of the module
683
 * @param int $visibleoncoursepage state of the module on the course page
684
 * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
685
 * @return bool false when the module was not found, true otherwise
686
 */
687
function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) {
688
    global $DB, $CFG;
689
    require_once($CFG->libdir.'/gradelib.php');
690
    require_once($CFG->dirroot.'/calendar/lib.php');
691
 
692
    if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
693
        return false;
694
    }
695
 
696
    // Create events and propagate visibility to associated grade items if the value has changed.
697
    // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
698
    if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
699
        return true;
700
    }
701
 
702
    if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
703
        return false;
704
    }
705
    if (($cm->visible != $visible) &&
706
            ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
707
        foreach($events as $event) {
708
            if ($visible) {
709
                $event = new calendar_event($event);
710
                $event->toggle_visibility(true);
711
            } else {
712
                $event = new calendar_event($event);
713
                $event->toggle_visibility(false);
714
            }
715
        }
716
    }
717
 
718
    // Updating visible and visibleold to keep them in sync. Only changing a section visibility will
719
    // affect visibleold to allow for an original visibility restore. See set_section_visible().
720
    $cminfo = new stdClass();
721
    $cminfo->id = $id;
722
    $cminfo->visible = $visible;
723
    $cminfo->visibleoncoursepage = $visibleoncoursepage;
724
    $cminfo->visibleold = $visible;
725
    $DB->update_record('course_modules', $cminfo);
726
 
727
    // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
728
    // Note that this must be done after updating the row in course_modules, in case
729
    // the modules grade_item_update function needs to access $cm->visible.
730
    if ($cm->visible != $visible &&
731
            plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
732
            component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
733
        $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
734
        component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
735
    } else if ($cm->visible != $visible) {
736
        $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
737
        if ($grade_items) {
738
            foreach ($grade_items as $grade_item) {
739
                $grade_item->set_hidden(!$visible);
740
            }
741
        }
742
    }
743
 
744
    if ($rebuildcache) {
745
        \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
746
        rebuild_course_cache($cm->course, false, true);
747
    }
748
    return true;
749
}
750
 
751
/**
752
 * Changes the course module name
753
 *
754
 * @param int $cmid course module id
755
 * @param string $name new value for a name
756
 * @return bool whether a change was made
757
 */
758
function set_coursemodule_name($cmid, $name) {
759
    $coursecontext = context_module::instance($cmid)->get_course_context();
760
    return formatactions::cm($coursecontext->instanceid)->rename($cmid, $name);
761
}
762
 
763
/**
764
 * This function will handle the whole deletion process of a module. This includes calling
765
 * the modules delete_instance function, deleting files, events, grades, conditional data,
766
 * the data in the course_module and course_sections table and adding a module deletion
767
 * event to the DB.
768
 *
769
 * @param int $cmid the course module id
770
 * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
771
 * @throws moodle_exception
772
 * @since Moodle 2.5
773
 */
774
function course_delete_module($cmid, $async = false) {
775
    // Check the 'course_module_background_deletion_recommended' hook first.
776
    // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
777
    // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
778
    // It's up to plugins to handle things like whether or not they are enabled.
779
    if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
780
        foreach ($pluginsfunction as $plugintype => $plugins) {
781
            foreach ($plugins as $pluginfunction) {
782
                if ($pluginfunction()) {
783
                    return course_module_flag_for_async_deletion($cmid);
784
                }
785
            }
786
        }
787
    }
788
 
789
    global $CFG, $DB;
790
 
791
    require_once($CFG->libdir.'/gradelib.php');
792
    require_once($CFG->libdir.'/questionlib.php');
793
    require_once($CFG->dirroot.'/blog/lib.php');
794
    require_once($CFG->dirroot.'/calendar/lib.php');
795
 
796
    // Get the course module.
797
    if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
798
        return true;
799
    }
800
 
801
    // Get the module context.
802
    $modcontext = context_module::instance($cm->id);
803
 
804
    // Get the course module name.
805
    $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
806
 
807
    // Get the file location of the delete_instance function for this module.
808
    $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
809
 
810
    // Include the file required to call the delete_instance function for this module.
811
    if (file_exists($modlib)) {
812
        require_once($modlib);
813
    } else {
814
        throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
815
            "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
816
    }
817
 
818
    $deleteinstancefunction = $modulename . '_delete_instance';
819
 
820
    // Ensure the delete_instance function exists for this module.
821
    if (!function_exists($deleteinstancefunction)) {
822
        throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
823
            "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
824
    }
825
 
826
    // Allow plugins to use this course module before we completely delete it.
827
    if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
828
        foreach ($pluginsfunction as $plugintype => $plugins) {
829
            foreach ($plugins as $pluginfunction) {
830
                $pluginfunction($cm);
831
            }
832
        }
833
    }
834
 
835
    // Call the delete_instance function, if it returns false throw an exception.
836
    if (!$deleteinstancefunction($cm->instance)) {
837
        throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
838
            "Cannot delete the module $modulename (instance).");
839
    }
840
 
841
    question_delete_activity($cm);
842
 
843
    // Remove all module files in case modules forget to do that.
844
    $fs = get_file_storage();
845
    $fs->delete_area_files($modcontext->id);
846
 
847
    // Delete events from calendar.
848
    if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
849
        $coursecontext = context_course::instance($cm->course);
850
        foreach($events as $event) {
851
            $event->context = $coursecontext;
852
            $calendarevent = calendar_event::load($event);
853
            $calendarevent->delete();
854
        }
855
    }
856
 
857
    // Delete grade items, outcome items and grades attached to modules.
858
    if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename,
859
                                                   'iteminstance' => $cm->instance, 'courseid' => $cm->course))) {
860
        foreach ($grade_items as $grade_item) {
861
            $grade_item->delete('moddelete');
862
        }
863
    }
864
 
865
    // Delete associated blogs and blog tag instances.
866
    blog_remove_associations_for_module($modcontext->id);
867
 
868
    // Delete completion and availability data; it is better to do this even if the
869
    // features are not turned on, in case they were turned on previously (these will be
870
    // very quick on an empty table).
871
    $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
872
    $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
873
    $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
874
                                                            'course' => $cm->course,
875
                                                            'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
876
 
877
    // Delete all tag instances associated with the instance of this module.
878
    core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
879
    core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
880
 
881
    // Notify the competency subsystem.
882
    \core_competency\api::hook_course_module_deleted($cm);
883
 
884
    // Delete the context.
885
    context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
886
 
887
    // Delete the module from the course_modules table.
888
    $DB->delete_records('course_modules', array('id' => $cm->id));
889
 
890
    // Delete module from that section.
891
    if (!delete_mod_from_section($cm->id, $cm->section)) {
892
        throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
893
            "Cannot delete the module $modulename (instance) from section.");
894
    }
895
 
896
    // Trigger event for course module delete action.
897
    $event = \core\event\course_module_deleted::create(array(
898
        'courseid' => $cm->course,
899
        'context'  => $modcontext,
900
        'objectid' => $cm->id,
901
        'other'    => array(
902
            'modulename'   => $modulename,
903
            'instanceid'   => $cm->instance,
904
        )
905
    ));
906
    $event->add_record_snapshot('course_modules', $cm);
907
    $event->trigger();
908
    \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
909
    rebuild_course_cache($cm->course, false, true);
910
}
911
 
912
/**
913
 * Schedule a course module for deletion in the background using an adhoc task.
914
 *
915
 * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
916
 * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
917
 *
918
 * @param int $cmid the course module id.
919
 * @return ?bool whether the module was successfully scheduled for deletion.
920
 * @throws \moodle_exception
921
 */
922
function course_module_flag_for_async_deletion($cmid) {
923
    global $CFG, $DB, $USER;
924
    require_once($CFG->libdir.'/gradelib.php');
925
    require_once($CFG->libdir.'/questionlib.php');
926
    require_once($CFG->dirroot.'/blog/lib.php');
927
    require_once($CFG->dirroot.'/calendar/lib.php');
928
 
929
    // Get the course module.
930
    if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
931
        return true;
932
    }
933
 
934
    // We need to be reasonably certain the deletion is going to succeed before we background the process.
935
    // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
936
 
937
    // Get the course module name.
938
    $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
939
 
940
    // Get the file location of the delete_instance function for this module.
941
    $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
942
 
943
    // Include the file required to call the delete_instance function for this module.
944
    if (file_exists($modlib)) {
945
        require_once($modlib);
946
    } else {
947
        throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
948
            "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
949
    }
950
 
951
    $deleteinstancefunction = $modulename . '_delete_instance';
952
 
953
    // Ensure the delete_instance function exists for this module.
954
    if (!function_exists($deleteinstancefunction)) {
955
        throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
956
            "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
957
    }
958
 
959
    // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
960
    $cm->deletioninprogress = '1';
961
    $DB->update_record('course_modules', $cm);
962
 
963
    // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
964
    $removaltask = new \core_course\task\course_delete_modules();
965
    $removaltask->set_custom_data(array(
966
        'cms' => array($cm),
967
        'userid' => $USER->id,
968
        'realuserid' => \core\session\manager::get_realuser()->id
969
    ));
970
 
971
    // Queue the task for the next run.
972
    \core\task\manager::queue_adhoc_task($removaltask);
973
 
974
    // Reset the course cache to hide the module.
975
    rebuild_course_cache($cm->course, true);
976
}
977
 
978
/**
979
 * Checks whether the given course has any course modules scheduled for adhoc deletion.
980
 *
981
 * @param int $courseid the id of the course.
982
 * @param bool $onlygradable whether to check only gradable modules or all modules.
983
 * @return bool true if the course contains any modules pending deletion, false otherwise.
984
 */
985
function course_modules_pending_deletion(int $courseid, bool $onlygradable = false): bool {
986
    if (empty($courseid)) {
987
        return false;
988
    }
989
 
990
    if ($onlygradable) {
991
        // Fetch modules with grade items.
992
        if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) {
993
            // Return early when there is none.
994
            return false;
995
        }
996
    }
997
 
998
    $modinfo = get_fast_modinfo($courseid);
999
    foreach ($modinfo->get_cms() as $module) {
1000
        if ($module->deletioninprogress == '1') {
1001
            if ($onlygradable) {
1002
                // Check if the module being deleted is in the list of course modules with grade items.
1003
                foreach ($coursegradeitems as $coursegradeitem) {
1004
                    if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) {
1005
                        // The module being deleted is within the gradable  modules.
1006
                        return true;
1007
                    }
1008
                }
1009
            } else {
1010
                return true;
1011
            }
1012
        }
1013
    }
1014
    return false;
1015
}
1016
 
1017
/**
1018
 * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course.
1019
 *
1020
 * @param int $courseid the course id.
1021
 * @param string $modulename the module name. E.g. 'assign', 'book', etc.
1022
 * @param int $instanceid the module instance id.
1023
 * @return bool true if the course module is pending deletion, false otherwise.
1024
 */
1025
function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) {
1026
    if (empty($courseid) || empty($modulename) || empty($instanceid)) {
1027
        return false;
1028
    }
1029
    $modinfo = get_fast_modinfo($courseid);
1030
    $instances = $modinfo->get_instances_of($modulename);
1031
    return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress;
1032
}
1033
 
1034
function delete_mod_from_section($modid, $sectionid) {
1035
    global $DB;
1036
 
1037
    if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) {
1038
 
1039
        $modarray = explode(",", $section->sequence);
1040
 
1041
        if ($key = moodle_array_keys_filter($modarray, $modid)) {
1042
            array_splice($modarray, $key[0], 1);
1043
            $newsequence = implode(",", $modarray);
1044
            $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id));
1045
            rebuild_course_cache($section->course, true);
1046
            return true;
1047
        } else {
1048
            return false;
1049
        }
1050
 
1051
    }
1052
    return false;
1053
}
1054
 
1055
/**
1056
 * This function updates the calendar events from the information stored in the module table and the course
1057
 * module table.
1058
 *
1059
 * @param  string $modulename Module name
1060
 * @param  stdClass $instance Module object. Either the $instance or the $cm must be supplied.
1061
 * @param  stdClass $cm Course module object. Either the $instance or the $cm must be supplied.
1062
 * @return bool Returns true if calendar events are updated.
1063
 * @since  Moodle 3.3.4
1064
 */
1065
function course_module_update_calendar_events($modulename, $instance = null, $cm = null) {
1066
    global $DB;
1067
 
1068
    if (isset($instance) || isset($cm)) {
1069
 
1070
        if (!isset($instance)) {
1071
            $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
1072
        }
1073
        if (!isset($cm)) {
1074
            $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
1075
        }
1076
        if (!empty($cm)) {
1077
            course_module_calendar_event_update_process($instance, $cm);
1078
        }
1079
        return true;
1080
    }
1081
    return false;
1082
}
1083
 
1084
/**
1085
 * Update all instances through out the site or in a course.
1086
 *
1087
 * @param  string  $modulename Module type to update.
1088
 * @param  integer $courseid   Course id to update events. 0 for the whole site.
1089
 * @return bool Returns True if the update was successful.
1090
 * @since  Moodle 3.3.4
1091
 */
1092
function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
1093
    global $DB;
1094
 
1095
    $instances = null;
1096
    if ($courseid) {
1097
        if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) {
1098
            return false;
1099
        }
1100
    } else {
1101
        if (!$instances = $DB->get_records($modulename)) {
1102
            return false;
1103
        }
1104
    }
1105
 
1106
    foreach ($instances as $instance) {
1107
        if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) {
1108
            course_module_calendar_event_update_process($instance, $cm);
1109
        }
1110
    }
1111
    return true;
1112
}
1113
 
1114
/**
1115
 * Calendar events for a module instance are updated.
1116
 *
1117
 * @param  stdClass $instance Module instance object.
1118
 * @param  stdClass $cm Course Module object.
1119
 * @since  Moodle 3.3.4
1120
 */
1121
function course_module_calendar_event_update_process($instance, $cm) {
1122
    // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which
1123
    // will remove the completion events.
1124
    $refresheventsfunction = $cm->modname . '_refresh_events';
1125
    if (function_exists($refresheventsfunction)) {
1126
        call_user_func($refresheventsfunction, $cm->course, $instance, $cm);
1127
    }
1128
    $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null;
1129
    \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected);
1130
}
1131
 
1132
/**
1133
 * Moves a section within a course, from a position to another.
1134
 * Be very careful: $section and $destination refer to section number,
1135
 * not id!.
1136
 *
1137
 * @param object $course
1138
 * @param int $section Section number (not id!!!)
1139
 * @param int $destination
1140
 * @param bool $ignorenumsections
1141
 * @return boolean Result
1142
 */
1143
function move_section_to($course, $section, $destination, $ignorenumsections = false) {
1144
/// Moves a whole course section up and down within the course
1145
    global $USER, $DB;
1146
 
1147
    if (!$destination && $destination != 0) {
1148
        return true;
1149
    }
1150
 
1151
    // compartibility with course formats using field 'numsections'
1152
    $courseformatoptions = course_get_format($course)->get_format_options();
1153
    if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) &&
1154
            ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) {
1155
        return false;
1156
    }
1157
 
1158
    // Get all sections for this course and re-order them (2 of them should now share the same section number)
1159
    if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id),
1160
            'section ASC, id ASC', 'id, section')) {
1161
        return false;
1162
    }
1163
 
1164
    $movedsections = reorder_sections($sections, $section, $destination);
1165
 
1166
    // Update all sections. Do this in 2 steps to avoid breaking database
1167
    // uniqueness constraint
1168
    $transaction = $DB->start_delegated_transaction();
1169
    foreach ($movedsections as $id => $position) {
1170
        if ((int) $sections[$id] !== $position) {
1171
            $DB->set_field('course_sections', 'section', -$position, ['id' => $id]);
1172
            // Invalidate the section cache by given section id.
1173
            course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1174
        }
1175
    }
1176
    foreach ($movedsections as $id => $position) {
1177
        if ((int) $sections[$id] !== $position) {
1178
            $DB->set_field('course_sections', 'section', $position, ['id' => $id]);
1179
            // Invalidate the section cache by given section id.
1180
            course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1181
        }
1182
    }
1183
 
1184
    // If we move the highlighted section itself, then just highlight the destination.
1185
    // Adjust the higlighted section location if we move something over it either direction.
1186
    if ($section == $course->marker) {
1187
        course_set_marker($course->id, $destination);
1188
    } else if ($section > $course->marker && $course->marker >= $destination) {
1189
        course_set_marker($course->id, $course->marker+1);
1190
    } else if ($section < $course->marker && $course->marker <= $destination) {
1191
        course_set_marker($course->id, $course->marker-1);
1192
    }
1193
 
1194
    $transaction->allow_commit();
1195
    rebuild_course_cache($course->id, true, true);
1196
    return true;
1197
}
1198
 
1199
/**
1200
 * This method will delete a course section and may delete all modules inside it.
1201
 *
1202
 * No permissions are checked here, use {@link course_can_delete_section()} to
1203
 * check if section can actually be deleted.
1204
 *
1205
 * @param int|stdClass $course
1206
 * @param int|stdClass|section_info $sectionornum
1207
 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1208
 * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
1209
 * @return bool whether section was deleted
1210
 */
1211
function course_delete_section($course, $sectionornum, $forcedeleteifnotempty = true, $async = false) {
1212
    $sectionnum = (is_object($sectionornum)) ? $sectionornum->section : (int)$sectionornum;
1213
    $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum);
1214
    if (!$sectioninfo) {
1215
        return false;
1216
    }
1217
    return formatactions::section($course)->delete($sectioninfo, $forcedeleteifnotempty, $async);
1218
}
1219
 
1220
/**
1221
 * Course section deletion, using an adhoc task for deletion of the modules it contains.
1222
 * 1. Schedule all modules within the section for adhoc removal.
1223
 * 2. Move all modules to course section 0.
1224
 * 3. Delete the resulting empty section.
1225
 *
1226
 * @param \stdClass $section the section to schedule for deletion.
1227
 * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
1228
 * @return bool true if the section was scheduled for deletion, false otherwise.
1229
 */
1230
function course_delete_section_async($section, $forcedeleteifnotempty = true) {
1231
    if (!is_object($section) || empty($section->id) || empty($section->course)) {
1232
        return false;
1233
    }
1234
    $sectioninfo = get_fast_modinfo($section->course)->get_section_info_by_id($section->id);
1235
    if (!$sectioninfo) {
1236
        return false;
1237
    }
1238
    return formatactions::section($section->course)->delete_async($sectioninfo, $forcedeleteifnotempty);
1239
}
1240
 
1241
/**
1242
 * Updates the course section
1243
 *
1244
 * This function does not check permissions or clean values - this has to be done prior to calling it.
1245
 *
1246
 * @param int|stdClass $courseorid
1247
 * @param stdClass|section_info $section record from course_sections table - it will be updated with the new values
1248
 * @param array|stdClass $data
1249
 */
1250
function course_update_section($courseorid, $section, $data) {
1251
    $sectioninfo = get_fast_modinfo($courseorid)->get_section_info_by_id($section->id);
1252
    formatactions::section($courseorid)->update($sectioninfo, $data);
1253
 
1254
    // Update $section object fields (for legacy compatibility).
1255
    $data = array_diff_key((array) $data, array_flip(['id', 'course', 'section', 'sequence']));
1256
    foreach ($data as $key => $value) {
1257
        if (property_exists($section, $key)) {
1258
            $section->$key = $value;
1259
        }
1260
    }
1261
}
1262
 
1263
/**
1264
 * Checks if the current user can delete a section (if course format allows it and user has proper permissions).
1265
 *
1266
 * @param int|stdClass $course
1267
 * @param int|stdClass|section_info $section
1268
 * @return bool
1269
 */
1270
function course_can_delete_section($course, $section) {
1271
    if (is_object($section)) {
1272
        $section = $section->section;
1273
    }
1274
    if (!$section) {
1275
        // Not possible to delete 0-section.
1276
        return false;
1277
    }
1278
    // Course format should allow to delete sections.
1279
    if (!course_get_format($course)->can_delete_section($section)) {
1280
        return false;
1281
    }
1282
    // Make sure user has capability to update course and move sections.
1283
    $context = context_course::instance(is_object($course) ? $course->id : $course);
1284
    if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) {
1285
        return false;
1286
    }
1287
    // Make sure user has capability to delete each activity in this section.
1288
    $modinfo = get_fast_modinfo($course);
1289
    if (!empty($modinfo->sections[$section])) {
1290
        foreach ($modinfo->sections[$section] as $cmid) {
1291
            if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
1292
                return false;
1293
            }
1294
        }
1295
    }
1296
    return true;
1297
}
1298
 
1299
/**
1300
 * Reordering algorithm for course sections. Given an array of section->section indexed by section->id,
1301
 * an original position number and a target position number, rebuilds the array so that the
1302
 * move is made without any duplication of section positions.
1303
 * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to
1304
 * insert a section before the first one, you must give 0 as the target (section 0 can never be moved).
1305
 *
1306
 * @param array $sections
1307
 * @param int $origin_position
1308
 * @param int $target_position
1309
 * @return array|false
1310
 */
1311
function reorder_sections($sections, $origin_position, $target_position) {
1312
    if (!is_array($sections)) {
1313
        return false;
1314
    }
1315
 
1316
    // We can't move section position 0
1317
    if ($origin_position < 1) {
1318
        echo "We can't move section position 0";
1319
        return false;
1320
    }
1321
 
1322
    // Locate origin section in sections array
1323
    if (!$origin_key = array_search($origin_position, $sections)) {
1324
        echo "searched position not in sections array";
1325
        return false; // searched position not in sections array
1326
    }
1327
 
1328
    // Extract origin section
1329
    $origin_section = $sections[$origin_key];
1330
    unset($sections[$origin_key]);
1331
 
1332
    // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!)
1333
    $found = false;
1334
    $append_array = array();
1335
    foreach ($sections as $id => $position) {
1336
        if ($found) {
1337
            $append_array[$id] = $position;
1338
            unset($sections[$id]);
1339
        }
1340
        if ($position == $target_position) {
1341
            if ($target_position < $origin_position) {
1342
                $append_array[$id] = $position;
1343
                unset($sections[$id]);
1344
            }
1345
            $found = true;
1346
        }
1347
    }
1348
 
1349
    // Append moved section
1350
    $sections[$origin_key] = $origin_section;
1351
 
1352
    // Append rest of array (if applicable)
1353
    if (!empty($append_array)) {
1354
        foreach ($append_array as $id => $position) {
1355
            $sections[$id] = $position;
1356
        }
1357
    }
1358
 
1359
    // Renumber positions
1360
    $position = 0;
1361
    foreach ($sections as $id => $p) {
1362
        $sections[$id] = $position;
1363
        $position++;
1364
    }
1365
 
1366
    return $sections;
1367
 
1368
}
1369
 
1370
/**
1371
 * Move the module object $mod to the specified $section
1372
 * If $beforemod exists then that is the module
1373
 * before which $modid should be inserted
1374
 *
1375
 * @param stdClass|cm_info $mod
1376
 * @param stdClass|section_info $section
1377
 * @param int|stdClass $beforemod id or object with field id corresponding to the module
1378
 *     before which the module needs to be included. Null for inserting in the
1379
 *     end of the section
1380
 * @return int new value for module visibility (0 or 1)
1381
 */
1382
function moveto_module($mod, $section, $beforemod=NULL) {
1383
    global $OUTPUT, $DB;
1384
 
1385
    // Current module visibility state - return value of this function.
1386
    $modvisible = $mod->visible;
1387
 
1388
    // Remove original module from original section.
1389
    if (! delete_mod_from_section($mod->id, $mod->section)) {
1390
        echo $OUTPUT->notification("Could not delete module from existing section");
1391
    }
1392
 
1393
    // Add the module into the new section.
1394
    course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod);
1395
 
1396
    // If moving to a hidden section then hide module.
1397
    if ($mod->section != $section->id) {
1398
        if (!$section->visible && $mod->visible) {
1399
            // Module was visible but must become hidden after moving to hidden section.
1400
            $modvisible = 0;
1401
            set_coursemodule_visible($mod->id, 0);
1402
            // Set visibleold to 1 so module will be visible when section is made visible.
1403
            $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
1404
        }
1405
        if ($section->visible && !$mod->visible) {
1406
            // Hidden module was moved to the visible section, restore the module visibility from visibleold.
1407
            set_coursemodule_visible($mod->id, $mod->visibleold);
1408
            $modvisible = $mod->visibleold;
1409
        }
1410
    }
1411
 
1412
    return $modvisible;
1413
}
1414
 
1415
/**
1416
 * Returns the list of all editing actions that current user can perform on the module
1417
 *
1418
 * @param cm_info $mod The module to produce editing buttons for
1419
 * @param int $indent The current indenting (default -1 means no move left-right actions)
1420
 * @param int $sr The section to link back to (used for creating the links)
1421
 * @return array array of action_link or pix_icon objects
1422
 */
1423
function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
1424
    global $COURSE, $SITE, $CFG;
1425
 
1426
    static $str;
1427
 
1428
    $coursecontext = context_course::instance($mod->course);
1429
    $modcontext = context_module::instance($mod->id);
1430
    $courseformat = course_get_format($mod->get_course());
1431
    $usecomponents = $courseformat->supports_components();
1432
    $sectioninfo = $mod->get_section_info();
1433
 
1434
    $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
1435
    $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
1436
 
1437
    // No permission to edit anything.
1438
    if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
1439
        return array();
1440
    }
1441
 
1442
    $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1443
 
1444
    if (!isset($str)) {
1445
        $str = get_strings(
1446
            [
1447
                'delete', 'move', 'moveright', 'moveleft', 'editsettings',
1448
                'duplicate', 'availability',
1449
            ],
1450
            'moodle'
1451
        );
1452
        $str->assign = get_string('assignroles', 'role');
1453
        $str->groupmode = get_string('groupmode', 'group');
1454
    }
1455
 
1456
    $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1457
 
1458
    if ($sr !== null) {
1459
        $baseurl->param('sr', $sr);
1460
    }
1461
    $actions = array();
1462
 
1463
    // Update.
1464
    if ($hasmanageactivities) {
1465
        $actions['update'] = new action_menu_link_secondary(
1466
            new moodle_url($baseurl, array('update' => $mod->id)),
1467
            new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')),
1468
            $str->editsettings,
1469
            array('class' => 'editing_update', 'data-action' => 'update')
1470
        );
1471
    }
1472
 
1473
    // Move (only for component compatible formats).
1474
    if ($hasmanageactivities && $usecomponents) {
1475
        $actions['move'] = new action_menu_link_secondary(
1476
            new moodle_url($baseurl, [
1477
                'sesskey' => sesskey(),
1478
                'copy' => $mod->id,
1479
            ]),
1480
            new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']),
1481
            $str->move,
1482
            [
1483
                'class' => 'editing_movecm',
1484
                'data-action' => 'moveCm',
1485
                'data-id' => $mod->id,
1486
            ]
1487
        );
1488
    }
1489
 
1490
    // Indent.
1491
    if ($hasmanageactivities && $indent >= 0) {
1492
        $indentlimits = new stdClass();
1493
        $indentlimits->min = 0;
1494
        // Legacy indentation could continue using a limit of 16,
1495
        // but components based formats will be forced to use one level indentation only.
1496
        $indentlimits->max = ($usecomponents) ? 1 : 16;
1497
        if (right_to_left()) {   // Exchange arrows on RTL
1498
            $rightarrow = 't/left';
1499
            $leftarrow  = 't/right';
1500
        } else {
1501
            $rightarrow = 't/right';
1502
            $leftarrow  = 't/left';
1503
        }
1504
 
1505
        if ($indent >= $indentlimits->max) {
1506
            $enabledclass = 'hidden';
1507
        } else {
1508
            $enabledclass = '';
1509
        }
1510
        $actions['moveright'] = new action_menu_link_secondary(
1511
            new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']),
1512
            new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']),
1513
            $str->moveright,
1514
            [
1515
                'class' => 'editing_moveright ' . $enabledclass,
1516
                'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright',
1517
                'data-keepopen' => true,
1518
                'data-sectionreturn' => $sr,
1519
                'data-id' => $mod->id,
1520
            ]
1521
        );
1522
 
1523
        if ($indent <= $indentlimits->min) {
1524
            $enabledclass = 'hidden';
1525
        } else {
1526
            $enabledclass = '';
1527
        }
1528
        $actions['moveleft'] = new action_menu_link_secondary(
1529
            new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '-1']),
1530
            new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']),
1531
            $str->moveleft,
1532
            [
1533
                'class' => 'editing_moveleft ' . $enabledclass,
1534
                'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft',
1535
                'data-keepopen' => true,
1536
                'data-sectionreturn' => $sr,
1537
                'data-id' => $mod->id,
1538
            ]
1539
        );
1540
 
1541
    }
1542
 
1543
    // Hide/Show/Available/Unavailable.
1544
    if (has_capability('moodle/course:activityvisibility', $modcontext)) {
1545
        $availabilityclass = $courseformat->get_output_classname('content\\cm\\visibility');
1546
        /** @var core_courseformat\output\local\content\cm\visibility */
1547
        $availability = new $availabilityclass($courseformat, $sectioninfo, $mod);
1548
        $availabilityitem = $availability->get_menu_item();
1549
        if ($availabilityitem) {
1550
            $actions['availability'] = $availabilityitem;
1551
        }
1552
    }
1553
 
1554
    // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
1555
    if (has_all_capabilities($dupecaps, $coursecontext) &&
1556
            plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
1557
            course_allowed_module($mod->get_course(), $mod->modname)) {
1558
        $actions['duplicate'] = new action_menu_link_secondary(
1559
            new moodle_url($baseurl, ['duplicate' => $mod->id]),
1560
            new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
1561
            $str->duplicate,
1562
            [
1563
                'class' => 'editing_duplicate',
1564
                'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate',
1565
                'data-sectionreturn' => $sr,
1566
                'data-id' => $mod->id,
1567
            ]
1568
        );
1569
    }
1570
 
1571
    // Assign.
1572
    if (has_capability('moodle/role:assign', $modcontext)){
1573
        $actions['assign'] = new action_menu_link_secondary(
1574
            new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
1575
            new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
1576
            $str->assign,
1577
            array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
1578
        );
1579
    }
1580
 
1581
    // Groupmode.
1582
    if ($courseformat->show_groupmode($mod) && $usecomponents  && !$mod->coursegroupmodeforce) {
1583
        $groupmodeclass = $courseformat->get_output_classname('content\\cm\\groupmode');
1584
        /** @var core_courseformat\output\local\content\cm\groupmode */
1585
        $groupmode = new $groupmodeclass($courseformat, $sectioninfo, $mod);
1586
        $actions['groupmode'] = new action_menu_subpanel(
1587
            $str->groupmode,
1588
            $groupmode->get_choice_list(),
1589
            ['class' => 'editing_groupmode'],
1590
            new pix_icon('i/groupv', '', 'moodle', ['class' => 'iconsmall'])
1591
        );
1592
    }
1593
 
1594
    // Delete.
1595
    if ($hasmanageactivities) {
1596
        $actions['delete'] = new action_menu_link_secondary(
1597
            new moodle_url($baseurl, ['delete' => $mod->id]),
1598
            new pix_icon('t/delete', '', 'moodle', ['class' => 'iconsmall']),
1599
            $str->delete,
1600
            [
1601
                'class' => 'editing_delete text-danger',
1602
                'data-action' => ($usecomponents) ? 'cmDelete' : 'delete',
1603
                'data-sectionreturn' => $sr,
1604
                'data-id' => $mod->id,
1605
            ]
1606
        );
1607
    }
1608
 
1609
    return $actions;
1610
}
1611
 
1612
/**
1613
 * Returns the move action.
1614
 *
1615
 * @param cm_info $mod The module to produce a move button for
1616
 * @param int $sr The section to link back to (used for creating the links)
1617
 * @return string The markup for the move action, or an empty string if not available.
1618
 */
1619
function course_get_cm_move(cm_info $mod, $sr = null) {
1620
    global $OUTPUT;
1621
 
1622
    static $str;
1623
    static $baseurl;
1624
 
1625
    $modcontext = context_module::instance($mod->id);
1626
    $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1627
 
1628
    if (!isset($str)) {
1629
        $str = get_strings(array('move'));
1630
    }
1631
 
1632
    if (!isset($baseurl)) {
1633
        $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1634
 
1635
        if ($sr !== null) {
1636
            $baseurl->param('sr', $sr);
1637
        }
1638
    }
1639
 
1640
    if ($hasmanageactivities) {
1641
        $pixicon = 'i/dragdrop';
1642
 
1643
        if (!course_ajax_enabled($mod->get_course())) {
1644
            // Override for course frontpage until we get drag/drop working there.
1645
            $pixicon = 't/move';
1646
        }
1647
 
1648
        $attributes = [
1649
            'class' => 'editing_move',
1650
            'data-action' => 'move',
1651
            'data-sectionreturn' => $sr,
1652
            'title' => $str->move,
1653
            'aria-label' => $str->move,
1654
        ];
1655
        return html_writer::link(
1656
            new moodle_url($baseurl, ['copy' => $mod->id]),
1657
            $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']),
1658
            $attributes
1659
        );
1660
    }
1661
    return '';
1662
}
1663
 
1664
/**
1665
 * given a course object with shortname & fullname, this function will
1666
 * truncate the the number of chars allowed and add ... if it was too long
1667
 */
1668
function course_format_name ($course,$max=100) {
1669
 
1670
    $context = context_course::instance($course->id);
1671
    $shortname = format_string($course->shortname, true, array('context' => $context));
1672
    $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
1673
    $str = $shortname.': '. $fullname;
1674
    if (core_text::strlen($str) <= $max) {
1675
        return $str;
1676
    }
1677
    else {
1678
        return core_text::substr($str,0,$max-3).'...';
1679
    }
1680
}
1681
 
1682
/**
1683
 * Is the user allowed to add this type of module to this course?
1684
 * @param object $course the course settings. Only $course->id is used.
1685
 * @param string $modname the module name. E.g. 'forum' or 'quiz'.
1686
 * @param \stdClass $user the user to check, defaults to the global user if not provided.
1687
 * @return bool whether the current user is allowed to add this type of module to this course.
1688
 */
1689
function course_allowed_module($course, $modname, \stdClass $user = null) {
1690
    global $USER;
1691
    $user = $user ?? $USER;
1692
    if (is_numeric($modname)) {
1693
        throw new coding_exception('Function course_allowed_module no longer
1694
                supports numeric module ids. Please update your code to pass the module name.');
1695
    }
1696
 
1697
    $capability = 'mod/' . $modname . ':addinstance';
1698
    if (!get_capability_info($capability)) {
1699
        // Debug warning that the capability does not exist, but no more than once per page.
1700
        static $warned = array();
1701
        $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
1702
        if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) {
1703
            debugging('The module ' . $modname . ' does not define the standard capability ' .
1704
                    $capability , DEBUG_DEVELOPER);
1705
            $warned[$modname] = 1;
1706
        }
1707
 
1708
        // If the capability does not exist, the module can always be added.
1709
        return true;
1710
    }
1711
 
1712
    $coursecontext = context_course::instance($course->id);
1713
    return has_capability($capability, $coursecontext, $user);
1714
}
1715
 
1716
/**
1717
 * Efficiently moves many courses around while maintaining
1718
 * sortorder in order.
1719
 *
1720
 * @param array $courseids is an array of course ids
1721
 * @param int $categoryid
1722
 * @return bool success
1723
 */
1724
function move_courses($courseids, $categoryid) {
1725
    global $DB;
1726
 
1727
    if (empty($courseids)) {
1728
        // Nothing to do.
1729
        return false;
1730
    }
1731
 
1732
    if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
1733
        return false;
1734
    }
1735
 
1736
    $courseids = array_reverse($courseids);
1737
    $newparent = context_coursecat::instance($category->id);
1738
    $i = 1;
1739
 
1740
    list($where, $params) = $DB->get_in_or_equal($courseids);
1741
    $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname');
1742
    foreach ($dbcourses as $dbcourse) {
1743
        $course = new stdClass();
1744
        $course->id = $dbcourse->id;
1745
        $course->timemodified = time();
1746
        $course->category  = $category->id;
1747
        $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
1748
        if ($category->visible == 0) {
1749
            // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
1750
            // to previous state if somebody unhides the category.
1751
            $course->visible = 0;
1752
        }
1753
 
1754
        $DB->update_record('course', $course);
1755
 
1756
        // Update context, so it can be passed to event.
1757
        $context = context_course::instance($course->id);
1758
        $context->update_moved($newparent);
1759
 
1760
        // Trigger a course updated event.
1761
        $event = \core\event\course_updated::create(array(
1762
            'objectid' => $course->id,
1763
            'context' => context_course::instance($course->id),
1764
            'other' => array('shortname' => $dbcourse->shortname,
1765
                             'fullname' => $dbcourse->fullname,
1766
                             'updatedfields' => array('category' => $category->id))
1767
        ));
1768
        $event->trigger();
1769
    }
1770
    fix_course_sortorder();
1771
    cache_helper::purge_by_event('changesincourse');
1772
 
1773
    return true;
1774
}
1775
 
1776
/**
1777
 * Returns the display name of the given section that the course prefers
1778
 *
1779
 * Implementation of this function is provided by course format
1780
 * @see core_courseformat\base::get_section_name()
1781
 *
1782
 * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
1783
 * @param int|stdClass $section Section object from database or just field course_sections.section
1784
 * @return string Display name that the course format prefers, e.g. "Week 2"
1785
 */
1786
function get_section_name($courseorid, $section) {
1787
    return course_get_format($courseorid)->get_section_name($section);
1788
}
1789
 
1790
/**
1791
 * Tells if current course format uses sections
1792
 *
1793
 * @param string $format Course format ID e.g. 'weeks' $course->format
1794
 * @return bool
1795
 */
1796
function course_format_uses_sections($format) {
1797
    $course = new stdClass();
1798
    $course->format = $format;
1799
    return course_get_format($course)->uses_sections();
1800
}
1801
 
1802
/**
1803
 * Returns the information about the ajax support in the given source format
1804
 *
1805
 * The returned object's property (boolean)capable indicates that
1806
 * the course format supports Moodle course ajax features.
1807
 *
1808
 * @param string $format
1809
 * @return stdClass
1810
 */
1811
function course_format_ajax_support($format) {
1812
    $course = new stdClass();
1813
    $course->format = $format;
1814
    return course_get_format($course)->supports_ajax();
1815
}
1816
 
1817
/**
1818
 * Can the current user delete this course?
1819
 * Course creators have exception,
1820
 * 1 day after the creation they can sill delete the course.
1821
 * @param int $courseid
1822
 * @return boolean
1823
 */
1824
function can_delete_course($courseid) {
1825
    global $USER;
1826
 
1827
    $context = context_course::instance($courseid);
1828
 
1829
    if (has_capability('moodle/course:delete', $context)) {
1830
        return true;
1831
    }
1832
 
1833
    // hack: now try to find out if creator created this course recently (1 day)
1834
    if (!has_capability('moodle/course:create', $context)) {
1835
        return false;
1836
    }
1837
 
1838
    $since = time() - 60*60*24;
1839
    $course = get_course($courseid);
1840
 
1841
    if ($course->timecreated < $since) {
1842
        return false; // Return if the course was not created in last 24 hours.
1843
    }
1844
 
1845
    $logmanger = get_log_manager();
1846
    $readers = $logmanger->get_readers('\core\log\sql_reader');
1847
    $reader = reset($readers);
1848
 
1849
    if (empty($reader)) {
1850
        return false; // No log reader found.
1851
    }
1852
 
1853
    // A proper reader.
1854
    $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since";
1855
    $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created');
1856
 
1857
    return (bool)$reader->get_events_select_count($select, $params);
1858
}
1859
 
1860
/**
1861
 * Save the Your name for 'Some role' strings.
1862
 *
1863
 * @param integer $courseid the id of this course.
1864
 * @param array|stdClass $data the data that came from the course settings form.
1865
 */
1866
function save_local_role_names($courseid, $data) {
1867
    global $DB;
1868
    $context = context_course::instance($courseid);
1869
 
1870
    foreach ($data as $fieldname => $value) {
1871
        if (strpos($fieldname, 'role_') !== 0) {
1872
            continue;
1873
        }
1874
        list($ignored, $roleid) = explode('_', $fieldname);
1875
 
1876
        // make up our mind whether we want to delete, update or insert
1877
        if (!$value) {
1878
            $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid));
1879
 
1880
        } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) {
1881
            $rolename->name = $value;
1882
            $DB->update_record('role_names', $rolename);
1883
 
1884
        } else {
1885
            $rolename = new stdClass;
1886
            $rolename->contextid = $context->id;
1887
            $rolename->roleid = $roleid;
1888
            $rolename->name = $value;
1889
            $DB->insert_record('role_names', $rolename);
1890
        }
1891
        // This will ensure the course contacts cache is purged..
1892
        core_course_category::role_assignment_changed($roleid, $context);
1893
    }
1894
}
1895
 
1896
/**
1897
 * Returns options to use in course overviewfiles filemanager
1898
 *
1899
 * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id;
1900
 *     may be empty if course does not exist yet (course create form)
1901
 * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc.
1902
 *     or null if overviewfiles are disabled
1903
 */
1904
function course_overviewfiles_options($course) {
1905
    global $CFG;
1906
    if (empty($CFG->courseoverviewfileslimit)) {
1907
        return null;
1908
    }
1909
 
1910
    // Create accepted file types based on config value, falling back to default all.
1911
    $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext);
1912
    if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) {
1913
        $acceptedtypes = '*';
1914
    }
1915
 
1916
    $options = array(
1917
        'maxfiles' => $CFG->courseoverviewfileslimit,
1918
        'maxbytes' => $CFG->maxbytes,
1919
        'subdirs' => 0,
1920
        'accepted_types' => $acceptedtypes
1921
    );
1922
    if (!empty($course->id)) {
1923
        $options['context'] = context_course::instance($course->id);
1924
    } else if (is_int($course) && $course > 0) {
1925
        $options['context'] = context_course::instance($course);
1926
    }
1927
    return $options;
1928
}
1929
 
1930
/**
1931
 * Create a course and either return a $course object
1932
 *
1933
 * Please note this functions does not verify any access control,
1934
 * the calling code is responsible for all validation (usually it is the form definition).
1935
 *
1936
 * @param array $editoroptions course description editor options
1937
 * @param object $data  - all the data needed for an entry in the 'course' table
1938
 * @return object new course instance
1939
 */
1940
function create_course($data, $editoroptions = NULL) {
1941
    global $DB, $CFG;
1942
 
1943
    //check the categoryid - must be given for all new courses
1944
    $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
1945
 
1946
    // Check if the shortname already exists.
1947
    if (!empty($data->shortname)) {
1948
        if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
1949
            throw new moodle_exception('shortnametaken', '', '', $data->shortname);
1950
        }
1951
    }
1952
 
1953
    // Check if the idnumber already exists.
1954
    if (!empty($data->idnumber)) {
1955
        if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1956
            throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
1957
        }
1958
    }
1959
 
1960
    if (empty($CFG->enablecourserelativedates)) {
1961
        // Make sure we're not setting the relative dates mode when the setting is disabled.
1962
        unset($data->relativedatesmode);
1963
    }
1964
 
1965
    if ($errorcode = course_validate_dates((array)$data)) {
1966
        throw new moodle_exception($errorcode);
1967
    }
1968
 
1969
    // Check if timecreated is given.
1970
    $data->timecreated  = !empty($data->timecreated) ? $data->timecreated : time();
1971
    $data->timemodified = $data->timecreated;
1972
 
1973
    // place at beginning of any category
1974
    $data->sortorder = 0;
1975
 
1976
    if ($editoroptions) {
1977
        // summary text is updated later, we need context to store the files first
1978
        $data->summary = '';
1979
        $data->summary_format = $data->summary_editor['format'];
1980
    }
1981
 
1982
    // Get default completion settings as a fallback in case the enablecompletion field is not set.
1983
    $courseconfig = get_config('moodlecourse');
1984
    $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
1985
    $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
1986
    // Unset showcompletionconditions when completion tracking is not enabled for the course.
1987
    if ($enablecompletion == COMPLETION_DISABLED) {
1988
        unset($data->showcompletionconditions);
1989
    } else if (!isset($data->showcompletionconditions)) {
1990
        // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
1991
        // This scenario can happen when a course is created through data generators or through a web service.
1992
        $data->showcompletionconditions = $courseconfig->showcompletionconditions;
1993
    }
1994
 
1995
    if (!isset($data->visible)) {
1996
        // data not from form, add missing visibility info
1997
        $data->visible = $category->visible;
1998
    }
1999
    $data->visibleold = $data->visible;
2000
 
2001
    $newcourseid = $DB->insert_record('course', $data);
2002
    $context = context_course::instance($newcourseid, MUST_EXIST);
2003
 
2004
    if ($editoroptions) {
2005
        // Save the files used in the summary editor and store
2006
        $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2007
        $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid));
2008
        $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid));
2009
    }
2010
    if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) {
2011
        // Save the course overviewfiles
2012
        $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2013
    }
2014
 
2015
    // update course format options
2016
    course_get_format($newcourseid)->update_course_format_options($data);
2017
 
2018
    $course = course_get_format($newcourseid)->get_course();
2019
 
2020
    fix_course_sortorder();
2021
    // purge appropriate caches in case fix_course_sortorder() did not change anything
2022
    cache_helper::purge_by_event('changesincourse');
2023
 
2024
    // Trigger a course created event.
2025
    $event = \core\event\course_created::create(array(
2026
        'objectid' => $course->id,
2027
        'context' => $context,
2028
        'other' => array('shortname' => $course->shortname,
2029
            'fullname' => $course->fullname)
2030
    ));
2031
 
2032
    $event->trigger();
2033
 
2034
    $data->id = $newcourseid;
2035
 
2036
    // Dispatch the hook for post course create actions.
2037
    di::get(hook\manager::class)->dispatch(
2038
        new \core_course\hook\after_course_created(
2039
            course: $data,
2040
        ),
2041
    );
2042
 
2043
    // Setup the blocks
2044
    blocks_add_default_course_blocks($course);
2045
 
2046
    // Create default section and initial sections if specified (unless they've already been created earlier).
2047
    // We do not want to call course_create_sections_if_missing() because to avoid creating course cache.
2048
    $numsections = isset($data->numsections) ? $data->numsections : 0;
2049
    $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]);
2050
    $newsections = array_diff(range(0, $numsections), $existingsections);
2051
    foreach ($newsections as $sectionnum) {
2052
        course_create_section($newcourseid, $sectionnum, true);
2053
    }
2054
 
2055
    // Save any custom role names.
2056
    save_local_role_names($course->id, (array)$data);
2057
 
2058
    // set up enrolments
2059
    enrol_course_updated(true, $course, $data);
2060
 
2061
    // Update course tags.
2062
    if (isset($data->tags)) {
2063
        core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $data->tags);
2064
    }
2065
    // Save custom fields if there are any of them in the form.
2066
    $handler = core_course\customfield\course_handler::create();
2067
    // Make sure to set the handler's parent context first.
2068
    $coursecatcontext = context_coursecat::instance($category->id);
2069
    $handler->set_parent_context($coursecatcontext);
2070
    // Save the custom field data.
2071
    $data->id = $course->id;
2072
    $handler->instance_form_save($data, true);
2073
 
2074
    di::get(hook\manager::class)->dispatch(
2075
        new \core_course\hook\after_form_submission($data, true),
2076
    );
2077
 
2078
    return $course;
2079
}
2080
 
2081
/**
2082
 * Update a course.
2083
 *
2084
 * Please note this functions does not verify any access control,
2085
 * the calling code is responsible for all validation (usually it is the form definition).
2086
 *
2087
 * @param object $data  - all the data needed for an entry in the 'course' table
2088
 * @param array $editoroptions course description editor options
2089
 * @return void
2090
 */
2091
function update_course($data, $editoroptions = NULL) {
2092
    global $DB, $CFG;
2093
 
2094
    // Prevent changes on front page course.
2095
    if ($data->id == SITEID) {
2096
        throw new moodle_exception('invalidcourse', 'error');
2097
    }
2098
 
2099
    $oldcourse = course_get_format($data->id)->get_course();
2100
    $context   = context_course::instance($oldcourse->id);
2101
 
2102
    // Make sure we're not changing whatever the course's relativedatesmode setting is.
2103
    unset($data->relativedatesmode);
2104
 
2105
    // Capture the updated fields for the log data.
2106
    $updatedfields = [];
2107
    foreach (get_object_vars($oldcourse) as $field => $value) {
2108
        if ($field == 'summary_editor') {
2109
            if (($data->$field)['text'] !== $value['text']) {
2110
                // The summary might be very long, we don't wan't to fill up the log record with the full text.
2111
                $updatedfields[$field] = '(updated)';
2112
            }
2113
        } else if ($field == 'tags' && isset($data->tags)) {
2114
            // Tags might not have the same array keys, just check the values.
2115
            if (array_values($data->$field) !== array_values($value)) {
2116
                $updatedfields[$field] = $data->$field;
2117
            }
2118
        } else {
2119
            if (isset($data->$field) && $data->$field != $value) {
2120
                $updatedfields[$field] = $data->$field;
2121
            }
2122
        }
2123
    }
2124
 
2125
    $data->timemodified = time();
2126
 
2127
    if ($editoroptions) {
2128
        $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2129
    }
2130
    if ($overviewfilesoptions = course_overviewfiles_options($data->id)) {
2131
        $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2132
    }
2133
 
2134
    // Check we don't have a duplicate shortname.
2135
    if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
2136
        if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
2137
            throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2138
        }
2139
    }
2140
 
2141
    // Check we don't have a duplicate idnumber.
2142
    if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
2143
        if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
2144
            throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2145
        }
2146
    }
2147
 
2148
    if ($errorcode = course_validate_dates((array)$data)) {
2149
        throw new moodle_exception($errorcode);
2150
    }
2151
 
2152
    if (!isset($data->category) or empty($data->category)) {
2153
        // prevent nulls and 0 in category field
2154
        unset($data->category);
2155
    }
2156
    $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
2157
 
2158
    if (!isset($data->visible)) {
2159
        // data not from form, add missing visibility info
2160
        $data->visible = $oldcourse->visible;
2161
    }
2162
 
2163
    if ($data->visible != $oldcourse->visible) {
2164
        // reset the visibleold flag when manually hiding/unhiding course
2165
        $data->visibleold = $data->visible;
2166
        $changesincoursecat = true;
2167
    } else {
2168
        if ($movecat) {
2169
            $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
2170
            if (empty($newcategory->visible)) {
2171
                // make sure when moving into hidden category the course is hidden automatically
2172
                $data->visible = 0;
2173
            }
2174
        }
2175
    }
2176
 
2177
    // Set newsitems to 0 if format does not support announcements.
2178
    if (isset($data->format)) {
2179
        $newcourseformat = course_get_format((object)['format' => $data->format]);
2180
        if (!$newcourseformat->supports_news()) {
2181
            $data->newsitems = 0;
2182
        }
2183
    }
2184
 
2185
    // Set showcompletionconditions to null when completion tracking has been disabled for the course.
2186
    if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
2187
        $data->showcompletionconditions = null;
2188
    }
2189
    // Update custom fields if there are any of them in the form.
2190
    $handler = core_course\customfield\course_handler::create();
2191
    $handler->instance_form_save($data);
2192
 
2193
    di::get(hook\manager::class)->dispatch(
2194
        new \core_course\hook\after_form_submission($data),
2195
    );
2196
 
2197
    // Update with the new data
2198
    $DB->update_record('course', $data);
2199
    // make sure the modinfo cache is reset
2200
    rebuild_course_cache($data->id);
2201
 
2202
    // Purge course image cache in case if course image has been updated.
2203
    \cache::make('core', 'course_image')->delete($data->id);
2204
 
2205
    // update course format options with full course data
2206
    course_get_format($data->id)->update_course_format_options($data, $oldcourse);
2207
 
2208
    $course = $DB->get_record('course', array('id'=>$data->id));
2209
 
2210
    if ($movecat) {
2211
        $newparent = context_coursecat::instance($course->category);
2212
        $context->update_moved($newparent);
2213
    }
2214
    $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder));
2215
    if ($fixcoursesortorder) {
2216
        fix_course_sortorder();
2217
    }
2218
 
2219
    // purge appropriate caches in case fix_course_sortorder() did not change anything
2220
    cache_helper::purge_by_event('changesincourse');
2221
    if ($changesincoursecat) {
2222
        cache_helper::purge_by_event('changesincoursecat');
2223
    }
2224
 
2225
    // Test for and remove blocks which aren't appropriate anymore
2226
    blocks_remove_inappropriate($course);
2227
 
2228
    // Save any custom role names.
2229
    save_local_role_names($course->id, $data);
2230
 
2231
    // update enrol settings
2232
    enrol_course_updated(false, $course, $data);
2233
 
2234
    // Update course tags.
2235
    if (isset($data->tags)) {
2236
        core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2237
    }
2238
 
2239
    // Trigger a course updated event.
2240
    $event = \core\event\course_updated::create(array(
2241
        'objectid' => $course->id,
2242
        'context' => context_course::instance($course->id),
2243
        'other' => array('shortname' => $course->shortname,
2244
                         'fullname' => $course->fullname,
2245
                         'updatedfields' => $updatedfields)
2246
    ));
2247
 
2248
    $event->trigger();
2249
 
2250
    // Dispatch the hook for post course update actions.
2251
    $hook = new \core_course\hook\after_course_updated(
2252
        course: $data,
2253
        oldcourse: $oldcourse,
2254
        changeincoursecat: $changesincoursecat,
2255
    );
2256
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
2257
 
2258
    if ($oldcourse->format !== $course->format) {
2259
        // Remove all options stored for the previous format
2260
        // We assume that new course format migrated everything it needed watching trigger
2261
        // 'course_updated' and in method format_XXX::update_course_format_options()
2262
        $DB->delete_records('course_format_options',
2263
                array('courseid' => $course->id, 'format' => $oldcourse->format));
2264
    }
2265
 
2266
    // Delete theme usage cache if the theme has been changed.
2267
    if (isset($data->theme) && ($data->theme != $oldcourse->theme)) {
2268
        theme_delete_used_in_context_cache($data->theme, $oldcourse->theme);
2269
    }
2270
}
2271
 
2272
/**
2273
 * Calculate the average number of enrolled participants per course.
2274
 *
2275
 * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
2276
 * Front page enrolments are excluded.
2277
 *
2278
 * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
2279
 * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
2280
 * @return float
2281
 */
2282
function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
2283
    global $DB;
2284
 
2285
    $params = [];
2286
 
2287
    $sql = "SELECT DISTINCT ue.userid, e.courseid
2288
              FROM {user_enrolments} ue
2289
              JOIN {enrol} e ON e.id = ue.enrolid
2290
              JOIN {course} c ON c.id = e.courseid ";
2291
 
2292
    if ($onlyactive || $lastloginsince) {
2293
        $sql .= "JOIN {user} u ON u.id = ue.userid ";
2294
    }
2295
 
2296
    $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
2297
 
2298
    if ($onlyactive) {
2299
        $sql .= "AND ue.status = :active
2300
                 AND e.status = :enabled
2301
                 AND ue.timestart < :now1
2302
                 AND (ue.timeend = 0 OR ue.timeend > :now2) ";
2303
 
2304
        // Same as in the enrollib - the rounding should help caching in the database.
2305
        $now = round(time(), -2);
2306
 
2307
        $params += [
2308
            'active' => ENROL_USER_ACTIVE,
2309
            'enabled' => ENROL_INSTANCE_ENABLED,
2310
            'now1' => $now,
2311
            'now2' => $now,
2312
        ];
2313
    }
2314
 
2315
    if ($lastloginsince) {
2316
        $sql .= "AND u.lastlogin > :lastlogin ";
2317
        $params['lastlogin'] = $lastloginsince;
2318
    }
2319
 
2320
    $sql = "SELECT COUNT(*)
2321
              FROM ($sql) total";
2322
 
2323
    $enrolmenttotal = $DB->count_records_sql($sql, $params);
2324
 
2325
    // Get the number of visible courses (exclude the front page).
2326
    $coursetotal = $DB->count_records('course', ['visible' => 1]);
2327
    $coursetotal = $coursetotal - 1;
2328
 
2329
    if (empty($coursetotal)) {
2330
        $participantaverage = 0;
2331
 
2332
    } else {
2333
        $participantaverage = $enrolmenttotal / $coursetotal;
2334
    }
2335
 
2336
    return $participantaverage;
2337
}
2338
 
2339
/**
2340
 * Average number of course modules
2341
 * @return integer
2342
 */
2343
function average_number_of_courses_modules() {
2344
    global $DB, $SITE;
2345
 
2346
    //count total of visible course module (except front page)
2347
    $sql = 'SELECT COUNT(*) FROM (
2348
        SELECT cm.course, cm.module
2349
        FROM {course} c, {course_modules} cm
2350
        WHERE c.id = cm.course
2351
            AND c.id <> :siteid
2352
            AND cm.visible = 1
2353
            AND c.visible = 1) total';
2354
    $params = array('siteid' => $SITE->id);
2355
    $moduletotal = $DB->count_records_sql($sql, $params);
2356
 
2357
 
2358
    //count total of visible courses (minus front page)
2359
    $coursetotal = $DB->count_records('course', array('visible' => 1));
2360
    $coursetotal = $coursetotal - 1 ;
2361
 
2362
    //average of course module
2363
    if (empty($coursetotal)) {
2364
        $coursemoduleaverage = 0;
2365
    } else {
2366
        $coursemoduleaverage = $moduletotal / $coursetotal;
2367
    }
2368
 
2369
    return $coursemoduleaverage;
2370
}
2371
 
2372
/**
2373
 * This class pertains to course requests and contains methods associated with
2374
 * create, approving, and removing course requests.
2375
 *
2376
 * Please note we do not allow embedded images here because there is no context
2377
 * to store them with proper access control.
2378
 *
2379
 * @copyright 2009 Sam Hemelryk
2380
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2381
 * @since Moodle 2.0
2382
 *
2383
 * @property-read int $id
2384
 * @property-read string $fullname
2385
 * @property-read string $shortname
2386
 * @property-read string $summary
2387
 * @property-read int $summaryformat
2388
 * @property-read int $summarytrust
2389
 * @property-read string $reason
2390
 * @property-read int $requester
2391
 */
2392
class course_request {
2393
 
2394
    /**
2395
     * This is the stdClass that stores the properties for the course request
2396
     * and is externally accessed through the __get magic method
2397
     * @var stdClass
2398
     */
2399
    protected $properties;
2400
 
2401
    /**
2402
     * An array of options for the summary editor used by course request forms.
2403
     * This is initially set by {@link summary_editor_options()}
2404
     * @var array
2405
     * @static
2406
     */
2407
    protected static $summaryeditoroptions;
2408
 
2409
    /**
2410
     * Static function to prepare the summary editor for working with a course
2411
     * request.
2412
     *
2413
     * @static
2414
     * @param null|stdClass $data Optional, an object containing the default values
2415
     *                       for the form, these may be modified when preparing the
2416
     *                       editor so this should be called before creating the form
2417
     * @return stdClass An object that can be used to set the default values for
2418
     *                   an mforms form
2419
     */
2420
    public static function prepare($data=null) {
2421
        if ($data === null) {
2422
            $data = new stdClass;
2423
        }
2424
        $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options());
2425
        return $data;
2426
    }
2427
 
2428
    /**
2429
     * Static function to create a new course request when passed an array of properties
2430
     * for it.
2431
     *
2432
     * This function also handles saving any files that may have been used in the editor
2433
     *
2434
     * @static
2435
     * @param stdClass $data
2436
     * @return course_request The newly created course request
2437
     */
2438
    public static function create($data) {
2439
        global $USER, $DB, $CFG;
2440
        $data->requester = $USER->id;
2441
 
2442
        // Setting the default category if none set.
2443
        if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
2444
            $data->category = $CFG->defaultrequestcategory;
2445
        }
2446
 
2447
        // Summary is a required field so copy the text over
2448
        $data->summary       = $data->summary_editor['text'];
2449
        $data->summaryformat = $data->summary_editor['format'];
2450
 
2451
        $data->id = $DB->insert_record('course_request', $data);
2452
 
2453
        // Create a new course_request object and return it
2454
        $request = new course_request($data);
2455
 
2456
        // Notify the admin if required.
2457
        if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) {
2458
 
2459
            $a = new stdClass;
2460
            $a->link = "$CFG->wwwroot/course/pending.php";
2461
            $a->user = fullname($USER);
2462
            $subject = get_string('courserequest');
2463
            $message = get_string('courserequestnotifyemail', 'admin', $a);
2464
            foreach ($users as $user) {
2465
                $request->notify($user, $USER, 'courserequested', $subject, $message);
2466
            }
2467
        }
2468
 
2469
        return $request;
2470
    }
2471
 
2472
    /**
2473
     * Returns an array of options to use with a summary editor
2474
     *
2475
     * @uses course_request::$summaryeditoroptions
2476
     * @return array An array of options to use with the editor
2477
     */
2478
    public static function summary_editor_options() {
2479
        global $CFG;
2480
        if (self::$summaryeditoroptions === null) {
2481
            self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0);
2482
        }
2483
        return self::$summaryeditoroptions;
2484
    }
2485
 
2486
    /**
2487
     * Loads the properties for this course request object. Id is required and if
2488
     * only id is provided then we load the rest of the properties from the database
2489
     *
2490
     * @param stdClass|int $properties Either an object containing properties
2491
     *                      or the course_request id to load
2492
     */
2493
    public function __construct($properties) {
2494
        global $DB;
2495
        if (empty($properties->id)) {
2496
            if (empty($properties)) {
2497
                throw new coding_exception('You must provide a course request id when creating a course_request object');
2498
            }
2499
            $id = $properties;
2500
            $properties = new stdClass;
2501
            $properties->id = (int)$id;
2502
            unset($id);
2503
        }
2504
        if (empty($properties->requester)) {
2505
            if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) {
2506
                throw new \moodle_exception('unknowncourserequest');
2507
            }
2508
        } else {
2509
            $this->properties = $properties;
2510
        }
2511
        $this->properties->collision = null;
2512
    }
2513
 
2514
    /**
2515
     * Returns the requested property
2516
     *
2517
     * @param string $key
2518
     * @return mixed
2519
     */
2520
    public function __get($key) {
2521
        return $this->properties->$key;
2522
    }
2523
 
2524
    /**
2525
     * Override this to ensure empty($request->blah) calls return a reliable answer...
2526
     *
2527
     * This is required because we define the __get method
2528
     *
2529
     * @param mixed $key
2530
     * @return bool True is it not empty, false otherwise
2531
     */
2532
    public function __isset($key) {
2533
        return (!empty($this->properties->$key));
2534
    }
2535
 
2536
    /**
2537
     * Returns the user who requested this course
2538
     *
2539
     * Uses a static var to cache the results and cut down the number of db queries
2540
     *
2541
     * @staticvar array $requesters An array of cached users
2542
     * @return stdClass The user who requested the course
2543
     */
2544
    public function get_requester() {
2545
        global $DB;
2546
        static $requesters= array();
2547
        if (!array_key_exists($this->properties->requester, $requesters)) {
2548
            $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester));
2549
        }
2550
        return $requesters[$this->properties->requester];
2551
    }
2552
 
2553
    /**
2554
     * Checks that the shortname used by the course does not conflict with any other
2555
     * courses that exist
2556
     *
2557
     * @param string|null $shortnamemark The string to append to the requests shortname
2558
     *                     should a conflict be found
2559
     * @return bool true is there is a conflict, false otherwise
2560
     */
2561
    public function check_shortname_collision($shortnamemark = '[*]') {
2562
        global $DB;
2563
 
2564
        if ($this->properties->collision !== null) {
2565
            return $this->properties->collision;
2566
        }
2567
 
2568
        if (empty($this->properties->shortname)) {
2569
            debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER);
2570
            $this->properties->collision = false;
2571
        } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) {
2572
            if (!empty($shortnamemark)) {
2573
                $this->properties->shortname .= ' '.$shortnamemark;
2574
            }
2575
            $this->properties->collision = true;
2576
        } else {
2577
            $this->properties->collision = false;
2578
        }
2579
        return $this->properties->collision;
2580
    }
2581
 
2582
    /**
2583
     * Checks user capability to approve a requested course
2584
     *
2585
     * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
2586
     * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
2587
     *
2588
     * @return bool
2589
     */
2590
    public function can_approve() {
2591
        global $CFG;
2592
        $category = null;
2593
        if ($this->properties->category) {
2594
            $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
2595
        } else if ($CFG->defaultrequestcategory) {
2596
            $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
2597
        }
2598
        if ($category) {
2599
            return has_capability('moodle/site:approvecourse', $category->get_context());
2600
        }
2601
 
2602
        // We can not determine the context where the course should be created. The approver should have
2603
        // both capabilities to approve courses and change course category in the system context.
2604
        return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
2605
    }
2606
 
2607
    /**
2608
     * Returns the category where this course request should be created
2609
     *
2610
     * Note that we don't check here that user has a capability to view
2611
     * hidden categories if he has capabilities 'moodle/site:approvecourse' and
2612
     * 'moodle/course:changecategory'
2613
     *
2614
     * @return core_course_category
2615
     */
2616
    public function get_category() {
2617
        global $CFG;
2618
        if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
2619
            return $category;
2620
        } else if ($CFG->defaultrequestcategory &&
2621
                ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
2622
            return $category;
2623
        } else {
2624
            return core_course_category::get_default();
2625
        }
2626
    }
2627
 
2628
    /**
2629
     * This function approves the request turning it into a course
2630
     *
2631
     * This function converts the course request into a course, at the same time
2632
     * transferring any files used in the summary to the new course and then removing
2633
     * the course request and the files associated with it.
2634
     *
2635
     * @return int The id of the course that was created from this request
2636
     */
2637
    public function approve() {
2638
        global $CFG, $DB, $USER;
2639
 
2640
        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2641
 
2642
        $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
2643
 
2644
        $courseconfig = get_config('moodlecourse');
2645
 
2646
        // Transfer appropriate settings
2647
        $data = clone($this->properties);
2648
        unset($data->id);
2649
        unset($data->reason);
2650
        unset($data->requester);
2651
 
2652
        // Set category
2653
        $category = $this->get_category();
2654
        $data->category = $category->id;
2655
        // Set misc settings
2656
        $data->requested = 1;
2657
 
2658
        // Apply course default settings
2659
        $data->format             = $courseconfig->format;
2660
        $data->newsitems          = $courseconfig->newsitems;
2661
        $data->showgrades         = $courseconfig->showgrades;
2662
        $data->showreports        = $courseconfig->showreports;
2663
        $data->maxbytes           = $courseconfig->maxbytes;
2664
        $data->groupmode          = $courseconfig->groupmode;
2665
        $data->groupmodeforce     = $courseconfig->groupmodeforce;
2666
        $data->visible            = $courseconfig->visible;
2667
        $data->visibleold         = $data->visible;
2668
        $data->lang               = $courseconfig->lang;
2669
        $data->enablecompletion   = $courseconfig->enablecompletion;
2670
        $data->numsections        = $courseconfig->numsections;
2671
        $data->startdate          = usergetmidnight(time());
2672
        if ($courseconfig->courseenddateenabled) {
2673
            $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
2674
        }
2675
 
2676
        list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
2677
 
2678
        $course = create_course($data);
2679
        $context = context_course::instance($course->id, MUST_EXIST);
2680
 
2681
        // add enrol instances
2682
        if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) {
2683
            if ($manual = enrol_get_plugin('manual')) {
2684
                $manual->add_default_instance($course);
2685
            }
2686
        }
2687
 
2688
        // enrol the requester as teacher if necessary
2689
        if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) {
2690
            enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid);
2691
        }
2692
 
2693
        $this->delete();
2694
 
2695
        $a = new stdClass();
2696
        $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
2697
        $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
2698
        $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
2699
 
2700
        return $course->id;
2701
    }
2702
 
2703
    /**
2704
     * Reject a course request
2705
     *
2706
     * This function rejects a course request, emailing the requesting user the
2707
     * provided notice and then removing the request from the database
2708
     *
2709
     * @param string $notice The message to display to the user
2710
     */
2711
    public function reject($notice) {
2712
        global $USER, $DB;
2713
        $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST);
2714
        $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice));
2715
        $this->delete();
2716
    }
2717
 
2718
    /**
2719
     * Deletes the course request and any associated files
2720
     */
2721
    public function delete() {
2722
        global $DB;
2723
        $DB->delete_records('course_request', array('id' => $this->properties->id));
2724
    }
2725
 
2726
    /**
2727
     * Send a message from one user to another using events_trigger
2728
     *
2729
     * @param object $touser
2730
     * @param object $fromuser
2731
     * @param string $name
2732
     * @param string $subject
2733
     * @param string $message
2734
     * @param int|null $courseid
2735
     */
2736
    protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) {
2737
        $eventdata = new \core\message\message();
2738
        $eventdata->courseid          = empty($courseid) ? SITEID : $courseid;
2739
        $eventdata->component         = 'moodle';
2740
        $eventdata->name              = $name;
2741
        $eventdata->userfrom          = $fromuser;
2742
        $eventdata->userto            = $touser;
2743
        $eventdata->subject           = $subject;
2744
        $eventdata->fullmessage       = $message;
2745
        $eventdata->fullmessageformat = FORMAT_PLAIN;
2746
        $eventdata->fullmessagehtml   = '';
2747
        $eventdata->smallmessage      = '';
2748
        $eventdata->notification      = 1;
2749
        message_send($eventdata);
2750
    }
2751
 
2752
    /**
2753
     * Checks if current user can request a course in this context
2754
     *
2755
     * @param context $context
2756
     * @return bool
2757
     */
2758
    public static function can_request(context $context) {
2759
        global $CFG;
2760
        if (empty($CFG->enablecourserequests)) {
2761
            return false;
2762
        }
2763
        if (has_capability('moodle/course:create', $context)) {
2764
            return false;
2765
        }
2766
 
2767
        if ($context instanceof context_system) {
2768
            $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
2769
            return $defaultcontext &&
2770
                has_capability('moodle/course:request', $defaultcontext);
2771
        } else if ($context instanceof context_coursecat) {
2772
            if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
2773
                return has_capability('moodle/course:request', $context);
2774
            }
2775
        }
2776
        return false;
2777
    }
2778
}
2779
 
2780
/**
2781
 * Return a list of page types
2782
 * @param string $pagetype current page type
2783
 * @param context $parentcontext Block's parent context
2784
 * @param context $currentcontext Current context of block
2785
 * @return array array of page types
2786
 */
2787
function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
2788
    if ($pagetype === 'course-index' || $pagetype === 'course-index-category') {
2789
        // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page
2790
        $pagetypes = array('*' => get_string('page-x', 'pagetype'),
2791
            'course-index-*' => get_string('page-course-index-x', 'pagetype'),
2792
        );
2793
    } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) {
2794
        // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.)
2795
        $pagetypes = array('*' => get_string('page-x', 'pagetype'));
2796
    } else {
2797
        // Otherwise consider it a page inside a course even if $currentcontext is null
2798
        $pagetypes = array('*' => get_string('page-x', 'pagetype'),
2799
            'course-*' => get_string('page-course-x', 'pagetype'),
2800
            'course-view-*' => get_string('page-course-view-x', 'pagetype')
2801
        );
2802
    }
2803
    return $pagetypes;
2804
}
2805
 
2806
/**
2807
 * Determine whether course ajax should be enabled for the specified course
2808
 *
2809
 * @param stdClass $course The course to test against
2810
 * @return boolean Whether course ajax is enabled or note
2811
 */
2812
function course_ajax_enabled($course) {
2813
    global $CFG, $PAGE, $SITE;
2814
 
2815
    // The user must be editing for AJAX to be included
2816
    if (!$PAGE->user_is_editing()) {
2817
        return false;
2818
    }
2819
 
2820
    // Check that the theme suports
2821
    if (!$PAGE->theme->enablecourseajax) {
2822
        return false;
2823
    }
2824
 
2825
    // Check that the course format supports ajax functionality
2826
    // The site 'format' doesn't have information on course format support
2827
    if ($SITE->id !== $course->id) {
2828
        $courseformatajaxsupport = course_format_ajax_support($course->format);
2829
        if (!$courseformatajaxsupport->capable) {
2830
            return false;
2831
        }
2832
    }
2833
 
2834
    // All conditions have been met so course ajax should be enabled
2835
    return true;
2836
}
2837
 
2838
/**
2839
 * Include the relevant javascript and language strings for the resource
2840
 * toolbox YUI module
2841
 *
2842
 * @param integer $id The ID of the course being applied to
2843
 * @param array $usedmodules An array containing the names of the modules in use on the page
2844
 * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
2845
 * @param stdClass $config An object containing configuration parameters for ajax modules including:
2846
 *          * resourceurl   The URL to post changes to for resource changes
2847
 *          * sectionurl    The URL to post changes to for section changes
2848
 *          * pageparams    Additional parameters to pass through in the post
2849
 * @return bool
2850
 */
2851
function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
2852
    global $CFG, $PAGE, $SITE;
2853
 
2854
    // Init the course editor module to support UI components.
2855
    $format = course_get_format($course);
2856
    include_course_editor($format);
2857
 
2858
    // Ensure that ajax should be included
2859
    if (!course_ajax_enabled($course)) {
2860
        return false;
2861
    }
2862
 
2863
    // Component based formats don't use YUI drag and drop anymore.
2864
    if (!$format->supports_components() && course_format_uses_sections($course->format)) {
2865
 
2866
        if (!$config) {
2867
            $config = new stdClass();
2868
        }
2869
 
2870
        // The URL to use for resource changes.
2871
        if (!isset($config->resourceurl)) {
2872
            $config->resourceurl = '/course/rest.php';
2873
        }
2874
 
2875
        // The URL to use for section changes.
2876
        if (!isset($config->sectionurl)) {
2877
            $config->sectionurl = '/course/rest.php';
2878
        }
2879
 
2880
        // Any additional parameters which need to be included on page submission.
2881
        if (!isset($config->pageparams)) {
2882
            $config->pageparams = array();
2883
        }
2884
 
2885
        $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
2886
            array(array(
2887
                'courseid' => $course->id,
2888
                'ajaxurl' => $config->sectionurl,
2889
                'config' => $config,
2890
            )), null, true);
2891
 
2892
        $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop',
2893
            array(array(
2894
                'courseid' => $course->id,
2895
                'ajaxurl' => $config->resourceurl,
2896
                'config' => $config,
2897
            )), null, true);
2898
 
2899
        // Require various strings for the command toolbox.
2900
        $PAGE->requires->strings_for_js(array(
2901
            'moveleft',
2902
            'deletechecktype',
2903
            'deletechecktypename',
2904
            'edittitle',
2905
            'edittitleinstructions',
2906
            'show',
2907
            'hide',
2908
            'highlight',
2909
            'highlightoff',
2910
            'groupsnone',
2911
            'groupsvisible',
2912
            'groupsseparate',
2913
            'movesection',
2914
            'movecoursemodule',
2915
            'movecoursesection',
2916
            'movecontent',
2917
            'tocontent',
2918
            'emptydragdropregion',
2919
            'afterresource',
2920
            'aftersection',
2921
            'totopofsection',
2922
        ), 'moodle');
2923
 
2924
        // Include section-specific strings for formats which support sections.
2925
        if (course_format_uses_sections($course->format)) {
2926
            $PAGE->requires->strings_for_js(array(
2927
                    'showfromothers',
2928
                    'hidefromothers',
2929
                ), 'format_' . $course->format);
2930
        }
2931
 
2932
        // For confirming resource deletion we need the name of the module in question.
2933
        foreach ($usedmodules as $module => $modname) {
2934
            $PAGE->requires->string_for_js('pluginname', $module);
2935
        }
2936
 
2937
        // Load drag and drop upload AJAX.
2938
        require_once($CFG->dirroot.'/course/dnduploadlib.php');
2939
        dndupload_add_to_course($course, $enabledmodules);
2940
    }
2941
 
2942
    $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
2943
 
2944
    return true;
2945
}
2946
 
2947
/**
2948
 * Include and configure the course editor modules.
2949
 *
2950
 * @param course_format $format the course format instance.
2951
 */
2952
function include_course_editor(course_format $format) {
2953
    global $PAGE, $SITE;
2954
 
2955
    $course = $format->get_course();
2956
 
2957
    if ($SITE->id === $course->id) {
2958
        return;
2959
    }
2960
 
2961
    $statekey = course_format::session_cache($course);
2962
 
2963
    // Edition mode and some format specs must be passed to the init method.
2964
    $setup = (object)[
2965
        'editing' => $format->show_editor(),
2966
        'supportscomponents' => $format->supports_components(),
2967
        'statekey' => $statekey,
2968
        'overriddenStrings' => $format->get_editor_custom_strings(),
2969
    ];
2970
    // All the new editor elements will be loaded after the course is presented and
2971
    // the initial course state will be generated using core_course_get_state webservice.
2972
    $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]);
2973
}
2974
 
2975
/**
2976
 * Returns the sorted list of available course formats, filtered by enabled if necessary
2977
 *
2978
 * @param bool $enabledonly return only formats that are enabled
2979
 * @return array array of sorted format names
2980
 */
2981
function get_sorted_course_formats($enabledonly = false) {
2982
    global $CFG;
2983
    $formats = core_component::get_plugin_list('format');
2984
 
2985
    if (!empty($CFG->format_plugins_sortorder)) {
2986
        $order = explode(',', $CFG->format_plugins_sortorder);
2987
        $order = array_merge(array_intersect($order, array_keys($formats)),
2988
                    array_diff(array_keys($formats), $order));
2989
    } else {
2990
        $order = array_keys($formats);
2991
    }
2992
    if (!$enabledonly) {
2993
        return $order;
2994
    }
2995
    $sortedformats = array();
2996
    foreach ($order as $formatname) {
2997
        if (!get_config('format_'.$formatname, 'disabled')) {
2998
            $sortedformats[] = $formatname;
2999
        }
3000
    }
3001
    return $sortedformats;
3002
}
3003
 
3004
/**
3005
 * The URL to use for the specified course (with section)
3006
 *
3007
 * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3008
 * @param int|stdClass $section Section object from database or just field course_sections.section
3009
 *     if omitted the course view page is returned
3010
 * @param array $options options for view URL. At the moment core uses:
3011
 *     'navigation' (bool) if true and section has no separate page, the function returns null
3012
 *     'sr' (int) used by multipage formats to specify to which section to return
3013
 * @return moodle_url The url of course
3014
 */
3015
function course_get_url($courseorid, $section = null, $options = array()) {
3016
    return course_get_format($courseorid)->get_view_url($section, $options);
3017
}
3018
 
3019
/**
3020
 * Create a module.
3021
 *
3022
 * It includes:
3023
 *      - capability checks and other checks
3024
 *      - create the module from the module info
3025
 *
3026
 * @param object $module
3027
 * @return object the created module info
3028
 * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
3029
 */
3030
function create_module($moduleinfo) {
3031
    global $DB, $CFG;
3032
 
3033
    require_once($CFG->dirroot . '/course/modlib.php');
3034
 
3035
    // Check manadatory attributs.
3036
    $mandatoryfields = array('modulename', 'course', 'section', 'visible');
3037
    if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) {
3038
        $mandatoryfields[] = 'introeditor';
3039
    }
3040
    foreach($mandatoryfields as $mandatoryfield) {
3041
        if (!isset($moduleinfo->{$mandatoryfield})) {
3042
            throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield);
3043
        }
3044
    }
3045
 
3046
    // Some additional checks (capability / existing instances).
3047
    $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST);
3048
    list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section);
3049
 
3050
    // Add the module.
3051
    $moduleinfo->module = $module->id;
3052
    $moduleinfo = add_moduleinfo($moduleinfo, $course, null);
3053
 
3054
    return $moduleinfo;
3055
}
3056
 
3057
/**
3058
 * Update a module.
3059
 *
3060
 * It includes:
3061
 *      - capability and other checks
3062
 *      - update the module
3063
 *
3064
 * @param object $module
3065
 * @return object the updated module info
3066
 * @throws moodle_exception if current user is not allowed to update the module
3067
 */
3068
function update_module($moduleinfo) {
3069
    global $DB, $CFG;
3070
 
3071
    require_once($CFG->dirroot . '/course/modlib.php');
3072
 
3073
    // Check the course module exists.
3074
    $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST);
3075
 
3076
    // Check the course exists.
3077
    $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
3078
 
3079
    // Some checks (capaibility / existing instances).
3080
    list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm);
3081
 
3082
    // Retrieve few information needed by update_moduleinfo.
3083
    $moduleinfo->modulename = $cm->modname;
3084
    if (!isset($moduleinfo->scale)) {
3085
        $moduleinfo->scale = 0;
3086
    }
3087
    $moduleinfo->type = 'mod';
3088
 
3089
    // Update the module.
3090
    list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null);
3091
 
3092
    return $moduleinfo;
3093
}
3094
 
3095
/**
3096
 * Duplicate a module on the course for ajax.
3097
 *
3098
 * @see mod_duplicate_module()
3099
 * @param object $course The course
3100
 * @param object $cm The course module to duplicate
3101
 * @param int $sr The section to link back to (used for creating the links)
3102
 * @throws moodle_exception if the plugin doesn't support duplication
3103
 * @return stdClass Object containing:
3104
 * - fullcontent: The HTML markup for the created CM
3105
 * - cmid: The CMID of the newly created CM
3106
 * - redirect: Whether to trigger a redirect following this change
3107
 */
3108
function mod_duplicate_activity($course, $cm, $sr = null) {
3109
    global $PAGE;
3110
 
3111
    $newcm = duplicate_module($course, $cm);
3112
 
3113
    $resp = new stdClass();
3114
    if ($newcm) {
3115
 
3116
        $format = course_get_format($course);
3117
        $renderer = $format->get_renderer($PAGE);
3118
        $modinfo = $format->get_modinfo();
3119
        $section = $modinfo->get_section_info($newcm->sectionnum);
3120
 
3121
        // Get the new element html content.
3122
        $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm);
3123
 
3124
        $resp->cmid = $newcm->id;
3125
    } else {
3126
        // Trigger a redirect.
3127
        $resp->redirect = true;
3128
    }
3129
    return $resp;
3130
}
3131
 
3132
/**
3133
 * Api to duplicate a module.
3134
 *
3135
 * @param object $course course object.
3136
 * @param object $cm course module object to be duplicated.
3137
 * @param int $sectionid section ID new course module will be placed in.
3138
 * @param bool $changename updates module name with text from duplicatedmodule lang string.
3139
 * @since Moodle 2.8
3140
 *
3141
 * @throws Exception
3142
 * @throws coding_exception
3143
 * @throws moodle_exception
3144
 * @throws restore_controller_exception
3145
 *
3146
 * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
3147
 */
3148
function duplicate_module($course, $cm, int $sectionid = null, bool $changename = true): ?cm_info {
3149
    global $CFG, $DB, $USER;
3150
    require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
3151
    require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
3152
    require_once($CFG->libdir . '/filelib.php');
3153
 
3154
    $a          = new stdClass();
3155
    $a->modtype = get_string('modulename', $cm->modname);
3156
    $a->modname = format_string($cm->name);
3157
 
3158
    if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
3159
        throw new moodle_exception('duplicatenosupport', 'error', '', $a);
3160
    }
3161
 
3162
    // Backup the activity.
3163
 
3164
    $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
3165
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
3166
 
3167
    $backupid       = $bc->get_backupid();
3168
    $backupbasepath = $bc->get_plan()->get_basepath();
3169
 
3170
    $bc->execute_plan();
3171
 
3172
    $bc->destroy();
3173
 
3174
    // Restore the backup immediately.
3175
 
3176
    $rc = new restore_controller($backupid, $course->id,
3177
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
3178
 
3179
    // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
3180
    $plan = $rc->get_plan();
3181
    $groupsetting = $plan->get_setting('groups');
3182
    if (empty($groupsetting->get_value())) {
3183
        $groupsetting->set_value(true);
3184
    }
3185
 
3186
    $cmcontext = context_module::instance($cm->id);
3187
    if (!$rc->execute_precheck()) {
3188
        $precheckresults = $rc->get_precheck_results();
3189
        if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
3190
            if (empty($CFG->keeptempdirectoriesonbackup)) {
3191
                fulldelete($backupbasepath);
3192
            }
3193
        }
3194
    }
3195
 
3196
    $rc->execute_plan();
3197
 
3198
    // Now a bit hacky part follows - we try to get the cmid of the newly
3199
    // restored copy of the module.
3200
    $newcmid = null;
3201
    $tasks = $rc->get_plan()->get_tasks();
3202
    foreach ($tasks as $task) {
3203
        if (is_subclass_of($task, 'restore_activity_task')) {
3204
            if ($task->get_old_contextid() == $cmcontext->id) {
3205
                $newcmid = $task->get_moduleid();
3206
                break;
3207
            }
3208
        }
3209
    }
3210
 
3211
    $rc->destroy();
3212
 
3213
    if (empty($CFG->keeptempdirectoriesonbackup)) {
3214
        fulldelete($backupbasepath);
3215
    }
3216
 
3217
    // If we know the cmid of the new course module, let us move it
3218
    // right below the original one. otherwise it will stay at the
3219
    // end of the section.
3220
    if ($newcmid) {
3221
        // Proceed with activity renaming before everything else. We don't use APIs here to avoid
3222
        // triggering a lot of create/update duplicated events.
3223
        $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
3224
        if ($changename) {
3225
            // Add ' (copy)' language string postfix to duplicated module.
3226
            $newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
3227
            set_coursemodule_name($newcm->id, $newname);
3228
        }
3229
 
3230
        $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]);
3231
        if (isset($sectionid)) {
3232
            moveto_module($newcm, $section);
3233
        } else {
3234
            $modarray = explode(",", trim($section->sequence));
3235
            $cmindex = array_search($cm->id, $modarray);
3236
            if ($cmindex !== false && $cmindex < count($modarray) - 1) {
3237
                moveto_module($newcm, $section, $modarray[$cmindex + 1]);
3238
            }
3239
        }
3240
 
3241
        // Update calendar events with the duplicated module.
3242
        // The following line is to be removed in MDL-58906.
3243
        course_module_update_calendar_events($newcm->modname, null, $newcm);
3244
 
3245
        // Trigger course module created event. We can trigger the event only if we know the newcmid.
3246
        $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
3247
        $event = \core\event\course_module_created::create_from_cm($newcm);
3248
        $event->trigger();
3249
    }
3250
 
3251
    return isset($newcm) ? $newcm : null;
3252
}
3253
 
3254
/**
3255
 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3256
 * Sorts by descending order of time.
3257
 *
3258
 * @param stdClass $a First object
3259
 * @param stdClass $b Second object
3260
 * @return int 0,1,-1 representing the order
3261
 */
3262
function compare_activities_by_time_desc($a, $b) {
3263
    // Make sure the activities actually have a timestamp property.
3264
    if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3265
        return 0;
3266
    }
3267
    // We treat instances without timestamp as if they have a timestamp of 0.
3268
    if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) {
3269
        return 1;
3270
    }
3271
    if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3272
        return -1;
3273
    }
3274
    if ($a->timestamp == $b->timestamp) {
3275
        return 0;
3276
    }
3277
    return ($a->timestamp > $b->timestamp) ? -1 : 1;
3278
}
3279
 
3280
/**
3281
 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3282
 * Sorts by ascending order of time.
3283
 *
3284
 * @param stdClass $a First object
3285
 * @param stdClass $b Second object
3286
 * @return int 0,1,-1 representing the order
3287
 */
3288
function compare_activities_by_time_asc($a, $b) {
3289
    // Make sure the activities actually have a timestamp property.
3290
    if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3291
      return 0;
3292
    }
3293
    // We treat instances without timestamp as if they have a timestamp of 0.
3294
    if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) {
3295
        return -1;
3296
    }
3297
    if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3298
        return 1;
3299
    }
3300
    if ($a->timestamp == $b->timestamp) {
3301
        return 0;
3302
    }
3303
    return ($a->timestamp < $b->timestamp) ? -1 : 1;
3304
}
3305
 
3306
/**
3307
 * Changes the visibility of a course.
3308
 *
3309
 * @param int $courseid The course to change.
3310
 * @param bool $show True to make it visible, false otherwise.
3311
 * @return bool
3312
 */
3313
function course_change_visibility($courseid, $show = true) {
3314
    $course = new stdClass;
3315
    $course->id = $courseid;
3316
    $course->visible = ($show) ? '1' : '0';
3317
    $course->visibleold = $course->visible;
3318
    update_course($course);
3319
    return true;
3320
}
3321
 
3322
/**
3323
 * Changes the course sortorder by one, moving it up or down one in respect to sort order.
3324
 *
3325
 * @param stdClass|core_course_list_element $course
3326
 * @param bool $up If set to true the course will be moved up one. Otherwise down one.
3327
 * @return bool
3328
 */
3329
function course_change_sortorder_by_one($course, $up) {
3330
    global $DB;
3331
    $params = array($course->sortorder, $course->category);
3332
    if ($up) {
3333
        $select = 'sortorder < ? AND category = ?';
3334
        $sort = 'sortorder DESC';
3335
    } else {
3336
        $select = 'sortorder > ? AND category = ?';
3337
        $sort = 'sortorder ASC';
3338
    }
3339
    fix_course_sortorder();
3340
    $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
3341
    if ($swapcourse) {
3342
        $swapcourse = reset($swapcourse);
3343
        $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
3344
        $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
3345
        // Finally reorder courses.
3346
        fix_course_sortorder();
3347
        cache_helper::purge_by_event('changesincourse');
3348
        return true;
3349
    }
3350
    return false;
3351
}
3352
 
3353
/**
3354
 * Changes the sort order of courses in a category so that the first course appears after the second.
3355
 *
3356
 * @param int|stdClass $courseorid The course to focus on.
3357
 * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
3358
 * @return bool
3359
 */
3360
function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
3361
    global $DB;
3362
 
3363
    if (!is_object($courseorid)) {
3364
        $course = get_course($courseorid);
3365
    } else {
3366
        $course = $courseorid;
3367
    }
3368
 
3369
    if ((int)$moveaftercourseid === 0) {
3370
        // We've moving the course to the start of the queue.
3371
        $sql = 'SELECT sortorder
3372
                      FROM {course}
3373
                     WHERE category = :categoryid
3374
                  ORDER BY sortorder';
3375
        $params = array(
3376
            'categoryid' => $course->category
3377
        );
3378
        $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
3379
 
3380
        $sql = 'UPDATE {course}
3381
                   SET sortorder = sortorder + 1
3382
                 WHERE category = :categoryid
3383
                   AND id <> :id';
3384
        $params = array(
3385
            'categoryid' => $course->category,
3386
            'id' => $course->id,
3387
        );
3388
        $DB->execute($sql, $params);
3389
        $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
3390
    } else if ($course->id === $moveaftercourseid) {
3391
        // They're the same - moronic.
3392
        debugging("Invalid move after course given.", DEBUG_DEVELOPER);
3393
        return false;
3394
    } else {
3395
        // Moving this course after the given course. It could be before it could be after.
3396
        $moveaftercourse = get_course($moveaftercourseid);
3397
        if ($course->category !== $moveaftercourse->category) {
3398
            debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
3399
            return false;
3400
        }
3401
        // Increment all courses in the same category that are ordered after the moveafter course.
3402
        // This makes a space for the course we're moving.
3403
        $sql = 'UPDATE {course}
3404
                       SET sortorder = sortorder + 1
3405
                     WHERE category = :categoryid
3406
                       AND sortorder > :sortorder';
3407
        $params = array(
3408
            'categoryid' => $moveaftercourse->category,
3409
            'sortorder' => $moveaftercourse->sortorder
3410
        );
3411
        $DB->execute($sql, $params);
3412
        $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
3413
    }
3414
    fix_course_sortorder();
3415
    cache_helper::purge_by_event('changesincourse');
3416
    return true;
3417
}
3418
 
3419
/**
3420
 * Trigger course viewed event. This API function is used when course view actions happens,
3421
 * usually in course/view.php but also in external functions.
3422
 *
3423
 * @param stdClass  $context course context object
3424
 * @param int $sectionnumber section number
3425
 * @since Moodle 2.9
3426
 */
3427
function course_view($context, $sectionnumber = 0) {
3428
 
3429
    $eventdata = array('context' => $context);
3430
 
3431
    if (!empty($sectionnumber)) {
3432
        $eventdata['other']['coursesectionnumber'] = $sectionnumber;
3433
    }
3434
 
3435
    $event = \core\event\course_viewed::create($eventdata);
3436
    $event->trigger();
3437
 
3438
    user_accesstime_log($context->instanceid);
3439
}
3440
 
3441
/**
3442
 * Returns courses tagged with a specified tag.
3443
 *
3444
 * @param core_tag_tag $tag
3445
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3446
 *             are displayed on the page and the per-page limit may be bigger
3447
 * @param int $fromctx context id where the link was displayed, may be used by callbacks
3448
 *            to display items in the same context first
3449
 * @param int $ctx context id where to search for records
3450
 * @param bool $rec search in subcontexts as well
3451
 * @param int $page 0-based number of page being displayed
3452
 * @return \core_tag\output\tagindex
3453
 */
3454
function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
3455
    global $CFG, $PAGE;
3456
 
3457
    $perpage = $exclusivemode ? $CFG->coursesperpage : 5;
3458
    $displayoptions = array(
3459
        'limit' => $perpage,
3460
        'offset' => $page * $perpage,
3461
        'viewmoreurl' => null,
3462
    );
3463
 
3464
    $courserenderer = $PAGE->get_renderer('core', 'course');
3465
    $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec));
3466
    $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions);
3467
    $totalpages = ceil($totalcount / $perpage);
3468
 
3469
    return new core_tag\output\tagindex($tag, 'core', 'course', $content,
3470
            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
3471
}
3472
 
3473
/**
3474
 * Implements callback inplace_editable() allowing to edit values in-place
3475
 *
3476
 * @param string $itemtype
3477
 * @param int $itemid
3478
 * @param mixed $newvalue
3479
 * @return ?\core\output\inplace_editable
3480
 */
3481
function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
3482
    if ($itemtype === 'activityname') {
3483
        return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue);
3484
    }
3485
}
3486
 
3487
/**
3488
 * This function calculates the minimum and maximum cutoff values for the timestart of
3489
 * the given event.
3490
 *
3491
 * It will return an array with two values, the first being the minimum cutoff value and
3492
 * the second being the maximum cutoff value. Either or both values can be null, which
3493
 * indicates there is no minimum or maximum, respectively.
3494
 *
3495
 * If a cutoff is required then the function must return an array containing the cutoff
3496
 * timestamp and error string to display to the user if the cutoff value is violated.
3497
 *
3498
 * A minimum and maximum cutoff return value will look like:
3499
 * [
3500
 *     [1505704373, 'The date must be after this date'],
3501
 *     [1506741172, 'The date must be before this date']
3502
 * ]
3503
 *
3504
 * @param calendar_event $event The calendar event to get the time range for
3505
 * @param stdClass $course The course object to get the range from
3506
 * @return array Returns an array with min and max date.
3507
 */
3508
function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) {
3509
    $mindate = null;
3510
    $maxdate = null;
3511
 
3512
    if ($course->startdate) {
3513
        $mindate = [
3514
            $course->startdate,
3515
            get_string('errorbeforecoursestart', 'calendar')
3516
        ];
3517
    }
3518
 
3519
    return [$mindate, $maxdate];
3520
}
3521
 
3522
/**
3523
 * Render the message drawer to be included in the top of the body of each page.
3524
 *
3525
 * @return string HTML
3526
 */
3527
function core_course_drawer(): string {
3528
    global $PAGE;
3529
 
3530
    // If the course index is explicitly set and if it should be hidden.
3531
    if ($PAGE->get_show_course_index() === false) {
3532
        return '';
3533
    }
3534
 
3535
    // Only add course index on non-site course pages.
3536
    if (!$PAGE->course || $PAGE->course->id == SITEID) {
3537
        return '';
3538
    }
3539
 
3540
    // Show course index to users can access the course only.
3541
    if (!can_access_course($PAGE->course, null, '', true)) {
3542
        return '';
3543
    }
3544
 
3545
    $format = course_get_format($PAGE->course);
3546
    $renderer = $format->get_renderer($PAGE);
3547
    if (method_exists($renderer, 'course_index_drawer')) {
3548
        return $renderer->course_index_drawer($format);
3549
    }
3550
 
3551
    return '';
3552
}
3553
 
3554
/**
3555
 * Returns course modules tagged with a specified tag ready for output on tag/index.php page
3556
 *
3557
 * This is a callback used by the tag area core/course_modules to search for course modules
3558
 * tagged with a specific tag.
3559
 *
3560
 * @param core_tag_tag $tag
3561
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3562
 *             are displayed on the page and the per-page limit may be bigger
3563
 * @param int $fromcontextid context id where the link was displayed, may be used by callbacks
3564
 *            to display items in the same context first
3565
 * @param int $contextid context id where to search for records
3566
 * @param bool $recursivecontext search in subcontexts as well
3567
 * @param int $page 0-based number of page being displayed
3568
 * @return ?\core_tag\output\tagindex
3569
 */
3570
function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
3571
                                          $recursivecontext = 1, $page = 0) {
3572
    global $OUTPUT;
3573
    $perpage = $exclusivemode ? 20 : 5;
3574
 
3575
    // Build select query.
3576
    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
3577
    $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
3578
                FROM {course_modules} cm
3579
                JOIN {tag_instance} tt ON cm.id = tt.itemid
3580
                JOIN {course} c ON cm.course = c.id
3581
                JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
3582
               WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
3583
                AND cm.deletioninprogress = 0
3584
                AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
3585
 
3586
    $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
3587
        'coursemodulecontextlevel' => CONTEXT_MODULE);
3588
    if ($contextid) {
3589
        $context = context::instance_by_id($contextid);
3590
        $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
3591
        $params['contextid'] = $context->id;
3592
        $params['path'] = $context->path.'/%';
3593
    }
3594
 
3595
    $query .= ' ORDER BY';
3596
    if ($fromcontextid) {
3597
        // In order-clause specify that modules from inside "fromctx" context should be returned first.
3598
        $fromcontext = context::instance_by_id($fromcontextid);
3599
        $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
3600
        $params['fromcontextid'] = $fromcontext->id;
3601
        $params['frompath'] = $fromcontext->path.'/%';
3602
    }
3603
    $query .= ' c.sortorder, cm.id';
3604
    $totalpages = $page + 1;
3605
 
3606
    // Use core_tag_index_builder to build and filter the list of items.
3607
    // Request one item more than we need so we know if next page exists.
3608
    $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
3609
    while ($item = $builder->has_item_that_needs_access_check()) {
3610
        context_helper::preload_from_record($item);
3611
        $courseid = $item->courseid;
3612
        if (!$builder->can_access_course($courseid)) {
3613
            $builder->set_accessible($item, false);
3614
            continue;
3615
        }
3616
        $modinfo = get_fast_modinfo($builder->get_course($courseid));
3617
        // Set accessibility of this item and all other items in the same course.
3618
        $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
3619
            if ($taggeditem->courseid == $courseid) {
3620
                $cm = $modinfo->get_cm($taggeditem->cmid);
3621
                $builder->set_accessible($taggeditem, $cm->uservisible);
3622
            }
3623
        });
3624
    }
3625
 
3626
    $items = $builder->get_items();
3627
    if (count($items) > $perpage) {
3628
        $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
3629
        array_pop($items);
3630
    }
3631
 
3632
    // Build the display contents.
3633
    if ($items) {
3634
        $tagfeed = new core_tag\output\tagfeed();
3635
        foreach ($items as $item) {
3636
            context_helper::preload_from_record($item);
3637
            $course = $builder->get_course($item->courseid);
3638
            $modinfo = get_fast_modinfo($course);
3639
            $cm = $modinfo->get_cm($item->cmid);
3640
            $courseurl = course_get_url($item->courseid, $cm->sectionnum);
3641
            $cmname = $cm->get_formatted_name();
3642
            if (!$exclusivemode) {
3643
                $cmname = shorten_text($cmname, 100);
3644
            }
3645
            $cmname = html_writer::link($cm->url?:$courseurl, $cmname);
3646
            $coursename = format_string($course->fullname, true,
3647
                    array('context' => context_course::instance($item->courseid)));
3648
            $coursename = html_writer::link($courseurl, $coursename);
3649
            $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
3650
            $tagfeed->add($icon, $cmname, $coursename);
3651
        }
3652
 
3653
        $content = $OUTPUT->render_from_template('core_tag/tagfeed',
3654
                $tagfeed->export_for_template($OUTPUT));
3655
 
3656
        return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
3657
                $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
3658
    }
3659
}
3660
 
3661
/**
3662
 * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
3663
 * This function also handles the frontpage course.
3664
 *
3665
 * @param  stdClass $context context object (it can be a course context or the system context for frontpage settings)
3666
 * @param  stdClass $course  the course where the settings are being rendered
3667
 * @return stdClass          the navigation options in a course and their availability status
3668
 * @since  Moodle 3.2
3669
 */
3670
function course_get_user_navigation_options($context, $course = null) {
3671
    global $CFG, $USER;
3672
 
3673
    $isloggedin = isloggedin();
3674
    $isguestuser = isguestuser();
3675
    $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
3676
 
3677
    if ($isfrontpage) {
3678
        $sitecontext = $context;
3679
    } else {
3680
        $sitecontext = context_system::instance();
3681
    }
3682
 
3683
    // Sets defaults for all options.
3684
    $options = (object) [
3685
        'badges' => false,
3686
        'blogs' => false,
3687
        'competencies' => false,
3688
        'grades' => false,
3689
        'notes' => false,
3690
        'participants' => false,
3691
        'search' => false,
3692
        'tags' => false,
3693
        'communication' => false,
3694
    ];
3695
 
3696
    $options->blogs = !empty($CFG->enableblogs) &&
3697
                        ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
3698
                        ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
3699
                        && has_capability('moodle/blog:view', $sitecontext);
3700
 
3701
    $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
3702
 
3703
    // Frontpage settings?
3704
    if ($isfrontpage) {
3705
        // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
3706
        $options->participants = course_can_view_participants($sitecontext);
3707
        $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
3708
        $options->tags = !empty($CFG->usetags) && $isloggedin;
3709
        $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
3710
    } else {
3711
        // We are in a course, so make sure we use the proper capability (course:viewparticipants).
3712
        $options->participants = course_can_view_participants($context);
3713
 
3714
        // Only display badges if they are enabled and the current user can manage them or if they can view them and have,
3715
        // at least, one available badge.
3716
        if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) {
3717
            $canmanage = has_any_capability([
3718
                    'moodle/badges:createbadge',
3719
                    'moodle/badges:awardbadge',
3720
                    'moodle/badges:configurecriteria',
3721
                    'moodle/badges:configuremessages',
3722
                    'moodle/badges:configuredetails',
3723
                    'moodle/badges:deletebadge',
3724
                ],
3725
                $context
3726
            );
3727
            $totalbadges = [];
3728
            $canview = false;
3729
            if (!$canmanage) {
3730
                // This only needs to be calculated if the user can't manage badges (to improve performance).
3731
                $canview = has_capability('moodle/badges:viewbadges', $context);
3732
                if ($canview) {
3733
                    require_once($CFG->dirroot.'/lib/badgeslib.php');
3734
                    if (is_null($course)) {
3735
                        $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id));
3736
                    } else {
3737
                        $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id));
3738
                    }
3739
                }
3740
            }
3741
 
3742
            $options->badges = ($canmanage || ($canview && $totalbadges > 0));
3743
        }
3744
        // Add view grade report is permitted.
3745
        $grades = false;
3746
 
3747
        if (has_capability('moodle/grade:viewall', $context)) {
3748
            $grades = true;
3749
        } else if (!empty($course->showgrades)) {
3750
            $reports = core_component::get_plugin_list('gradereport');
3751
            if (is_array($reports) && count($reports) > 0) {  // Get all installed reports.
3752
                arsort($reports);   // User is last, we want to test it first.
3753
                foreach ($reports as $plugin => $plugindir) {
3754
                    if (has_capability('gradereport/'.$plugin.':view', $context)) {
3755
                        // Stop when the first visible plugin is found.
3756
                        $grades = true;
3757
                        break;
3758
                    }
3759
                }
3760
            }
3761
        }
3762
        $options->grades = $grades;
3763
    }
3764
 
3765
    if (\core_communication\api::is_available()) {
3766
        $options->communication = has_capability('moodle/course:configurecoursecommunication', $context);
3767
    }
3768
 
3769
    if (\core_competency\api::is_enabled()) {
3770
        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
3771
        $options->competencies = has_any_capability($capabilities, $context);
3772
    }
3773
    return $options;
3774
}
3775
 
3776
/**
3777
 * Return an object with the list of administration options in a course that are available or not for the current user.
3778
 * This function also handles the frontpage settings.
3779
 *
3780
 * @param  stdClass $course  course object (for frontpage it should be a clone of $SITE)
3781
 * @param  stdClass $context context object (course context)
3782
 * @return stdClass          the administration options in a course and their availability status
3783
 * @since  Moodle 3.2
3784
 */
3785
function course_get_user_administration_options($course, $context) {
3786
    global $CFG;
3787
    $isfrontpage = $course->id == SITEID;
3788
    $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
3789
    $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0;
3790
    $options = new stdClass;
3791
    $options->update = has_capability('moodle/course:update', $context);
3792
    $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion &&
3793
        ($options->update || $hascompletionoptions);
3794
    $options->filters = has_capability('moodle/filter:manage', $context) &&
3795
                        count(filter_get_available_in_context($context)) > 0;
3796
    $options->reports = has_capability('moodle/site:viewreports', $context);
3797
    $options->backup = has_capability('moodle/backup:backupcourse', $context);
3798
    $options->restore = has_capability('moodle/restore:restorecourse', $context);
3799
    $options->copy = \core_course\management\helper::can_copy_course($course->id);
3800
    $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context));
3801
 
3802
    if (!$isfrontpage) {
3803
        $options->tags = has_capability('moodle/course:tag', $context);
3804
        $options->gradebook = has_capability('moodle/grade:manage', $context);
3805
        $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
3806
        $options->badges = !empty($CFG->enablebadges);
3807
        $options->import = has_capability('moodle/restore:restoretargetimport', $context);
3808
        $options->reset = has_capability('moodle/course:reset', $context);
3809
        $options->roles = has_capability('moodle/role:switchroles', $context);
3810
    } else {
3811
        // Set default options to false.
3812
        $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
3813
 
3814
        foreach ($listofoptions as $option) {
3815
            $options->$option = false;
3816
        }
3817
    }
3818
 
3819
    return $options;
3820
}
3821
 
3822
/**
3823
 * Validates course start and end dates.
3824
 *
3825
 * Checks that the end course date is not greater than the start course date.
3826
 *
3827
 * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input.
3828
 *
3829
 * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input.
3830
 * @return mixed False if everything alright, error codes otherwise.
3831
 */
3832
function course_validate_dates($coursedata) {
3833
 
3834
    // If both start and end dates are set end date should be later than the start date.
3835
    if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) &&
3836
            ($coursedata['enddate'] < $coursedata['startdate'])) {
3837
        return 'enddatebeforestartdate';
3838
    }
3839
 
3840
    // If start date is not set end date can not be set.
3841
    if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) {
3842
        return 'nostartdatenoenddate';
3843
    }
3844
 
3845
    return false;
3846
}
3847
 
3848
/**
3849
 * Check for course updates in the given context level instances (only modules supported right Now)
3850
 *
3851
 * @param  stdClass $course  course object
3852
 * @param  array $tocheck    instances to check for updates
3853
 * @param  array $filter check only for updates in these areas
3854
 * @return array list of warnings and instances with updates information
3855
 * @since  Moodle 3.2
3856
 */
3857
function course_check_updates($course, $tocheck, $filter = array()) {
3858
    global $CFG, $DB;
3859
 
3860
    $instances = array();
3861
    $warnings = array();
3862
    $modulescallbacksupport = array();
3863
    $modinfo = get_fast_modinfo($course);
3864
 
3865
    $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since');
3866
 
3867
    // Check instances.
3868
    foreach ($tocheck as $instance) {
3869
        if ($instance['contextlevel'] == 'module') {
3870
            // Check module visibility.
3871
            try {
3872
                $cm = $modinfo->get_cm($instance['id']);
3873
            } catch (Exception $e) {
3874
                $warnings[] = array(
3875
                    'item' => 'module',
3876
                    'itemid' => $instance['id'],
3877
                    'warningcode' => 'cmidnotincourse',
3878
                    'message' => 'This module id does not belong to this course.'
3879
                );
3880
                continue;
3881
            }
3882
 
3883
            if (!$cm->uservisible) {
3884
                $warnings[] = array(
3885
                    'item' => 'module',
3886
                    'itemid' => $instance['id'],
3887
                    'warningcode' => 'nonuservisible',
3888
                    'message' => 'You don\'t have access to this module.'
3889
                );
3890
                continue;
3891
            }
3892
            if (empty($supportedplugins['mod_' . $cm->modname])) {
3893
                $warnings[] = array(
3894
                    'item' => 'module',
3895
                    'itemid' => $instance['id'],
3896
                    'warningcode' => 'missingcallback',
3897
                    'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'],
3898
                );
3899
                continue;
3900
            }
3901
            // Retrieve the module instance.
3902
            $instances[] = array(
3903
                'contextlevel' => $instance['contextlevel'],
3904
                'id' => $instance['id'],
3905
                'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter)
3906
            );
3907
 
3908
        } else {
3909
            $warnings[] = array(
3910
                'item' => 'contextlevel',
3911
                'itemid' => $instance['id'],
3912
                'warningcode' => 'contextlevelnotsupported',
3913
                'message' => 'Context level not yet supported ' . $instance['contextlevel'],
3914
            );
3915
        }
3916
    }
3917
    return array($instances, $warnings);
3918
}
3919
 
3920
/**
3921
 * This function classifies a course as past, in progress or future.
3922
 *
3923
 * This function may incur a DB hit to calculate course completion.
3924
 * @param stdClass $course Course record
3925
 * @param stdClass $user User record (optional - defaults to $USER).
3926
 * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
3927
 * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
3928
 */
3929
function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
3930
    global $USER;
3931
 
3932
    if ($user == null) {
3933
        $user = $USER;
3934
    }
3935
 
3936
    if ($completioninfo == null) {
3937
        $completioninfo = new completion_info($course);
3938
    }
3939
 
3940
    // Let plugins override data for timeline classification.
3941
    $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php');
3942
    foreach ($pluginsfunction as $plugintype => $plugins) {
3943
        foreach ($plugins as $pluginfunction) {
3944
            $pluginfunction($course, $user, $completioninfo);
3945
        }
3946
    }
3947
 
3948
    $today = time();
3949
    // End date past.
3950
    if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) {
3951
        return COURSE_TIMELINE_PAST;
3952
    }
3953
 
3954
    // Course was completed.
3955
    if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
3956
        return COURSE_TIMELINE_PAST;
3957
    }
3958
 
3959
    // Start date not reached.
3960
    if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) {
3961
        return COURSE_TIMELINE_FUTURE;
3962
    }
3963
 
3964
    // Everything else is in progress.
3965
    return COURSE_TIMELINE_INPROGRESS;
3966
}
3967
 
3968
/**
3969
 * This function calculates the end date to use for display classification purposes,
3970
 * incorporating the grace period, if any.
3971
 *
3972
 * @param stdClass $course The course record.
3973
 * @return int The new enddate.
3974
 */
3975
function course_classify_end_date($course) {
3976
    global $CFG;
3977
    $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter;
3978
    $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days");
3979
    return $enddate->getTimestamp();
3980
}
3981
 
3982
/**
3983
 * This function calculates the start date to use for display classification purposes,
3984
 * incorporating the grace period, if any.
3985
 *
3986
 * @param stdClass $course The course record.
3987
 * @return int The new startdate.
3988
 */
3989
function course_classify_start_date($course) {
3990
    global $CFG;
3991
    $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore;
3992
    $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days");
3993
    return $startdate->getTimestamp();
3994
}
3995
 
3996
/**
3997
 * Group a list of courses into either past, future, or in progress.
3998
 *
3999
 * The return value will be an array indexed by the COURSE_TIMELINE_* constants
4000
 * with each value being an array of courses in that group.
4001
 * E.g.
4002
 * [
4003
 *      COURSE_TIMELINE_PAST => [... list of past courses ...],
4004
 *      COURSE_TIMELINE_FUTURE => [],
4005
 *      COURSE_TIMELINE_INPROGRESS => []
4006
 * ]
4007
 *
4008
 * @param array $courses List of courses to be grouped.
4009
 * @return array
4010
 */
4011
function course_classify_courses_for_timeline(array $courses) {
4012
    return array_reduce($courses, function($carry, $course) {
4013
        $classification = course_classify_for_timeline($course);
4014
        array_push($carry[$classification], $course);
4015
 
4016
        return $carry;
4017
    }, [
4018
        COURSE_TIMELINE_PAST => [],
4019
        COURSE_TIMELINE_FUTURE => [],
4020
        COURSE_TIMELINE_INPROGRESS => []
4021
    ]);
4022
}
4023
 
4024
/**
4025
 * Get the list of enrolled courses for the current user.
4026
 *
4027
 * This function returns a Generator. The courses will be loaded from the database
4028
 * in chunks rather than a single query.
4029
 *
4030
 * @param int $limit Restrict result set to this amount
4031
 * @param int $offset Skip this number of records from the start of the result set
4032
 * @param string|null $sort SQL string for sorting
4033
 * @param string|null $fields SQL string for fields to be returned
4034
 * @param int $dbquerylimit The number of records to load per DB request
4035
 * @param array $includecourses courses ids to be restricted
4036
 * @param array $hiddencourses courses ids to be excluded
4037
 * @return Generator
4038
 */
4039
function course_get_enrolled_courses_for_logged_in_user(
4040
    int $limit = 0,
4041
    int $offset = 0,
4042
    string $sort = null,
4043
    string $fields = null,
4044
    int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4045
    array $includecourses = [],
4046
    array $hiddencourses = []
4047
): Generator {
4048
 
4049
    $haslimit = !empty($limit);
4050
    $recordsloaded = 0;
4051
    $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4052
 
4053
    while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
4054
        yield from $courses;
4055
 
4056
        $recordsloaded += $querylimit;
4057
 
4058
        if (count($courses) < $querylimit) {
4059
            break;
4060
        }
4061
        if ($haslimit && $recordsloaded >= $limit) {
4062
            break;
4063
        }
4064
 
4065
        $offset += $querylimit;
4066
    }
4067
}
4068
 
4069
/**
4070
 * Get the list of enrolled courses the current user searched for.
4071
 *
4072
 * This function returns a Generator. The courses will be loaded from the database
4073
 * in chunks rather than a single query.
4074
 *
4075
 * @param int $limit Restrict result set to this amount
4076
 * @param int $offset Skip this number of records from the start of the result set
4077
 * @param string|null $sort SQL string for sorting
4078
 * @param string|null $fields SQL string for fields to be returned
4079
 * @param int $dbquerylimit The number of records to load per DB request
4080
 * @param array $searchcriteria contains search criteria
4081
 * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
4082
 *                       search is always category-independent
4083
 * @return Generator
4084
 */
4085
function course_get_enrolled_courses_for_logged_in_user_from_search(
4086
    int $limit = 0,
4087
    int $offset = 0,
4088
    string $sort = null,
4089
    string $fields = null,
4090
    int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4091
    array $searchcriteria = [],
4092
    array $options = []
4093
): Generator {
4094
 
4095
    $haslimit = !empty($limit);
4096
    $recordsloaded = 0;
4097
    $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4098
    $ids = core_course_category::search_courses($searchcriteria, $options);
4099
 
4100
    // If no courses were found matching the criteria return back.
4101
    if (empty($ids)) {
4102
        return;
4103
    }
4104
 
4105
    while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) {
4106
        yield from $courses;
4107
 
4108
        $recordsloaded += $querylimit;
4109
 
4110
        if (count($courses) < $querylimit) {
4111
            break;
4112
        }
4113
        if ($haslimit && $recordsloaded >= $limit) {
4114
            break;
4115
        }
4116
 
4117
        $offset += $querylimit;
4118
    }
4119
}
4120
 
4121
/**
4122
 * Search the given $courses for any that match the given $classification up to the specified
4123
 * $limit.
4124
 *
4125
 * This function will return the subset of courses that match the classification as well as the
4126
 * number of courses it had to process to build that subset.
4127
 *
4128
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4129
 * the courses from the database in chunks.
4130
 *
4131
 * @param array|Traversable $courses List of courses to process
4132
 * @param string $classification One of the COURSE_TIMELINE_* constants
4133
 * @param int $limit Limit the number of results to this amount
4134
 * @return array First value is the filtered courses, second value is the number of courses processed
4135
 */
4136
function course_filter_courses_by_timeline_classification(
4137
    $courses,
4138
    string $classification,
4139
    int $limit = 0
4140
): array {
4141
 
4142
    if (!in_array($classification,
4143
            [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
4144
                COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) {
4145
        $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
4146
            . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE';
4147
        throw new moodle_exception($message);
4148
    }
4149
 
4150
    $filteredcourses = [];
4151
    $numberofcoursesprocessed = 0;
4152
    $filtermatches = 0;
4153
 
4154
    foreach ($courses as $course) {
4155
        $numberofcoursesprocessed++;
4156
        $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
4157
 
4158
        // Added as of MDL-63457 toggle viewability for each user.
4159
        if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
4160
            $classification == COURSE_TIMELINE_SEARCH||
4161
            (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
4162
            $filteredcourses[] = $course;
4163
            $filtermatches++;
4164
        }
4165
 
4166
        if ($limit && $filtermatches >= $limit) {
4167
            // We've found the number of requested courses. No need to continue searching.
4168
            break;
4169
        }
4170
    }
4171
 
4172
    // Return the number of filtered courses as well as the number of courses that were searched
4173
    // in order to find the matching courses. This allows the calling code to do some kind of
4174
    // pagination.
4175
    return [$filteredcourses, $numberofcoursesprocessed];
4176
}
4177
 
4178
/**
4179
 * Search the given $courses for any that match the given $classification up to the specified
4180
 * $limit.
4181
 *
4182
 * This function will return the subset of courses that are favourites as well as the
4183
 * number of courses it had to process to build that subset.
4184
 *
4185
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4186
 * the courses from the database in chunks.
4187
 *
4188
 * @param array|Traversable $courses List of courses to process
4189
 * @param array $favouritecourseids Array of favourite courses.
4190
 * @param int $limit Limit the number of results to this amount
4191
 * @return array First value is the filtered courses, second value is the number of courses processed
4192
 */
4193
function course_filter_courses_by_favourites(
4194
    $courses,
4195
    $favouritecourseids,
4196
    int $limit = 0
4197
): array {
4198
 
4199
    $filteredcourses = [];
4200
    $numberofcoursesprocessed = 0;
4201
    $filtermatches = 0;
4202
 
4203
    foreach ($courses as $course) {
4204
        $numberofcoursesprocessed++;
4205
 
4206
        if (in_array($course->id, $favouritecourseids)) {
4207
            $filteredcourses[] = $course;
4208
            $filtermatches++;
4209
        }
4210
 
4211
        if ($limit && $filtermatches >= $limit) {
4212
            // We've found the number of requested courses. No need to continue searching.
4213
            break;
4214
        }
4215
    }
4216
 
4217
    // Return the number of filtered courses as well as the number of courses that were searched
4218
    // in order to find the matching courses. This allows the calling code to do some kind of
4219
    // pagination.
4220
    return [$filteredcourses, $numberofcoursesprocessed];
4221
}
4222
 
4223
/**
4224
 * Search the given $courses for any that have a $customfieldname value that matches the given
4225
 * $customfieldvalue, up to the specified $limit.
4226
 *
4227
 * This function will return the subset of courses that matches the value as well as the
4228
 * number of courses it had to process to build that subset.
4229
 *
4230
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4231
 * the courses from the database in chunks.
4232
 *
4233
 * @param array|Traversable $courses List of courses to process
4234
 * @param string $customfieldname the shortname of the custom field to match against
4235
 * @param string $customfieldvalue the value this custom field needs to match
4236
 * @param int $limit Limit the number of results to this amount
4237
 * @return array First value is the filtered courses, second value is the number of courses processed
4238
 */
4239
function course_filter_courses_by_customfield(
4240
    $courses,
4241
    $customfieldname,
4242
    $customfieldvalue,
4243
    int $limit = 0
4244
): array {
4245
    global $DB;
4246
 
4247
    if (!$courses) {
4248
        return [[], 0];
4249
    }
4250
 
4251
    // Prepare the list of courses to search through.
4252
    $coursesbyid = [];
4253
    foreach ($courses as $course) {
4254
        $coursesbyid[$course->id] = $course;
4255
    }
4256
    if (!$coursesbyid) {
4257
        return [[], 0];
4258
    }
4259
    list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
4260
 
4261
    // Get the id of the custom field.
4262
    $sql = "
4263
       SELECT f.id
4264
         FROM {customfield_field} f
4265
         JOIN {customfield_category} cat ON cat.id = f.categoryid
4266
        WHERE f.shortname = ?
4267
          AND cat.component = 'core_course'
4268
          AND cat.area = 'course'
4269
    ";
4270
    $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
4271
    if (!$fieldid) {
4272
        return [[], 0];
4273
    }
4274
 
4275
    // Get a list of courseids that match that custom field value.
4276
    if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
4277
        $comparevalue = $DB->sql_compare_text('cd.value');
4278
        $sql = "
4279
           SELECT c.id
4280
             FROM {course} c
4281
        LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
4282
            WHERE c.id $csql
4283
              AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
4284
        ";
4285
        $params['fieldid'] = $fieldid;
4286
        $matchcourseids = $DB->get_fieldset_sql($sql, $params);
4287
    } else {
4288
        $comparevalue = $DB->sql_compare_text('value');
4289
        $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
4290
        $params['fieldid'] = $fieldid;
4291
        $params['customfieldvalue'] = $customfieldvalue;
4292
        $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
4293
    }
4294
 
4295
    // Prepare the list of courses to return.
4296
    $filteredcourses = [];
4297
    $numberofcoursesprocessed = 0;
4298
    $filtermatches = 0;
4299
 
4300
    foreach ($coursesbyid as $course) {
4301
        $numberofcoursesprocessed++;
4302
 
4303
        if (in_array($course->id, $matchcourseids)) {
4304
            $filteredcourses[] = $course;
4305
            $filtermatches++;
4306
        }
4307
 
4308
        if ($limit && $filtermatches >= $limit) {
4309
            // We've found the number of requested courses. No need to continue searching.
4310
            break;
4311
        }
4312
    }
4313
 
4314
    // Return the number of filtered courses as well as the number of courses that were searched
4315
    // in order to find the matching courses. This allows the calling code to do some kind of
4316
    // pagination.
4317
    return [$filteredcourses, $numberofcoursesprocessed];
4318
}
4319
 
4320
/**
4321
 * Check module updates since a given time.
4322
 * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
4323
 *
4324
 * @param  cm_info $cm        course module data
4325
 * @param  int $from          the time to check
4326
 * @param  array $fileareas   additional file ares to check
4327
 * @param  array $filter      if we need to filter and return only selected updates
4328
 * @return stdClass object with the different updates
4329
 * @since  Moodle 3.2
4330
 */
4331
function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) {
4332
    global $DB, $CFG, $USER;
4333
 
4334
    $context = $cm->context;
4335
    $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
4336
 
4337
    $updates = new stdClass();
4338
    $course = get_course($cm->course);
4339
    $component = 'mod_' . $cm->modname;
4340
 
4341
    // Check changes in the module configuration.
4342
    if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) {
4343
        $updates->configuration = (object) array('updated' => false);
4344
        if ($updates->configuration->updated = $mod->timemodified > $from) {
4345
            $updates->configuration->timeupdated = $mod->timemodified;
4346
        }
4347
    }
4348
 
4349
    // Check for updates in files.
4350
    if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) {
4351
        $fileareas[] = 'intro';
4352
    }
4353
    if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) {
4354
        $fs = get_file_storage();
4355
        $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from);
4356
        foreach ($fileareas as $filearea) {
4357
            $updates->{$filearea . 'files'} = (object) array('updated' => false);
4358
        }
4359
        foreach ($files as $file) {
4360
            $updates->{$file->get_filearea() . 'files'}->updated = true;
4361
            $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id();
4362
        }
4363
    }
4364
 
4365
    // Check completion.
4366
    $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES);
4367
    $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS);
4368
    if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) {
4369
        $updates->completion = (object) array('updated' => false);
4370
        $completion = new completion_info($course);
4371
        // Use wholecourse to cache all the modules the first time.
4372
        $completiondata = $completion->get_data($cm, true);
4373
        if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) {
4374
            $updates->completion->timemodified = $completiondata->timemodified;
4375
        }
4376
    }
4377
 
4378
    // Check grades.
4379
    $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE);
4380
    $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES);
4381
    if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) {
4382
        require_once($CFG->libdir . '/gradelib.php');
4383
        $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id);
4384
 
4385
        if (empty($filter) or in_array('gradeitems', $filter)) {
4386
            $updates->gradeitems = (object) array('updated' => false);
4387
            foreach ($grades->items as $gradeitem) {
4388
                foreach ($gradeitem->grades as $grade) {
4389
                    if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4390
                        $updates->gradeitems->updated = true;
4391
                        $updates->gradeitems->itemids[] = $gradeitem->id;
4392
                    }
4393
                }
4394
            }
4395
        }
4396
 
4397
        if (empty($filter) or in_array('outcomes', $filter)) {
4398
            $updates->outcomes = (object) array('updated' => false);
4399
            foreach ($grades->outcomes as $outcome) {
4400
                foreach ($outcome->grades as $grade) {
4401
                    if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4402
                        $updates->outcomes->updated = true;
4403
                        $updates->outcomes->itemids[] = $outcome->id;
4404
                    }
4405
                }
4406
            }
4407
        }
4408
    }
4409
 
4410
    // Check comments.
4411
    if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) {
4412
        $updates->comments = (object) array('updated' => false);
4413
        require_once($CFG->dirroot . '/comment/lib.php');
4414
        require_once($CFG->dirroot . '/comment/locallib.php');
4415
        $manager = new comment_manager();
4416
        $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm);
4417
        if (!empty($comments)) {
4418
            $updates->comments->updated = true;
4419
            $updates->comments->itemids = array_keys($comments);
4420
        }
4421
    }
4422
 
4423
    // Check ratings.
4424
    if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) {
4425
        $updates->ratings = (object) array('updated' => false);
4426
        require_once($CFG->dirroot . '/rating/lib.php');
4427
        $manager = new rating_manager();
4428
        $ratings = $manager->get_component_ratings_since($context, $component, $from);
4429
        if (!empty($ratings)) {
4430
            $updates->ratings->updated = true;
4431
            $updates->ratings->itemids = array_keys($ratings);
4432
        }
4433
    }
4434
 
4435
    return $updates;
4436
}
4437
 
4438
/**
4439
 * Returns true if the user can view the participant page, false otherwise,
4440
 *
4441
 * @param context $context The context we are checking.
4442
 * @return bool
4443
 */
4444
function course_can_view_participants($context) {
4445
    $viewparticipantscap = 'moodle/course:viewparticipants';
4446
    if ($context->contextlevel == CONTEXT_SYSTEM) {
4447
        $viewparticipantscap = 'moodle/site:viewparticipants';
4448
    }
4449
 
4450
    return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
4451
}
4452
 
4453
/**
4454
 * Checks if a user can view the participant page, if not throws an exception.
4455
 *
4456
 * @param context $context The context we are checking.
4457
 * @throws required_capability_exception
4458
 */
4459
function course_require_view_participants($context) {
4460
    if (!course_can_view_participants($context)) {
4461
        $viewparticipantscap = 'moodle/course:viewparticipants';
4462
        if ($context->contextlevel == CONTEXT_SYSTEM) {
4463
            $viewparticipantscap = 'moodle/site:viewparticipants';
4464
        }
4465
        throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
4466
    }
4467
}
4468
 
4469
/**
4470
 * Return whether the user can download from the specified backup file area in the given context.
4471
 *
4472
 * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'.
4473
 * @param \context $context
4474
 * @param stdClass $user the user object. If not provided, the current user will be checked.
4475
 * @return bool true if the user is allowed to download in the context, false otherwise.
4476
 */
4477
function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) {
4478
    $candownload = false;
4479
    switch ($filearea) {
4480
        case 'course':
4481
        case 'backup':
4482
            $candownload = has_capability('moodle/backup:downloadfile', $context, $user);
4483
            break;
4484
        case 'automated':
4485
            // Given the automated backups may contain userinfo, we restrict access such that only users who are able to
4486
            // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo'
4487
            // doesn't make sense here.
4488
            $candownload = has_capability('moodle/backup:downloadfile', $context, $user) &&
4489
                           has_capability('moodle/restore:userinfo', $context, $user);
4490
            break;
4491
        default:
4492
            break;
4493
 
4494
    }
4495
    return $candownload;
4496
}
4497
 
4498
/**
4499
 * Get a list of hidden courses
4500
 *
4501
 * @param int|object|null $user User override to get the filter from. Defaults to current user
4502
 * @return array $ids List of hidden courses
4503
 * @throws coding_exception
4504
 */
4505
function get_hidden_courses_on_timeline($user = null) {
4506
    global $USER;
4507
 
4508
    if (empty($user)) {
4509
        $user = $USER->id;
4510
    }
4511
 
4512
    $preferences = get_user_preferences(null, null, $user);
4513
    $ids = [];
4514
    foreach ($preferences as $key => $value) {
4515
        if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
4516
            $id = preg_split('/block_myoverview_hidden_course_/', $key);
4517
            $ids[] = $id[1];
4518
        }
4519
    }
4520
 
4521
    return $ids;
4522
}
4523
 
4524
/**
4525
 * Returns a list of the most recently courses accessed by a user
4526
 *
4527
 * @param int $userid User id from which the courses will be obtained
4528
 * @param int $limit Restrict result set to this amount
4529
 * @param int $offset Skip this number of records from the start of the result set
4530
 * @param string|null $sort SQL string for sorting
4531
 * @return array
4532
 */
4533
function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) {
4534
 
4535
    global $CFG, $USER, $DB;
4536
 
4537
    if (empty($userid)) {
4538
        $userid = $USER->id;
4539
    }
4540
 
4541
    $basefields = [
4542
        'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
4543
        'shortname', 'fullname', 'timeaccess', 'component', 'visible',
4544
        'showactivitydates', 'showcompletionconditions', 'pdfexportfont'
4545
    ];
4546
 
4547
    if (empty($sort)) {
4548
        $sort = 'timeaccess DESC';
4549
    } else {
4550
        // The SQL string for sorting can define sorting by multiple columns.
4551
        $rawsorts = explode(',', $sort);
4552
        $sorts = array();
4553
        // Validate and trim the sort parameters in the SQL string for sorting.
4554
        foreach ($rawsorts as $rawsort) {
4555
            $sort = trim($rawsort);
4556
            $sortparams = explode(' ', $sort);
4557
            // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
4558
            if (count($sortparams) > 2) {
4559
                throw new invalid_parameter_exception(
4560
                    'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
4561
            }
4562
            $sortfield = trim($sortparams[0]);
4563
            // Validate the value which defines the field to sort by.
4564
            if (!in_array($sortfield, $basefields)) {
4565
                throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
4566
                    implode(', ', $basefields) . '.');
4567
            }
4568
            $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
4569
            // Validate the value which defines the sort direction (if present).
4570
            $allowedsortdirections = ['asc', 'desc'];
4571
            if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
4572
                throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
4573
                    implode(', ', $allowedsortdirections) . '.');
4574
            }
4575
            $sorts[] = $sort;
4576
        }
4577
        $sort = implode(',', $sorts);
4578
    }
4579
 
4580
    $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
4581
 
4582
    $coursefields = 'c.' . join(',', $basefields);
4583
 
4584
    // Ask the favourites service to give us the join SQL for favourited courses,
4585
    // so we can include favourite information in the query.
4586
    $usercontext = \context_user::instance($userid);
4587
    $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4588
    list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid');
4589
 
4590
    $sql = "SELECT $coursefields, $ctxfields
4591
              FROM {course} c
4592
              JOIN {context} ctx
4593
                   ON ctx.contextlevel = :contextlevel
4594
                   AND ctx.instanceid = c.id
4595
              JOIN {user_lastaccess} ul
4596
                   ON ul.courseid = c.id
4597
            $favsql
4598
         LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol
4599
             WHERE ul.userid = :userid
4600
               AND c.visible = :visible
4601
               AND (eg.id IS NOT NULL
4602
                    OR EXISTS (SELECT e.id
4603
                             FROM {enrol} e
4604
                             JOIN {user_enrolments} ue ON ue.enrolid = e.id
4605
                            WHERE e.courseid = c.id
4606
                              AND e.status = :statusenrol
4607
                              AND ue.status = :status
4608
                              AND ue.userid = :userid2
4609
                              AND ue.timestart < :now1
4610
                              AND (ue.timeend = 0 OR ue.timeend > :now2)
4611
                          ))
4612
          ORDER BY $sort";
4613
 
4614
    $now = round(time(), -2); // Improves db caching.
4615
    $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
4616
               'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now,
4617
               'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams;
4618
 
4619
    $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit);
4620
 
4621
    // Filter courses if last access field is hidden.
4622
    $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
4623
 
4624
    if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) {
4625
        $recentcourses = array_filter($recentcourses, function($course) {
4626
            context_helper::preload_from_record($course);
4627
            $context = context_course::instance($course->id, IGNORE_MISSING);
4628
            // If last access was a hidden field, a user requesting info about another user would need permission to view hidden
4629
            // fields.
4630
            return has_capability('moodle/course:viewhiddenuserfields', $context);
4631
        });
4632
    }
4633
 
4634
    return $recentcourses;
4635
}
4636
 
4637
/**
4638
 * Calculate the course start date and offset for the given user ids.
4639
 *
4640
 * If the course is a fixed date course then the course start date will be returned.
4641
 * If the course is a relative date course then the course date will be calculated and
4642
 * and offset provided.
4643
 *
4644
 * The dates are returned as an array with the index being the user id. The array
4645
 * contains the start date and start offset values for the user.
4646
 *
4647
 * If the user is not enrolled in the course then the course start date will be returned.
4648
 *
4649
 * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the
4650
 * former is enrolled in the course at 1563244693 and the latter is not enrolled then the
4651
 * return value would look like:
4652
 * [
4653
 *      '123' => [
4654
 *          'start' => 1563244693,
4655
 *          'startoffset' => 693
4656
 *      ],
4657
 *      '456' => [
4658
 *          'start' => 1563244000,
4659
 *          'startoffset' => 0
4660
 *      ]
4661
 * ]
4662
 *
4663
 * @param stdClass $course The course to fetch dates for.
4664
 * @param array $userids The list of user ids to get dates for.
4665
 * @return array
4666
 */
4667
function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array {
4668
    if (empty($course->relativedatesmode)) {
4669
        // This course isn't set to relative dates so we can early return with the course
4670
        // start date.
4671
        return array_reduce($userids, function($carry, $userid) use ($course) {
4672
            $carry[$userid] = [
4673
                'start' => $course->startdate,
4674
                'startoffset' => 0
4675
            ];
4676
            return $carry;
4677
        }, []);
4678
    }
4679
 
4680
    // We're dealing with a relative dates course now so we need to calculate some dates.
4681
    $cache = cache::make('core', 'course_user_dates');
4682
    $dates = [];
4683
    $uncacheduserids = [];
4684
 
4685
    // Try fetching the values from the cache so that we don't need to do a DB request.
4686
    foreach ($userids as $userid) {
4687
        $cachekey = "{$course->id}_{$userid}";
4688
        $cachedvalue = $cache->get($cachekey);
4689
 
4690
        if ($cachedvalue === false) {
4691
            // Looks like we haven't seen this user for this course before so we'll have
4692
            // to fetch it.
4693
            $uncacheduserids[] = $userid;
4694
        } else {
4695
            [$start, $startoffset] = $cachedvalue;
4696
            $dates[$userid] = [
4697
                'start' => $start,
4698
                'startoffset' => $startoffset
4699
            ];
4700
        }
4701
    }
4702
 
4703
    if (!empty($uncacheduserids)) {
4704
        // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param
4705
        // to false because it filters out users with enrolment start times in the future which
4706
        // we don't want.
4707
        $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids);
4708
 
4709
        foreach ($uncacheduserids as $userid) {
4710
            // Find the user enrolment that has the earliest start date.
4711
            $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) {
4712
                // Only consider enrolments for this user if the user enrolment is active and the
4713
                // enrolment method is enabled.
4714
                if (
4715
                    $enrolment->uestatus == ENROL_USER_ACTIVE &&
4716
                    $enrolment->estatus == ENROL_INSTANCE_ENABLED &&
4717
                    $enrolment->id == $userid
4718
                ) {
4719
                    if (is_null($carry)) {
4720
                        // Haven't found an enrolment yet for this user so use the one we just found.
4721
                        $carry = $enrolment;
4722
                    } else {
4723
                        // We've already found an enrolment for this user so let's use which ever one
4724
                        // has the earliest start time.
4725
                        $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment;
4726
                    }
4727
                }
4728
 
4729
                return $carry;
4730
            }, null);
4731
 
4732
            if ($enrolment) {
4733
                // The course is in relative dates mode so we calculate the student's start
4734
                // date based on their enrolment start date.
4735
                $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart;
4736
                $startoffset = $start - $course->startdate;
4737
            } else {
4738
                // The user is not enrolled in the course so default back to the course start date.
4739
                $start = $course->startdate;
4740
                $startoffset = 0;
4741
            }
4742
 
4743
            $dates[$userid] = [
4744
                'start' => $start,
4745
                'startoffset' => $startoffset
4746
            ];
4747
 
4748
            $cachekey = "{$course->id}_{$userid}";
4749
            $cache->set($cachekey, [$start, $startoffset]);
4750
        }
4751
    }
4752
 
4753
    return $dates;
4754
}
4755
 
4756
/**
4757
 * Calculate the course start date and offset for the given user id.
4758
 *
4759
 * If the course is a fixed date course then the course start date will be returned.
4760
 * If the course is a relative date course then the course date will be calculated and
4761
 * and offset provided.
4762
 *
4763
 * The return array contains the start date and start offset values for the user.
4764
 *
4765
 * If the user is not enrolled in the course then the course start date will be returned.
4766
 *
4767
 * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693
4768
 * then the return would be:
4769
 * [
4770
 *      'start' => 1563244693,
4771
 *      'startoffset' => 693
4772
 * ]
4773
 *
4774
 * If the use was not enrolled then the return would be:
4775
 * [
4776
 *      'start' => 1563244000,
4777
 *      'startoffset' => 0
4778
 * ]
4779
 *
4780
 * @param stdClass $course The course to fetch dates for.
4781
 * @param int $userid The user id to get dates for.
4782
 * @return array
4783
 */
4784
function course_get_course_dates_for_user_id(stdClass $course, int $userid): array {
4785
    return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid];
4786
}
4787
 
4788
/**
4789
 * Renders the course copy form for the modal on the course management screen.
4790
 *
4791
 * @param array $args
4792
 * @return string $o Form HTML.
4793
 */
4794
function course_output_fragment_new_base_form($args) {
4795
 
4796
    $serialiseddata = json_decode($args['jsonformdata'], true);
4797
    $formdata = [];
4798
    if (!empty($serialiseddata)) {
4799
        parse_str($serialiseddata, $formdata);
4800
    }
4801
 
4802
    $context = context_course::instance($args['courseid']);
4803
    $copycaps = \core_course\management\helper::get_course_copy_capabilities();
4804
    require_all_capabilities($copycaps, $context);
4805
 
4806
    $course = get_course($args['courseid']);
4807
    $mform = new \core_backup\output\copy_form(
4808
        null,
4809
        array('course' => $course, 'returnto' => '', 'returnurl' => ''),
4810
        'post', '', ['class' => 'ignoredirty'], true, $formdata);
4811
 
4812
    if (!empty($serialiseddata)) {
4813
        // If we were passed non-empty form data we want the mform to call validation functions and show errors.
4814
        $mform->is_validated();
4815
    }
4816
 
4817
    ob_start();
4818
    $mform->display();
4819
    $o = ob_get_contents();
4820
    ob_end_clean();
4821
 
4822
    return $o;
4823
}
4824
 
4825
/**
4826
 * Get the current course image for the given course.
4827
 *
4828
 * @param \stdClass $course
4829
 * @return null|stored_file
4830
 */
4831
function course_get_courseimage(\stdClass $course): ?stored_file {
4832
    $courseinlist = new core_course_list_element($course);
4833
    foreach ($courseinlist->get_course_overviewfiles() as $file) {
4834
        if ($file->is_valid_image()) {
4835
            return $file;
4836
        }
4837
    }
4838
    return null;
4839
}
4840
 
4841
/**
4842
 * Get course specific data for configuring a communication instance.
4843
 *
4844
 * @param integer $courseid The course id.
4845
 * @return array Returns course data, context and heading.
4846
 */
4847
function course_get_communication_instance_data(int $courseid): array {
4848
    // Do some checks and prepare instance specific data.
4849
    $course = get_course($courseid);
4850
    require_login($course);
4851
    $context = context_course::instance($course->id);
4852
    require_capability('moodle/course:configurecoursecommunication', $context);
4853
 
4854
    $heading = $course->fullname;
4855
    $returnurl = new moodle_url('/course/view.php', ['id' => $courseid]);
4856
 
4857
    return [$course, $context, $heading, $returnurl];
4858
}
4859
 
4860
/**
4861
 * Update a course using communication configuration data.
4862
 *
4863
 * @param stdClass $data The data to update the course with.
4864
 */
4865
function course_update_communication_instance_data(stdClass $data): void {
4866
    $data->id = $data->instanceid; // For correct use in update_course.
4867
    core_communication\helper::update_course_communication_instance(
4868
        course: $data,
4869
        changesincoursecat: false,
4870
    );
4871
}
4872
 
4873
/**
4874
 * Trigger course section viewed event.
4875
 *
4876
 * @param context_course $context course context object
4877
 * @param int $sectionid section number
4878
 * @since Moodle 4.4.
4879
 */
4880
function course_section_view(context_course $context, int $sectionid) {
4881
 
4882
    $eventdata = [
4883
        'objectid' => $sectionid,
4884
        'context' => $context,
4885
    ];
4886
    $event = \core\event\section_viewed::create($eventdata);
4887
    $event->trigger();
4888
 
4889
    user_accesstime_log($context->instanceid);
4890
}