Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * 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'],
11 efrain 1590
            new pix_icon('t/groupv', '', 'moodle', ['class' => 'iconsmall'])
1 efrain 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
 
11 efrain 2984
    $formats = core_plugin_manager::instance()->get_installed_plugins('format');
2985
 
1 efrain 2986
    if (!empty($CFG->format_plugins_sortorder)) {
2987
        $order = explode(',', $CFG->format_plugins_sortorder);
2988
        $order = array_merge(array_intersect($order, array_keys($formats)),
2989
                    array_diff(array_keys($formats), $order));
2990
    } else {
2991
        $order = array_keys($formats);
2992
    }
2993
    if (!$enabledonly) {
2994
        return $order;
2995
    }
2996
    $sortedformats = array();
2997
    foreach ($order as $formatname) {
11 efrain 2998
        $component = "format_{$formatname}";
2999
        $componentdir = core_component::get_component_directory($component);
3000
        if ($componentdir !== null && !get_config($component, 'disabled')) {
1 efrain 3001
            $sortedformats[] = $formatname;
3002
        }
3003
    }
3004
    return $sortedformats;
3005
}
3006
 
3007
/**
3008
 * The URL to use for the specified course (with section)
3009
 *
3010
 * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3011
 * @param int|stdClass $section Section object from database or just field course_sections.section
3012
 *     if omitted the course view page is returned
3013
 * @param array $options options for view URL. At the moment core uses:
3014
 *     'navigation' (bool) if true and section has no separate page, the function returns null
3015
 *     'sr' (int) used by multipage formats to specify to which section to return
3016
 * @return moodle_url The url of course
3017
 */
3018
function course_get_url($courseorid, $section = null, $options = array()) {
3019
    return course_get_format($courseorid)->get_view_url($section, $options);
3020
}
3021
 
3022
/**
3023
 * Create a module.
3024
 *
3025
 * It includes:
3026
 *      - capability checks and other checks
3027
 *      - create the module from the module info
3028
 *
3029
 * @param object $module
3030
 * @return object the created module info
3031
 * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
3032
 */
3033
function create_module($moduleinfo) {
3034
    global $DB, $CFG;
3035
 
3036
    require_once($CFG->dirroot . '/course/modlib.php');
3037
 
3038
    // Check manadatory attributs.
3039
    $mandatoryfields = array('modulename', 'course', 'section', 'visible');
3040
    if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) {
3041
        $mandatoryfields[] = 'introeditor';
3042
    }
3043
    foreach($mandatoryfields as $mandatoryfield) {
3044
        if (!isset($moduleinfo->{$mandatoryfield})) {
3045
            throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield);
3046
        }
3047
    }
3048
 
3049
    // Some additional checks (capability / existing instances).
3050
    $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST);
3051
    list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section);
3052
 
3053
    // Add the module.
3054
    $moduleinfo->module = $module->id;
3055
    $moduleinfo = add_moduleinfo($moduleinfo, $course, null);
3056
 
3057
    return $moduleinfo;
3058
}
3059
 
3060
/**
3061
 * Update a module.
3062
 *
3063
 * It includes:
3064
 *      - capability and other checks
3065
 *      - update the module
3066
 *
3067
 * @param object $module
3068
 * @return object the updated module info
3069
 * @throws moodle_exception if current user is not allowed to update the module
3070
 */
3071
function update_module($moduleinfo) {
3072
    global $DB, $CFG;
3073
 
3074
    require_once($CFG->dirroot . '/course/modlib.php');
3075
 
3076
    // Check the course module exists.
3077
    $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST);
3078
 
3079
    // Check the course exists.
3080
    $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
3081
 
3082
    // Some checks (capaibility / existing instances).
3083
    list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm);
3084
 
3085
    // Retrieve few information needed by update_moduleinfo.
3086
    $moduleinfo->modulename = $cm->modname;
3087
    if (!isset($moduleinfo->scale)) {
3088
        $moduleinfo->scale = 0;
3089
    }
3090
    $moduleinfo->type = 'mod';
3091
 
3092
    // Update the module.
3093
    list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null);
3094
 
3095
    return $moduleinfo;
3096
}
3097
 
3098
/**
3099
 * Duplicate a module on the course for ajax.
3100
 *
3101
 * @see mod_duplicate_module()
3102
 * @param object $course The course
3103
 * @param object $cm The course module to duplicate
3104
 * @param int $sr The section to link back to (used for creating the links)
3105
 * @throws moodle_exception if the plugin doesn't support duplication
3106
 * @return stdClass Object containing:
3107
 * - fullcontent: The HTML markup for the created CM
3108
 * - cmid: The CMID of the newly created CM
3109
 * - redirect: Whether to trigger a redirect following this change
3110
 */
3111
function mod_duplicate_activity($course, $cm, $sr = null) {
3112
    global $PAGE;
3113
 
3114
    $newcm = duplicate_module($course, $cm);
3115
 
3116
    $resp = new stdClass();
3117
    if ($newcm) {
3118
 
3119
        $format = course_get_format($course);
3120
        $renderer = $format->get_renderer($PAGE);
3121
        $modinfo = $format->get_modinfo();
3122
        $section = $modinfo->get_section_info($newcm->sectionnum);
3123
 
3124
        // Get the new element html content.
3125
        $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm);
3126
 
3127
        $resp->cmid = $newcm->id;
3128
    } else {
3129
        // Trigger a redirect.
3130
        $resp->redirect = true;
3131
    }
3132
    return $resp;
3133
}
3134
 
3135
/**
3136
 * Api to duplicate a module.
3137
 *
3138
 * @param object $course course object.
3139
 * @param object $cm course module object to be duplicated.
3140
 * @param int $sectionid section ID new course module will be placed in.
3141
 * @param bool $changename updates module name with text from duplicatedmodule lang string.
3142
 * @since Moodle 2.8
3143
 *
3144
 * @throws Exception
3145
 * @throws coding_exception
3146
 * @throws moodle_exception
3147
 * @throws restore_controller_exception
3148
 *
3149
 * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
3150
 */
3151
function duplicate_module($course, $cm, int $sectionid = null, bool $changename = true): ?cm_info {
3152
    global $CFG, $DB, $USER;
3153
    require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
3154
    require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
3155
    require_once($CFG->libdir . '/filelib.php');
3156
 
3157
    $a          = new stdClass();
3158
    $a->modtype = get_string('modulename', $cm->modname);
3159
    $a->modname = format_string($cm->name);
3160
 
3161
    if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
3162
        throw new moodle_exception('duplicatenosupport', 'error', '', $a);
3163
    }
3164
 
3165
    // Backup the activity.
3166
 
3167
    $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
3168
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
3169
 
3170
    $backupid       = $bc->get_backupid();
3171
    $backupbasepath = $bc->get_plan()->get_basepath();
3172
 
3173
    $bc->execute_plan();
3174
 
3175
    $bc->destroy();
3176
 
3177
    // Restore the backup immediately.
3178
 
3179
    $rc = new restore_controller($backupid, $course->id,
3180
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
3181
 
3182
    // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
3183
    $plan = $rc->get_plan();
3184
    $groupsetting = $plan->get_setting('groups');
3185
    if (empty($groupsetting->get_value())) {
3186
        $groupsetting->set_value(true);
3187
    }
3188
 
3189
    $cmcontext = context_module::instance($cm->id);
3190
    if (!$rc->execute_precheck()) {
3191
        $precheckresults = $rc->get_precheck_results();
3192
        if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
3193
            if (empty($CFG->keeptempdirectoriesonbackup)) {
3194
                fulldelete($backupbasepath);
3195
            }
3196
        }
3197
    }
3198
 
3199
    $rc->execute_plan();
3200
 
3201
    // Now a bit hacky part follows - we try to get the cmid of the newly
3202
    // restored copy of the module.
3203
    $newcmid = null;
3204
    $tasks = $rc->get_plan()->get_tasks();
3205
    foreach ($tasks as $task) {
3206
        if (is_subclass_of($task, 'restore_activity_task')) {
3207
            if ($task->get_old_contextid() == $cmcontext->id) {
3208
                $newcmid = $task->get_moduleid();
3209
                break;
3210
            }
3211
        }
3212
    }
3213
 
3214
    $rc->destroy();
3215
 
3216
    if (empty($CFG->keeptempdirectoriesonbackup)) {
3217
        fulldelete($backupbasepath);
3218
    }
3219
 
3220
    // If we know the cmid of the new course module, let us move it
3221
    // right below the original one. otherwise it will stay at the
3222
    // end of the section.
3223
    if ($newcmid) {
3224
        // Proceed with activity renaming before everything else. We don't use APIs here to avoid
3225
        // triggering a lot of create/update duplicated events.
3226
        $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
3227
        if ($changename) {
3228
            // Add ' (copy)' language string postfix to duplicated module.
3229
            $newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
3230
            set_coursemodule_name($newcm->id, $newname);
3231
        }
3232
 
3233
        $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]);
3234
        if (isset($sectionid)) {
3235
            moveto_module($newcm, $section);
3236
        } else {
3237
            $modarray = explode(",", trim($section->sequence));
3238
            $cmindex = array_search($cm->id, $modarray);
3239
            if ($cmindex !== false && $cmindex < count($modarray) - 1) {
3240
                moveto_module($newcm, $section, $modarray[$cmindex + 1]);
3241
            }
3242
        }
3243
 
3244
        // Update calendar events with the duplicated module.
3245
        // The following line is to be removed in MDL-58906.
3246
        course_module_update_calendar_events($newcm->modname, null, $newcm);
3247
 
11 efrain 3248
        // Copy permission overrides to new course module.
3249
        $newcmcontext = context_module::instance($newcm->id);
3250
        $overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]);
3251
        foreach ($overrides as $override) {
3252
            $override->contextid = $newcmcontext->id;
3253
            unset($override->id);
3254
            $DB->insert_record('role_capabilities', $override);
3255
        }
3256
 
3257
        // Copy locally assigned roles to new course module.
3258
        $overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]);
3259
        foreach ($overrides as $override) {
3260
            $override->contextid = $newcmcontext->id;
3261
            unset($override->id);
3262
            $DB->insert_record('role_assignments', $override);
3263
        }
3264
 
1 efrain 3265
        // Trigger course module created event. We can trigger the event only if we know the newcmid.
3266
        $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
3267
        $event = \core\event\course_module_created::create_from_cm($newcm);
3268
        $event->trigger();
3269
    }
3270
 
3271
    return isset($newcm) ? $newcm : null;
3272
}
3273
 
3274
/**
3275
 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3276
 * Sorts by descending order of time.
3277
 *
3278
 * @param stdClass $a First object
3279
 * @param stdClass $b Second object
3280
 * @return int 0,1,-1 representing the order
3281
 */
3282
function compare_activities_by_time_desc($a, $b) {
3283
    // Make sure the activities actually have a timestamp property.
3284
    if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3285
        return 0;
3286
    }
3287
    // We treat instances without timestamp as if they have a timestamp of 0.
3288
    if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) {
3289
        return 1;
3290
    }
3291
    if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3292
        return -1;
3293
    }
3294
    if ($a->timestamp == $b->timestamp) {
3295
        return 0;
3296
    }
3297
    return ($a->timestamp > $b->timestamp) ? -1 : 1;
3298
}
3299
 
3300
/**
3301
 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3302
 * Sorts by ascending order of time.
3303
 *
3304
 * @param stdClass $a First object
3305
 * @param stdClass $b Second object
3306
 * @return int 0,1,-1 representing the order
3307
 */
3308
function compare_activities_by_time_asc($a, $b) {
3309
    // Make sure the activities actually have a timestamp property.
3310
    if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3311
      return 0;
3312
    }
3313
    // We treat instances without timestamp as if they have a timestamp of 0.
3314
    if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) {
3315
        return -1;
3316
    }
3317
    if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3318
        return 1;
3319
    }
3320
    if ($a->timestamp == $b->timestamp) {
3321
        return 0;
3322
    }
3323
    return ($a->timestamp < $b->timestamp) ? -1 : 1;
3324
}
3325
 
3326
/**
3327
 * Changes the visibility of a course.
3328
 *
3329
 * @param int $courseid The course to change.
3330
 * @param bool $show True to make it visible, false otherwise.
3331
 * @return bool
3332
 */
3333
function course_change_visibility($courseid, $show = true) {
3334
    $course = new stdClass;
3335
    $course->id = $courseid;
3336
    $course->visible = ($show) ? '1' : '0';
3337
    $course->visibleold = $course->visible;
3338
    update_course($course);
3339
    return true;
3340
}
3341
 
3342
/**
3343
 * Changes the course sortorder by one, moving it up or down one in respect to sort order.
3344
 *
3345
 * @param stdClass|core_course_list_element $course
3346
 * @param bool $up If set to true the course will be moved up one. Otherwise down one.
3347
 * @return bool
3348
 */
3349
function course_change_sortorder_by_one($course, $up) {
3350
    global $DB;
3351
    $params = array($course->sortorder, $course->category);
3352
    if ($up) {
3353
        $select = 'sortorder < ? AND category = ?';
3354
        $sort = 'sortorder DESC';
3355
    } else {
3356
        $select = 'sortorder > ? AND category = ?';
3357
        $sort = 'sortorder ASC';
3358
    }
3359
    fix_course_sortorder();
3360
    $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
3361
    if ($swapcourse) {
3362
        $swapcourse = reset($swapcourse);
3363
        $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
3364
        $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
3365
        // Finally reorder courses.
3366
        fix_course_sortorder();
3367
        cache_helper::purge_by_event('changesincourse');
3368
        return true;
3369
    }
3370
    return false;
3371
}
3372
 
3373
/**
3374
 * Changes the sort order of courses in a category so that the first course appears after the second.
3375
 *
3376
 * @param int|stdClass $courseorid The course to focus on.
3377
 * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
3378
 * @return bool
3379
 */
3380
function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
3381
    global $DB;
3382
 
3383
    if (!is_object($courseorid)) {
3384
        $course = get_course($courseorid);
3385
    } else {
3386
        $course = $courseorid;
3387
    }
3388
 
3389
    if ((int)$moveaftercourseid === 0) {
3390
        // We've moving the course to the start of the queue.
3391
        $sql = 'SELECT sortorder
3392
                      FROM {course}
3393
                     WHERE category = :categoryid
3394
                  ORDER BY sortorder';
3395
        $params = array(
3396
            'categoryid' => $course->category
3397
        );
3398
        $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
3399
 
3400
        $sql = 'UPDATE {course}
3401
                   SET sortorder = sortorder + 1
3402
                 WHERE category = :categoryid
3403
                   AND id <> :id';
3404
        $params = array(
3405
            'categoryid' => $course->category,
3406
            'id' => $course->id,
3407
        );
3408
        $DB->execute($sql, $params);
3409
        $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
3410
    } else if ($course->id === $moveaftercourseid) {
3411
        // They're the same - moronic.
3412
        debugging("Invalid move after course given.", DEBUG_DEVELOPER);
3413
        return false;
3414
    } else {
3415
        // Moving this course after the given course. It could be before it could be after.
3416
        $moveaftercourse = get_course($moveaftercourseid);
3417
        if ($course->category !== $moveaftercourse->category) {
3418
            debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
3419
            return false;
3420
        }
3421
        // Increment all courses in the same category that are ordered after the moveafter course.
3422
        // This makes a space for the course we're moving.
3423
        $sql = 'UPDATE {course}
3424
                       SET sortorder = sortorder + 1
3425
                     WHERE category = :categoryid
3426
                       AND sortorder > :sortorder';
3427
        $params = array(
3428
            'categoryid' => $moveaftercourse->category,
3429
            'sortorder' => $moveaftercourse->sortorder
3430
        );
3431
        $DB->execute($sql, $params);
3432
        $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
3433
    }
3434
    fix_course_sortorder();
3435
    cache_helper::purge_by_event('changesincourse');
3436
    return true;
3437
}
3438
 
3439
/**
3440
 * Trigger course viewed event. This API function is used when course view actions happens,
3441
 * usually in course/view.php but also in external functions.
3442
 *
3443
 * @param stdClass  $context course context object
3444
 * @param int $sectionnumber section number
3445
 * @since Moodle 2.9
3446
 */
3447
function course_view($context, $sectionnumber = 0) {
3448
 
3449
    $eventdata = array('context' => $context);
3450
 
3451
    if (!empty($sectionnumber)) {
3452
        $eventdata['other']['coursesectionnumber'] = $sectionnumber;
3453
    }
3454
 
3455
    $event = \core\event\course_viewed::create($eventdata);
3456
    $event->trigger();
3457
 
3458
    user_accesstime_log($context->instanceid);
3459
}
3460
 
3461
/**
3462
 * Returns courses tagged with a specified tag.
3463
 *
3464
 * @param core_tag_tag $tag
3465
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3466
 *             are displayed on the page and the per-page limit may be bigger
3467
 * @param int $fromctx context id where the link was displayed, may be used by callbacks
3468
 *            to display items in the same context first
3469
 * @param int $ctx context id where to search for records
3470
 * @param bool $rec search in subcontexts as well
3471
 * @param int $page 0-based number of page being displayed
3472
 * @return \core_tag\output\tagindex
3473
 */
3474
function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
3475
    global $CFG, $PAGE;
3476
 
3477
    $perpage = $exclusivemode ? $CFG->coursesperpage : 5;
3478
    $displayoptions = array(
3479
        'limit' => $perpage,
3480
        'offset' => $page * $perpage,
3481
        'viewmoreurl' => null,
3482
    );
3483
 
3484
    $courserenderer = $PAGE->get_renderer('core', 'course');
3485
    $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec));
3486
    $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions);
3487
    $totalpages = ceil($totalcount / $perpage);
3488
 
3489
    return new core_tag\output\tagindex($tag, 'core', 'course', $content,
3490
            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
3491
}
3492
 
3493
/**
3494
 * Implements callback inplace_editable() allowing to edit values in-place
3495
 *
3496
 * @param string $itemtype
3497
 * @param int $itemid
3498
 * @param mixed $newvalue
3499
 * @return ?\core\output\inplace_editable
3500
 */
3501
function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
3502
    if ($itemtype === 'activityname') {
3503
        return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue);
3504
    }
3505
}
3506
 
3507
/**
3508
 * This function calculates the minimum and maximum cutoff values for the timestart of
3509
 * the given event.
3510
 *
3511
 * It will return an array with two values, the first being the minimum cutoff value and
3512
 * the second being the maximum cutoff value. Either or both values can be null, which
3513
 * indicates there is no minimum or maximum, respectively.
3514
 *
3515
 * If a cutoff is required then the function must return an array containing the cutoff
3516
 * timestamp and error string to display to the user if the cutoff value is violated.
3517
 *
3518
 * A minimum and maximum cutoff return value will look like:
3519
 * [
3520
 *     [1505704373, 'The date must be after this date'],
3521
 *     [1506741172, 'The date must be before this date']
3522
 * ]
3523
 *
3524
 * @param calendar_event $event The calendar event to get the time range for
3525
 * @param stdClass $course The course object to get the range from
3526
 * @return array Returns an array with min and max date.
3527
 */
3528
function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) {
3529
    $mindate = null;
3530
    $maxdate = null;
3531
 
3532
    if ($course->startdate) {
3533
        $mindate = [
3534
            $course->startdate,
3535
            get_string('errorbeforecoursestart', 'calendar')
3536
        ];
3537
    }
3538
 
3539
    return [$mindate, $maxdate];
3540
}
3541
 
3542
/**
3543
 * Render the message drawer to be included in the top of the body of each page.
3544
 *
3545
 * @return string HTML
3546
 */
3547
function core_course_drawer(): string {
3548
    global $PAGE;
3549
 
3550
    // If the course index is explicitly set and if it should be hidden.
3551
    if ($PAGE->get_show_course_index() === false) {
3552
        return '';
3553
    }
3554
 
3555
    // Only add course index on non-site course pages.
3556
    if (!$PAGE->course || $PAGE->course->id == SITEID) {
3557
        return '';
3558
    }
3559
 
3560
    // Show course index to users can access the course only.
3561
    if (!can_access_course($PAGE->course, null, '', true)) {
3562
        return '';
3563
    }
3564
 
3565
    $format = course_get_format($PAGE->course);
3566
    $renderer = $format->get_renderer($PAGE);
3567
    if (method_exists($renderer, 'course_index_drawer')) {
3568
        return $renderer->course_index_drawer($format);
3569
    }
3570
 
3571
    return '';
3572
}
3573
 
3574
/**
3575
 * Returns course modules tagged with a specified tag ready for output on tag/index.php page
3576
 *
3577
 * This is a callback used by the tag area core/course_modules to search for course modules
3578
 * tagged with a specific tag.
3579
 *
3580
 * @param core_tag_tag $tag
3581
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3582
 *             are displayed on the page and the per-page limit may be bigger
3583
 * @param int $fromcontextid context id where the link was displayed, may be used by callbacks
3584
 *            to display items in the same context first
3585
 * @param int $contextid context id where to search for records
3586
 * @param bool $recursivecontext search in subcontexts as well
3587
 * @param int $page 0-based number of page being displayed
3588
 * @return ?\core_tag\output\tagindex
3589
 */
3590
function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
3591
                                          $recursivecontext = 1, $page = 0) {
3592
    global $OUTPUT;
3593
    $perpage = $exclusivemode ? 20 : 5;
3594
 
3595
    // Build select query.
3596
    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
3597
    $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
3598
                FROM {course_modules} cm
3599
                JOIN {tag_instance} tt ON cm.id = tt.itemid
3600
                JOIN {course} c ON cm.course = c.id
3601
                JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
3602
               WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
3603
                AND cm.deletioninprogress = 0
3604
                AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
3605
 
3606
    $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
3607
        'coursemodulecontextlevel' => CONTEXT_MODULE);
3608
    if ($contextid) {
3609
        $context = context::instance_by_id($contextid);
3610
        $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
3611
        $params['contextid'] = $context->id;
3612
        $params['path'] = $context->path.'/%';
3613
    }
3614
 
3615
    $query .= ' ORDER BY';
3616
    if ($fromcontextid) {
3617
        // In order-clause specify that modules from inside "fromctx" context should be returned first.
3618
        $fromcontext = context::instance_by_id($fromcontextid);
3619
        $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
3620
        $params['fromcontextid'] = $fromcontext->id;
3621
        $params['frompath'] = $fromcontext->path.'/%';
3622
    }
3623
    $query .= ' c.sortorder, cm.id';
3624
    $totalpages = $page + 1;
3625
 
3626
    // Use core_tag_index_builder to build and filter the list of items.
3627
    // Request one item more than we need so we know if next page exists.
3628
    $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
3629
    while ($item = $builder->has_item_that_needs_access_check()) {
3630
        context_helper::preload_from_record($item);
3631
        $courseid = $item->courseid;
3632
        if (!$builder->can_access_course($courseid)) {
3633
            $builder->set_accessible($item, false);
3634
            continue;
3635
        }
3636
        $modinfo = get_fast_modinfo($builder->get_course($courseid));
3637
        // Set accessibility of this item and all other items in the same course.
3638
        $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
3639
            if ($taggeditem->courseid == $courseid) {
3640
                $cm = $modinfo->get_cm($taggeditem->cmid);
3641
                $builder->set_accessible($taggeditem, $cm->uservisible);
3642
            }
3643
        });
3644
    }
3645
 
3646
    $items = $builder->get_items();
3647
    if (count($items) > $perpage) {
3648
        $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
3649
        array_pop($items);
3650
    }
3651
 
3652
    // Build the display contents.
3653
    if ($items) {
3654
        $tagfeed = new core_tag\output\tagfeed();
3655
        foreach ($items as $item) {
3656
            context_helper::preload_from_record($item);
3657
            $course = $builder->get_course($item->courseid);
3658
            $modinfo = get_fast_modinfo($course);
3659
            $cm = $modinfo->get_cm($item->cmid);
3660
            $courseurl = course_get_url($item->courseid, $cm->sectionnum);
3661
            $cmname = $cm->get_formatted_name();
3662
            if (!$exclusivemode) {
3663
                $cmname = shorten_text($cmname, 100);
3664
            }
3665
            $cmname = html_writer::link($cm->url?:$courseurl, $cmname);
3666
            $coursename = format_string($course->fullname, true,
3667
                    array('context' => context_course::instance($item->courseid)));
3668
            $coursename = html_writer::link($courseurl, $coursename);
3669
            $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
3670
            $tagfeed->add($icon, $cmname, $coursename);
3671
        }
3672
 
3673
        $content = $OUTPUT->render_from_template('core_tag/tagfeed',
3674
                $tagfeed->export_for_template($OUTPUT));
3675
 
3676
        return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
3677
                $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
3678
    }
3679
}
3680
 
3681
/**
3682
 * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
3683
 * This function also handles the frontpage course.
3684
 *
3685
 * @param  stdClass $context context object (it can be a course context or the system context for frontpage settings)
3686
 * @param  stdClass $course  the course where the settings are being rendered
3687
 * @return stdClass          the navigation options in a course and their availability status
3688
 * @since  Moodle 3.2
3689
 */
3690
function course_get_user_navigation_options($context, $course = null) {
3691
    global $CFG, $USER;
3692
 
3693
    $isloggedin = isloggedin();
3694
    $isguestuser = isguestuser();
3695
    $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
3696
 
3697
    if ($isfrontpage) {
3698
        $sitecontext = $context;
3699
    } else {
3700
        $sitecontext = context_system::instance();
3701
    }
3702
 
3703
    // Sets defaults for all options.
3704
    $options = (object) [
3705
        'badges' => false,
3706
        'blogs' => false,
3707
        'competencies' => false,
3708
        'grades' => false,
3709
        'notes' => false,
3710
        'participants' => false,
3711
        'search' => false,
3712
        'tags' => false,
3713
        'communication' => false,
3714
    ];
3715
 
3716
    $options->blogs = !empty($CFG->enableblogs) &&
3717
                        ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
3718
                        ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
3719
                        && has_capability('moodle/blog:view', $sitecontext);
3720
 
3721
    $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
3722
 
3723
    // Frontpage settings?
3724
    if ($isfrontpage) {
3725
        // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
3726
        $options->participants = course_can_view_participants($sitecontext);
3727
        $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
3728
        $options->tags = !empty($CFG->usetags) && $isloggedin;
3729
        $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
3730
    } else {
3731
        // We are in a course, so make sure we use the proper capability (course:viewparticipants).
3732
        $options->participants = course_can_view_participants($context);
3733
 
3734
        // Only display badges if they are enabled and the current user can manage them or if they can view them and have,
3735
        // at least, one available badge.
3736
        if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) {
3737
            $canmanage = has_any_capability([
3738
                    'moodle/badges:createbadge',
3739
                    'moodle/badges:awardbadge',
3740
                    'moodle/badges:configurecriteria',
3741
                    'moodle/badges:configuremessages',
3742
                    'moodle/badges:configuredetails',
3743
                    'moodle/badges:deletebadge',
3744
                ],
3745
                $context
3746
            );
3747
            $totalbadges = [];
3748
            $canview = false;
3749
            if (!$canmanage) {
3750
                // This only needs to be calculated if the user can't manage badges (to improve performance).
3751
                $canview = has_capability('moodle/badges:viewbadges', $context);
3752
                if ($canview) {
3753
                    require_once($CFG->dirroot.'/lib/badgeslib.php');
3754
                    if (is_null($course)) {
3755
                        $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id));
3756
                    } else {
3757
                        $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id));
3758
                    }
3759
                }
3760
            }
3761
 
3762
            $options->badges = ($canmanage || ($canview && $totalbadges > 0));
3763
        }
3764
        // Add view grade report is permitted.
3765
        $grades = false;
3766
 
3767
        if (has_capability('moodle/grade:viewall', $context)) {
3768
            $grades = true;
3769
        } else if (!empty($course->showgrades)) {
3770
            $reports = core_component::get_plugin_list('gradereport');
3771
            if (is_array($reports) && count($reports) > 0) {  // Get all installed reports.
3772
                arsort($reports);   // User is last, we want to test it first.
3773
                foreach ($reports as $plugin => $plugindir) {
3774
                    if (has_capability('gradereport/'.$plugin.':view', $context)) {
3775
                        // Stop when the first visible plugin is found.
3776
                        $grades = true;
3777
                        break;
3778
                    }
3779
                }
3780
            }
3781
        }
3782
        $options->grades = $grades;
3783
    }
3784
 
3785
    if (\core_communication\api::is_available()) {
3786
        $options->communication = has_capability('moodle/course:configurecoursecommunication', $context);
3787
    }
3788
 
3789
    if (\core_competency\api::is_enabled()) {
3790
        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
3791
        $options->competencies = has_any_capability($capabilities, $context);
3792
    }
3793
    return $options;
3794
}
3795
 
3796
/**
3797
 * Return an object with the list of administration options in a course that are available or not for the current user.
3798
 * This function also handles the frontpage settings.
3799
 *
3800
 * @param  stdClass $course  course object (for frontpage it should be a clone of $SITE)
3801
 * @param  stdClass $context context object (course context)
3802
 * @return stdClass          the administration options in a course and their availability status
3803
 * @since  Moodle 3.2
3804
 */
3805
function course_get_user_administration_options($course, $context) {
3806
    global $CFG;
3807
    $isfrontpage = $course->id == SITEID;
3808
    $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
3809
    $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0;
3810
    $options = new stdClass;
3811
    $options->update = has_capability('moodle/course:update', $context);
3812
    $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion &&
3813
        ($options->update || $hascompletionoptions);
3814
    $options->filters = has_capability('moodle/filter:manage', $context) &&
3815
                        count(filter_get_available_in_context($context)) > 0;
3816
    $options->reports = has_capability('moodle/site:viewreports', $context);
3817
    $options->backup = has_capability('moodle/backup:backupcourse', $context);
3818
    $options->restore = has_capability('moodle/restore:restorecourse', $context);
3819
    $options->copy = \core_course\management\helper::can_copy_course($course->id);
3820
    $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context));
3821
 
3822
    if (!$isfrontpage) {
3823
        $options->tags = has_capability('moodle/course:tag', $context);
3824
        $options->gradebook = has_capability('moodle/grade:manage', $context);
3825
        $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
3826
        $options->badges = !empty($CFG->enablebadges);
3827
        $options->import = has_capability('moodle/restore:restoretargetimport', $context);
3828
        $options->reset = has_capability('moodle/course:reset', $context);
3829
        $options->roles = has_capability('moodle/role:switchroles', $context);
3830
    } else {
3831
        // Set default options to false.
3832
        $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
3833
 
3834
        foreach ($listofoptions as $option) {
3835
            $options->$option = false;
3836
        }
3837
    }
3838
 
3839
    return $options;
3840
}
3841
 
3842
/**
3843
 * Validates course start and end dates.
3844
 *
3845
 * Checks that the end course date is not greater than the start course date.
3846
 *
3847
 * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input.
3848
 *
3849
 * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input.
3850
 * @return mixed False if everything alright, error codes otherwise.
3851
 */
3852
function course_validate_dates($coursedata) {
3853
 
3854
    // If both start and end dates are set end date should be later than the start date.
3855
    if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) &&
3856
            ($coursedata['enddate'] < $coursedata['startdate'])) {
3857
        return 'enddatebeforestartdate';
3858
    }
3859
 
3860
    // If start date is not set end date can not be set.
3861
    if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) {
3862
        return 'nostartdatenoenddate';
3863
    }
3864
 
3865
    return false;
3866
}
3867
 
3868
/**
3869
 * Check for course updates in the given context level instances (only modules supported right Now)
3870
 *
3871
 * @param  stdClass $course  course object
3872
 * @param  array $tocheck    instances to check for updates
3873
 * @param  array $filter check only for updates in these areas
3874
 * @return array list of warnings and instances with updates information
3875
 * @since  Moodle 3.2
3876
 */
3877
function course_check_updates($course, $tocheck, $filter = array()) {
3878
    global $CFG, $DB;
3879
 
3880
    $instances = array();
3881
    $warnings = array();
3882
    $modulescallbacksupport = array();
3883
    $modinfo = get_fast_modinfo($course);
3884
 
3885
    $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since');
3886
 
3887
    // Check instances.
3888
    foreach ($tocheck as $instance) {
3889
        if ($instance['contextlevel'] == 'module') {
3890
            // Check module visibility.
3891
            try {
3892
                $cm = $modinfo->get_cm($instance['id']);
3893
            } catch (Exception $e) {
3894
                $warnings[] = array(
3895
                    'item' => 'module',
3896
                    'itemid' => $instance['id'],
3897
                    'warningcode' => 'cmidnotincourse',
3898
                    'message' => 'This module id does not belong to this course.'
3899
                );
3900
                continue;
3901
            }
3902
 
3903
            if (!$cm->uservisible) {
3904
                $warnings[] = array(
3905
                    'item' => 'module',
3906
                    'itemid' => $instance['id'],
3907
                    'warningcode' => 'nonuservisible',
3908
                    'message' => 'You don\'t have access to this module.'
3909
                );
3910
                continue;
3911
            }
3912
            if (empty($supportedplugins['mod_' . $cm->modname])) {
3913
                $warnings[] = array(
3914
                    'item' => 'module',
3915
                    'itemid' => $instance['id'],
3916
                    'warningcode' => 'missingcallback',
3917
                    'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'],
3918
                );
3919
                continue;
3920
            }
3921
            // Retrieve the module instance.
3922
            $instances[] = array(
3923
                'contextlevel' => $instance['contextlevel'],
3924
                'id' => $instance['id'],
3925
                'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter)
3926
            );
3927
 
3928
        } else {
3929
            $warnings[] = array(
3930
                'item' => 'contextlevel',
3931
                'itemid' => $instance['id'],
3932
                'warningcode' => 'contextlevelnotsupported',
3933
                'message' => 'Context level not yet supported ' . $instance['contextlevel'],
3934
            );
3935
        }
3936
    }
3937
    return array($instances, $warnings);
3938
}
3939
 
3940
/**
3941
 * This function classifies a course as past, in progress or future.
3942
 *
3943
 * This function may incur a DB hit to calculate course completion.
3944
 * @param stdClass $course Course record
3945
 * @param stdClass $user User record (optional - defaults to $USER).
3946
 * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
3947
 * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
3948
 */
3949
function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
3950
    global $USER;
3951
 
3952
    if ($user == null) {
3953
        $user = $USER;
3954
    }
3955
 
3956
    if ($completioninfo == null) {
3957
        $completioninfo = new completion_info($course);
3958
    }
3959
 
3960
    // Let plugins override data for timeline classification.
3961
    $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php');
3962
    foreach ($pluginsfunction as $plugintype => $plugins) {
3963
        foreach ($plugins as $pluginfunction) {
3964
            $pluginfunction($course, $user, $completioninfo);
3965
        }
3966
    }
3967
 
3968
    $today = time();
3969
    // End date past.
3970
    if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) {
3971
        return COURSE_TIMELINE_PAST;
3972
    }
3973
 
3974
    // Course was completed.
3975
    if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
3976
        return COURSE_TIMELINE_PAST;
3977
    }
3978
 
3979
    // Start date not reached.
3980
    if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) {
3981
        return COURSE_TIMELINE_FUTURE;
3982
    }
3983
 
3984
    // Everything else is in progress.
3985
    return COURSE_TIMELINE_INPROGRESS;
3986
}
3987
 
3988
/**
3989
 * This function calculates the end date to use for display classification purposes,
3990
 * incorporating the grace period, if any.
3991
 *
3992
 * @param stdClass $course The course record.
3993
 * @return int The new enddate.
3994
 */
3995
function course_classify_end_date($course) {
3996
    global $CFG;
3997
    $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter;
3998
    $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days");
3999
    return $enddate->getTimestamp();
4000
}
4001
 
4002
/**
4003
 * This function calculates the start date to use for display classification purposes,
4004
 * incorporating the grace period, if any.
4005
 *
4006
 * @param stdClass $course The course record.
4007
 * @return int The new startdate.
4008
 */
4009
function course_classify_start_date($course) {
4010
    global $CFG;
4011
    $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore;
4012
    $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days");
4013
    return $startdate->getTimestamp();
4014
}
4015
 
4016
/**
4017
 * Group a list of courses into either past, future, or in progress.
4018
 *
4019
 * The return value will be an array indexed by the COURSE_TIMELINE_* constants
4020
 * with each value being an array of courses in that group.
4021
 * E.g.
4022
 * [
4023
 *      COURSE_TIMELINE_PAST => [... list of past courses ...],
4024
 *      COURSE_TIMELINE_FUTURE => [],
4025
 *      COURSE_TIMELINE_INPROGRESS => []
4026
 * ]
4027
 *
4028
 * @param array $courses List of courses to be grouped.
4029
 * @return array
4030
 */
4031
function course_classify_courses_for_timeline(array $courses) {
4032
    return array_reduce($courses, function($carry, $course) {
4033
        $classification = course_classify_for_timeline($course);
4034
        array_push($carry[$classification], $course);
4035
 
4036
        return $carry;
4037
    }, [
4038
        COURSE_TIMELINE_PAST => [],
4039
        COURSE_TIMELINE_FUTURE => [],
4040
        COURSE_TIMELINE_INPROGRESS => []
4041
    ]);
4042
}
4043
 
4044
/**
4045
 * Get the list of enrolled courses for the current user.
4046
 *
4047
 * This function returns a Generator. The courses will be loaded from the database
4048
 * in chunks rather than a single query.
4049
 *
4050
 * @param int $limit Restrict result set to this amount
4051
 * @param int $offset Skip this number of records from the start of the result set
4052
 * @param string|null $sort SQL string for sorting
4053
 * @param string|null $fields SQL string for fields to be returned
4054
 * @param int $dbquerylimit The number of records to load per DB request
4055
 * @param array $includecourses courses ids to be restricted
4056
 * @param array $hiddencourses courses ids to be excluded
4057
 * @return Generator
4058
 */
4059
function course_get_enrolled_courses_for_logged_in_user(
4060
    int $limit = 0,
4061
    int $offset = 0,
4062
    string $sort = null,
4063
    string $fields = null,
4064
    int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4065
    array $includecourses = [],
4066
    array $hiddencourses = []
4067
): Generator {
4068
 
4069
    $haslimit = !empty($limit);
4070
    $recordsloaded = 0;
4071
    $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4072
 
4073
    while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
4074
        yield from $courses;
4075
 
4076
        $recordsloaded += $querylimit;
4077
 
4078
        if (count($courses) < $querylimit) {
4079
            break;
4080
        }
4081
        if ($haslimit && $recordsloaded >= $limit) {
4082
            break;
4083
        }
4084
 
4085
        $offset += $querylimit;
4086
    }
4087
}
4088
 
4089
/**
4090
 * Get the list of enrolled courses the current user searched for.
4091
 *
4092
 * This function returns a Generator. The courses will be loaded from the database
4093
 * in chunks rather than a single query.
4094
 *
4095
 * @param int $limit Restrict result set to this amount
4096
 * @param int $offset Skip this number of records from the start of the result set
4097
 * @param string|null $sort SQL string for sorting
4098
 * @param string|null $fields SQL string for fields to be returned
4099
 * @param int $dbquerylimit The number of records to load per DB request
4100
 * @param array $searchcriteria contains search criteria
4101
 * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
4102
 *                       search is always category-independent
4103
 * @return Generator
4104
 */
4105
function course_get_enrolled_courses_for_logged_in_user_from_search(
4106
    int $limit = 0,
4107
    int $offset = 0,
4108
    string $sort = null,
4109
    string $fields = null,
4110
    int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4111
    array $searchcriteria = [],
4112
    array $options = []
4113
): Generator {
4114
 
4115
    $haslimit = !empty($limit);
4116
    $recordsloaded = 0;
4117
    $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4118
    $ids = core_course_category::search_courses($searchcriteria, $options);
4119
 
4120
    // If no courses were found matching the criteria return back.
4121
    if (empty($ids)) {
4122
        return;
4123
    }
4124
 
4125
    while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) {
4126
        yield from $courses;
4127
 
4128
        $recordsloaded += $querylimit;
4129
 
4130
        if (count($courses) < $querylimit) {
4131
            break;
4132
        }
4133
        if ($haslimit && $recordsloaded >= $limit) {
4134
            break;
4135
        }
4136
 
4137
        $offset += $querylimit;
4138
    }
4139
}
4140
 
4141
/**
4142
 * Search the given $courses for any that match the given $classification up to the specified
4143
 * $limit.
4144
 *
4145
 * This function will return the subset of courses that match the classification as well as the
4146
 * number of courses it had to process to build that subset.
4147
 *
4148
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4149
 * the courses from the database in chunks.
4150
 *
4151
 * @param array|Traversable $courses List of courses to process
4152
 * @param string $classification One of the COURSE_TIMELINE_* constants
4153
 * @param int $limit Limit the number of results to this amount
4154
 * @return array First value is the filtered courses, second value is the number of courses processed
4155
 */
4156
function course_filter_courses_by_timeline_classification(
4157
    $courses,
4158
    string $classification,
4159
    int $limit = 0
4160
): array {
4161
 
4162
    if (!in_array($classification,
4163
            [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
4164
                COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) {
4165
        $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
4166
            . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE';
4167
        throw new moodle_exception($message);
4168
    }
4169
 
4170
    $filteredcourses = [];
4171
    $numberofcoursesprocessed = 0;
4172
    $filtermatches = 0;
4173
 
4174
    foreach ($courses as $course) {
4175
        $numberofcoursesprocessed++;
4176
        $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
4177
 
4178
        // Added as of MDL-63457 toggle viewability for each user.
4179
        if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
4180
            $classification == COURSE_TIMELINE_SEARCH||
4181
            (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
4182
            $filteredcourses[] = $course;
4183
            $filtermatches++;
4184
        }
4185
 
4186
        if ($limit && $filtermatches >= $limit) {
4187
            // We've found the number of requested courses. No need to continue searching.
4188
            break;
4189
        }
4190
    }
4191
 
4192
    // Return the number of filtered courses as well as the number of courses that were searched
4193
    // in order to find the matching courses. This allows the calling code to do some kind of
4194
    // pagination.
4195
    return [$filteredcourses, $numberofcoursesprocessed];
4196
}
4197
 
4198
/**
4199
 * Search the given $courses for any that match the given $classification up to the specified
4200
 * $limit.
4201
 *
4202
 * This function will return the subset of courses that are favourites as well as the
4203
 * number of courses it had to process to build that subset.
4204
 *
4205
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4206
 * the courses from the database in chunks.
4207
 *
4208
 * @param array|Traversable $courses List of courses to process
4209
 * @param array $favouritecourseids Array of favourite courses.
4210
 * @param int $limit Limit the number of results to this amount
4211
 * @return array First value is the filtered courses, second value is the number of courses processed
4212
 */
4213
function course_filter_courses_by_favourites(
4214
    $courses,
4215
    $favouritecourseids,
4216
    int $limit = 0
4217
): array {
4218
 
4219
    $filteredcourses = [];
4220
    $numberofcoursesprocessed = 0;
4221
    $filtermatches = 0;
4222
 
4223
    foreach ($courses as $course) {
4224
        $numberofcoursesprocessed++;
4225
 
4226
        if (in_array($course->id, $favouritecourseids)) {
4227
            $filteredcourses[] = $course;
4228
            $filtermatches++;
4229
        }
4230
 
4231
        if ($limit && $filtermatches >= $limit) {
4232
            // We've found the number of requested courses. No need to continue searching.
4233
            break;
4234
        }
4235
    }
4236
 
4237
    // Return the number of filtered courses as well as the number of courses that were searched
4238
    // in order to find the matching courses. This allows the calling code to do some kind of
4239
    // pagination.
4240
    return [$filteredcourses, $numberofcoursesprocessed];
4241
}
4242
 
4243
/**
4244
 * Search the given $courses for any that have a $customfieldname value that matches the given
4245
 * $customfieldvalue, up to the specified $limit.
4246
 *
4247
 * This function will return the subset of courses that matches the value as well as the
4248
 * number of courses it had to process to build that subset.
4249
 *
4250
 * It is recommended that for larger sets of courses this function is given a Generator that loads
4251
 * the courses from the database in chunks.
4252
 *
4253
 * @param array|Traversable $courses List of courses to process
4254
 * @param string $customfieldname the shortname of the custom field to match against
4255
 * @param string $customfieldvalue the value this custom field needs to match
4256
 * @param int $limit Limit the number of results to this amount
4257
 * @return array First value is the filtered courses, second value is the number of courses processed
4258
 */
4259
function course_filter_courses_by_customfield(
4260
    $courses,
4261
    $customfieldname,
4262
    $customfieldvalue,
4263
    int $limit = 0
4264
): array {
4265
    global $DB;
4266
 
4267
    if (!$courses) {
4268
        return [[], 0];
4269
    }
4270
 
4271
    // Prepare the list of courses to search through.
4272
    $coursesbyid = [];
4273
    foreach ($courses as $course) {
4274
        $coursesbyid[$course->id] = $course;
4275
    }
4276
    if (!$coursesbyid) {
4277
        return [[], 0];
4278
    }
4279
    list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
4280
 
4281
    // Get the id of the custom field.
4282
    $sql = "
4283
       SELECT f.id
4284
         FROM {customfield_field} f
4285
         JOIN {customfield_category} cat ON cat.id = f.categoryid
4286
        WHERE f.shortname = ?
4287
          AND cat.component = 'core_course'
4288
          AND cat.area = 'course'
4289
    ";
4290
    $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
4291
    if (!$fieldid) {
4292
        return [[], 0];
4293
    }
4294
 
4295
    // Get a list of courseids that match that custom field value.
4296
    if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
4297
        $comparevalue = $DB->sql_compare_text('cd.value');
4298
        $sql = "
4299
           SELECT c.id
4300
             FROM {course} c
4301
        LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
4302
            WHERE c.id $csql
4303
              AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
4304
        ";
4305
        $params['fieldid'] = $fieldid;
4306
        $matchcourseids = $DB->get_fieldset_sql($sql, $params);
4307
    } else {
4308
        $comparevalue = $DB->sql_compare_text('value');
4309
        $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
4310
        $params['fieldid'] = $fieldid;
4311
        $params['customfieldvalue'] = $customfieldvalue;
4312
        $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
4313
    }
4314
 
4315
    // Prepare the list of courses to return.
4316
    $filteredcourses = [];
4317
    $numberofcoursesprocessed = 0;
4318
    $filtermatches = 0;
4319
 
4320
    foreach ($coursesbyid as $course) {
4321
        $numberofcoursesprocessed++;
4322
 
4323
        if (in_array($course->id, $matchcourseids)) {
4324
            $filteredcourses[] = $course;
4325
            $filtermatches++;
4326
        }
4327
 
4328
        if ($limit && $filtermatches >= $limit) {
4329
            // We've found the number of requested courses. No need to continue searching.
4330
            break;
4331
        }
4332
    }
4333
 
4334
    // Return the number of filtered courses as well as the number of courses that were searched
4335
    // in order to find the matching courses. This allows the calling code to do some kind of
4336
    // pagination.
4337
    return [$filteredcourses, $numberofcoursesprocessed];
4338
}
4339
 
4340
/**
4341
 * Check module updates since a given time.
4342
 * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
4343
 *
4344
 * @param  cm_info $cm        course module data
4345
 * @param  int $from          the time to check
4346
 * @param  array $fileareas   additional file ares to check
4347
 * @param  array $filter      if we need to filter and return only selected updates
4348
 * @return stdClass object with the different updates
4349
 * @since  Moodle 3.2
4350
 */
4351
function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) {
4352
    global $DB, $CFG, $USER;
4353
 
4354
    $context = $cm->context;
4355
    $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
4356
 
4357
    $updates = new stdClass();
4358
    $course = get_course($cm->course);
4359
    $component = 'mod_' . $cm->modname;
4360
 
4361
    // Check changes in the module configuration.
4362
    if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) {
4363
        $updates->configuration = (object) array('updated' => false);
4364
        if ($updates->configuration->updated = $mod->timemodified > $from) {
4365
            $updates->configuration->timeupdated = $mod->timemodified;
4366
        }
4367
    }
4368
 
4369
    // Check for updates in files.
4370
    if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) {
4371
        $fileareas[] = 'intro';
4372
    }
4373
    if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) {
4374
        $fs = get_file_storage();
4375
        $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from);
4376
        foreach ($fileareas as $filearea) {
4377
            $updates->{$filearea . 'files'} = (object) array('updated' => false);
4378
        }
4379
        foreach ($files as $file) {
4380
            $updates->{$file->get_filearea() . 'files'}->updated = true;
4381
            $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id();
4382
        }
4383
    }
4384
 
4385
    // Check completion.
4386
    $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES);
4387
    $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS);
4388
    if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) {
4389
        $updates->completion = (object) array('updated' => false);
4390
        $completion = new completion_info($course);
4391
        // Use wholecourse to cache all the modules the first time.
4392
        $completiondata = $completion->get_data($cm, true);
4393
        if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) {
4394
            $updates->completion->timemodified = $completiondata->timemodified;
4395
        }
4396
    }
4397
 
4398
    // Check grades.
4399
    $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE);
4400
    $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES);
4401
    if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) {
4402
        require_once($CFG->libdir . '/gradelib.php');
4403
        $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id);
4404
 
4405
        if (empty($filter) or in_array('gradeitems', $filter)) {
4406
            $updates->gradeitems = (object) array('updated' => false);
4407
            foreach ($grades->items as $gradeitem) {
4408
                foreach ($gradeitem->grades as $grade) {
4409
                    if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4410
                        $updates->gradeitems->updated = true;
4411
                        $updates->gradeitems->itemids[] = $gradeitem->id;
4412
                    }
4413
                }
4414
            }
4415
        }
4416
 
4417
        if (empty($filter) or in_array('outcomes', $filter)) {
4418
            $updates->outcomes = (object) array('updated' => false);
4419
            foreach ($grades->outcomes as $outcome) {
4420
                foreach ($outcome->grades as $grade) {
4421
                    if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4422
                        $updates->outcomes->updated = true;
4423
                        $updates->outcomes->itemids[] = $outcome->id;
4424
                    }
4425
                }
4426
            }
4427
        }
4428
    }
4429
 
4430
    // Check comments.
4431
    if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) {
4432
        $updates->comments = (object) array('updated' => false);
4433
        require_once($CFG->dirroot . '/comment/lib.php');
4434
        require_once($CFG->dirroot . '/comment/locallib.php');
4435
        $manager = new comment_manager();
4436
        $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm);
4437
        if (!empty($comments)) {
4438
            $updates->comments->updated = true;
4439
            $updates->comments->itemids = array_keys($comments);
4440
        }
4441
    }
4442
 
4443
    // Check ratings.
4444
    if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) {
4445
        $updates->ratings = (object) array('updated' => false);
4446
        require_once($CFG->dirroot . '/rating/lib.php');
4447
        $manager = new rating_manager();
4448
        $ratings = $manager->get_component_ratings_since($context, $component, $from);
4449
        if (!empty($ratings)) {
4450
            $updates->ratings->updated = true;
4451
            $updates->ratings->itemids = array_keys($ratings);
4452
        }
4453
    }
4454
 
4455
    return $updates;
4456
}
4457
 
4458
/**
4459
 * Returns true if the user can view the participant page, false otherwise,
4460
 *
4461
 * @param context $context The context we are checking.
4462
 * @return bool
4463
 */
4464
function course_can_view_participants($context) {
4465
    $viewparticipantscap = 'moodle/course:viewparticipants';
4466
    if ($context->contextlevel == CONTEXT_SYSTEM) {
4467
        $viewparticipantscap = 'moodle/site:viewparticipants';
4468
    }
4469
 
4470
    return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
4471
}
4472
 
4473
/**
4474
 * Checks if a user can view the participant page, if not throws an exception.
4475
 *
4476
 * @param context $context The context we are checking.
4477
 * @throws required_capability_exception
4478
 */
4479
function course_require_view_participants($context) {
4480
    if (!course_can_view_participants($context)) {
4481
        $viewparticipantscap = 'moodle/course:viewparticipants';
4482
        if ($context->contextlevel == CONTEXT_SYSTEM) {
4483
            $viewparticipantscap = 'moodle/site:viewparticipants';
4484
        }
4485
        throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
4486
    }
4487
}
4488
 
4489
/**
4490
 * Return whether the user can download from the specified backup file area in the given context.
4491
 *
4492
 * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'.
4493
 * @param \context $context
4494
 * @param stdClass $user the user object. If not provided, the current user will be checked.
4495
 * @return bool true if the user is allowed to download in the context, false otherwise.
4496
 */
4497
function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) {
4498
    $candownload = false;
4499
    switch ($filearea) {
4500
        case 'course':
4501
        case 'backup':
4502
            $candownload = has_capability('moodle/backup:downloadfile', $context, $user);
4503
            break;
4504
        case 'automated':
4505
            // Given the automated backups may contain userinfo, we restrict access such that only users who are able to
4506
            // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo'
4507
            // doesn't make sense here.
4508
            $candownload = has_capability('moodle/backup:downloadfile', $context, $user) &&
4509
                           has_capability('moodle/restore:userinfo', $context, $user);
4510
            break;
4511
        default:
4512
            break;
4513
 
4514
    }
4515
    return $candownload;
4516
}
4517
 
4518
/**
4519
 * Get a list of hidden courses
4520
 *
4521
 * @param int|object|null $user User override to get the filter from. Defaults to current user
4522
 * @return array $ids List of hidden courses
4523
 * @throws coding_exception
4524
 */
4525
function get_hidden_courses_on_timeline($user = null) {
4526
    global $USER;
4527
 
4528
    if (empty($user)) {
4529
        $user = $USER->id;
4530
    }
4531
 
4532
    $preferences = get_user_preferences(null, null, $user);
4533
    $ids = [];
4534
    foreach ($preferences as $key => $value) {
4535
        if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
4536
            $id = preg_split('/block_myoverview_hidden_course_/', $key);
4537
            $ids[] = $id[1];
4538
        }
4539
    }
4540
 
4541
    return $ids;
4542
}
4543
 
4544
/**
4545
 * Returns a list of the most recently courses accessed by a user
4546
 *
4547
 * @param int $userid User id from which the courses will be obtained
4548
 * @param int $limit Restrict result set to this amount
4549
 * @param int $offset Skip this number of records from the start of the result set
4550
 * @param string|null $sort SQL string for sorting
4551
 * @return array
4552
 */
4553
function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) {
4554
 
4555
    global $CFG, $USER, $DB;
4556
 
4557
    if (empty($userid)) {
4558
        $userid = $USER->id;
4559
    }
4560
 
4561
    $basefields = [
4562
        'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
4563
        'shortname', 'fullname', 'timeaccess', 'component', 'visible',
4564
        'showactivitydates', 'showcompletionconditions', 'pdfexportfont'
4565
    ];
4566
 
4567
    if (empty($sort)) {
4568
        $sort = 'timeaccess DESC';
4569
    } else {
4570
        // The SQL string for sorting can define sorting by multiple columns.
4571
        $rawsorts = explode(',', $sort);
4572
        $sorts = array();
4573
        // Validate and trim the sort parameters in the SQL string for sorting.
4574
        foreach ($rawsorts as $rawsort) {
4575
            $sort = trim($rawsort);
4576
            $sortparams = explode(' ', $sort);
4577
            // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
4578
            if (count($sortparams) > 2) {
4579
                throw new invalid_parameter_exception(
4580
                    'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
4581
            }
4582
            $sortfield = trim($sortparams[0]);
4583
            // Validate the value which defines the field to sort by.
4584
            if (!in_array($sortfield, $basefields)) {
4585
                throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
4586
                    implode(', ', $basefields) . '.');
4587
            }
4588
            $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
4589
            // Validate the value which defines the sort direction (if present).
4590
            $allowedsortdirections = ['asc', 'desc'];
4591
            if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
4592
                throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
4593
                    implode(', ', $allowedsortdirections) . '.');
4594
            }
4595
            $sorts[] = $sort;
4596
        }
4597
        $sort = implode(',', $sorts);
4598
    }
4599
 
4600
    $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
4601
 
4602
    $coursefields = 'c.' . join(',', $basefields);
4603
 
4604
    // Ask the favourites service to give us the join SQL for favourited courses,
4605
    // so we can include favourite information in the query.
4606
    $usercontext = \context_user::instance($userid);
4607
    $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4608
    list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid');
4609
 
4610
    $sql = "SELECT $coursefields, $ctxfields
4611
              FROM {course} c
4612
              JOIN {context} ctx
4613
                   ON ctx.contextlevel = :contextlevel
4614
                   AND ctx.instanceid = c.id
4615
              JOIN {user_lastaccess} ul
4616
                   ON ul.courseid = c.id
4617
            $favsql
4618
         LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol
4619
             WHERE ul.userid = :userid
4620
               AND c.visible = :visible
4621
               AND (eg.id IS NOT NULL
4622
                    OR EXISTS (SELECT e.id
4623
                             FROM {enrol} e
4624
                             JOIN {user_enrolments} ue ON ue.enrolid = e.id
4625
                            WHERE e.courseid = c.id
4626
                              AND e.status = :statusenrol
4627
                              AND ue.status = :status
4628
                              AND ue.userid = :userid2
4629
                              AND ue.timestart < :now1
4630
                              AND (ue.timeend = 0 OR ue.timeend > :now2)
4631
                          ))
4632
          ORDER BY $sort";
4633
 
4634
    $now = round(time(), -2); // Improves db caching.
4635
    $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
4636
               'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now,
4637
               'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams;
4638
 
4639
    $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit);
4640
 
4641
    // Filter courses if last access field is hidden.
4642
    $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
4643
 
4644
    if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) {
4645
        $recentcourses = array_filter($recentcourses, function($course) {
4646
            context_helper::preload_from_record($course);
4647
            $context = context_course::instance($course->id, IGNORE_MISSING);
4648
            // If last access was a hidden field, a user requesting info about another user would need permission to view hidden
4649
            // fields.
4650
            return has_capability('moodle/course:viewhiddenuserfields', $context);
4651
        });
4652
    }
4653
 
4654
    return $recentcourses;
4655
}
4656
 
4657
/**
4658
 * Calculate the course start date and offset for the given user ids.
4659
 *
4660
 * If the course is a fixed date course then the course start date will be returned.
4661
 * If the course is a relative date course then the course date will be calculated and
4662
 * and offset provided.
4663
 *
4664
 * The dates are returned as an array with the index being the user id. The array
4665
 * contains the start date and start offset values for the user.
4666
 *
4667
 * If the user is not enrolled in the course then the course start date will be returned.
4668
 *
4669
 * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the
4670
 * former is enrolled in the course at 1563244693 and the latter is not enrolled then the
4671
 * return value would look like:
4672
 * [
4673
 *      '123' => [
4674
 *          'start' => 1563244693,
4675
 *          'startoffset' => 693
4676
 *      ],
4677
 *      '456' => [
4678
 *          'start' => 1563244000,
4679
 *          'startoffset' => 0
4680
 *      ]
4681
 * ]
4682
 *
4683
 * @param stdClass $course The course to fetch dates for.
4684
 * @param array $userids The list of user ids to get dates for.
4685
 * @return array
4686
 */
4687
function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array {
4688
    if (empty($course->relativedatesmode)) {
4689
        // This course isn't set to relative dates so we can early return with the course
4690
        // start date.
4691
        return array_reduce($userids, function($carry, $userid) use ($course) {
4692
            $carry[$userid] = [
4693
                'start' => $course->startdate,
4694
                'startoffset' => 0
4695
            ];
4696
            return $carry;
4697
        }, []);
4698
    }
4699
 
4700
    // We're dealing with a relative dates course now so we need to calculate some dates.
4701
    $cache = cache::make('core', 'course_user_dates');
4702
    $dates = [];
4703
    $uncacheduserids = [];
4704
 
4705
    // Try fetching the values from the cache so that we don't need to do a DB request.
4706
    foreach ($userids as $userid) {
4707
        $cachekey = "{$course->id}_{$userid}";
4708
        $cachedvalue = $cache->get($cachekey);
4709
 
4710
        if ($cachedvalue === false) {
4711
            // Looks like we haven't seen this user for this course before so we'll have
4712
            // to fetch it.
4713
            $uncacheduserids[] = $userid;
4714
        } else {
4715
            [$start, $startoffset] = $cachedvalue;
4716
            $dates[$userid] = [
4717
                'start' => $start,
4718
                'startoffset' => $startoffset
4719
            ];
4720
        }
4721
    }
4722
 
4723
    if (!empty($uncacheduserids)) {
4724
        // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param
4725
        // to false because it filters out users with enrolment start times in the future which
4726
        // we don't want.
4727
        $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids);
4728
 
4729
        foreach ($uncacheduserids as $userid) {
4730
            // Find the user enrolment that has the earliest start date.
4731
            $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) {
4732
                // Only consider enrolments for this user if the user enrolment is active and the
4733
                // enrolment method is enabled.
4734
                if (
4735
                    $enrolment->uestatus == ENROL_USER_ACTIVE &&
4736
                    $enrolment->estatus == ENROL_INSTANCE_ENABLED &&
4737
                    $enrolment->id == $userid
4738
                ) {
4739
                    if (is_null($carry)) {
4740
                        // Haven't found an enrolment yet for this user so use the one we just found.
4741
                        $carry = $enrolment;
4742
                    } else {
4743
                        // We've already found an enrolment for this user so let's use which ever one
4744
                        // has the earliest start time.
4745
                        $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment;
4746
                    }
4747
                }
4748
 
4749
                return $carry;
4750
            }, null);
4751
 
4752
            if ($enrolment) {
4753
                // The course is in relative dates mode so we calculate the student's start
4754
                // date based on their enrolment start date.
4755
                $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart;
4756
                $startoffset = $start - $course->startdate;
4757
            } else {
4758
                // The user is not enrolled in the course so default back to the course start date.
4759
                $start = $course->startdate;
4760
                $startoffset = 0;
4761
            }
4762
 
4763
            $dates[$userid] = [
4764
                'start' => $start,
4765
                'startoffset' => $startoffset
4766
            ];
4767
 
4768
            $cachekey = "{$course->id}_{$userid}";
4769
            $cache->set($cachekey, [$start, $startoffset]);
4770
        }
4771
    }
4772
 
4773
    return $dates;
4774
}
4775
 
4776
/**
4777
 * Calculate the course start date and offset for the given user id.
4778
 *
4779
 * If the course is a fixed date course then the course start date will be returned.
4780
 * If the course is a relative date course then the course date will be calculated and
4781
 * and offset provided.
4782
 *
4783
 * The return array contains the start date and start offset values for the user.
4784
 *
4785
 * If the user is not enrolled in the course then the course start date will be returned.
4786
 *
4787
 * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693
4788
 * then the return would be:
4789
 * [
4790
 *      'start' => 1563244693,
4791
 *      'startoffset' => 693
4792
 * ]
4793
 *
4794
 * If the use was not enrolled then the return would be:
4795
 * [
4796
 *      'start' => 1563244000,
4797
 *      'startoffset' => 0
4798
 * ]
4799
 *
4800
 * @param stdClass $course The course to fetch dates for.
4801
 * @param int $userid The user id to get dates for.
4802
 * @return array
4803
 */
4804
function course_get_course_dates_for_user_id(stdClass $course, int $userid): array {
4805
    return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid];
4806
}
4807
 
4808
/**
4809
 * Renders the course copy form for the modal on the course management screen.
4810
 *
4811
 * @param array $args
4812
 * @return string $o Form HTML.
4813
 */
4814
function course_output_fragment_new_base_form($args) {
4815
 
4816
    $serialiseddata = json_decode($args['jsonformdata'], true);
4817
    $formdata = [];
4818
    if (!empty($serialiseddata)) {
4819
        parse_str($serialiseddata, $formdata);
4820
    }
4821
 
4822
    $context = context_course::instance($args['courseid']);
4823
    $copycaps = \core_course\management\helper::get_course_copy_capabilities();
4824
    require_all_capabilities($copycaps, $context);
4825
 
4826
    $course = get_course($args['courseid']);
4827
    $mform = new \core_backup\output\copy_form(
4828
        null,
4829
        array('course' => $course, 'returnto' => '', 'returnurl' => ''),
4830
        'post', '', ['class' => 'ignoredirty'], true, $formdata);
4831
 
4832
    if (!empty($serialiseddata)) {
4833
        // If we were passed non-empty form data we want the mform to call validation functions and show errors.
4834
        $mform->is_validated();
4835
    }
4836
 
4837
    ob_start();
4838
    $mform->display();
4839
    $o = ob_get_contents();
4840
    ob_end_clean();
4841
 
4842
    return $o;
4843
}
4844
 
4845
/**
4846
 * Get the current course image for the given course.
4847
 *
4848
 * @param \stdClass $course
4849
 * @return null|stored_file
4850
 */
4851
function course_get_courseimage(\stdClass $course): ?stored_file {
4852
    $courseinlist = new core_course_list_element($course);
4853
    foreach ($courseinlist->get_course_overviewfiles() as $file) {
4854
        if ($file->is_valid_image()) {
4855
            return $file;
4856
        }
4857
    }
4858
    return null;
4859
}
4860
 
4861
/**
4862
 * Get course specific data for configuring a communication instance.
4863
 *
4864
 * @param integer $courseid The course id.
4865
 * @return array Returns course data, context and heading.
4866
 */
4867
function course_get_communication_instance_data(int $courseid): array {
4868
    // Do some checks and prepare instance specific data.
4869
    $course = get_course($courseid);
4870
    require_login($course);
4871
    $context = context_course::instance($course->id);
4872
    require_capability('moodle/course:configurecoursecommunication', $context);
4873
 
4874
    $heading = $course->fullname;
4875
    $returnurl = new moodle_url('/course/view.php', ['id' => $courseid]);
4876
 
4877
    return [$course, $context, $heading, $returnurl];
4878
}
4879
 
4880
/**
4881
 * Update a course using communication configuration data.
4882
 *
4883
 * @param stdClass $data The data to update the course with.
4884
 */
4885
function course_update_communication_instance_data(stdClass $data): void {
4886
    $data->id = $data->instanceid; // For correct use in update_course.
4887
    core_communication\helper::update_course_communication_instance(
4888
        course: $data,
4889
        changesincoursecat: false,
4890
    );
4891
}
4892
 
4893
/**
4894
 * Trigger course section viewed event.
4895
 *
4896
 * @param context_course $context course context object
4897
 * @param int $sectionid section number
4898
 * @since Moodle 4.4.
4899
 */
4900
function course_section_view(context_course $context, int $sectionid) {
4901
 
4902
    $eventdata = [
4903
        'objectid' => $sectionid,
4904
        'context' => $context,
4905
    ];
4906
    $event = \core\event\section_viewed::create($eventdata);
4907
    $event->trigger();
4908
 
4909
    user_accesstime_log($context->instanceid);
4910
}