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
 * Upgrade helper functions
19
 *
20
 * This file is used for special upgrade functions - for example groups and gradebook.
21
 * These functions must use SQL and database related functions only- no other Moodle API,
22
 * because it might depend on db structures that are not yet present during upgrade.
23
 * (Do not use functions from accesslib.php, grades classes or group functions at all!)
24
 *
25
 * @package   core_install
26
 * @category  upgrade
27
 * @copyright 2007 Petr Skoda (http://skodak.org)
28
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 */
30
 
31
defined('MOODLE_INTERNAL') || die();
32
 
33
/**
34
 * Returns all non-view and non-temp tables with sane names.
35
 * Prints list of non-supported tables using $OUTPUT->notification()
36
 *
37
 * @return array
38
 */
39
function upgrade_mysql_get_supported_tables() {
40
    global $OUTPUT, $DB;
41
 
42
    $tables = array();
43
    $patprefix = str_replace('_', '\\_', $DB->get_prefix());
44
    $pregprefix = preg_quote($DB->get_prefix(), '/');
45
 
46
    $sql = "SHOW FULL TABLES LIKE '$patprefix%'";
47
    $rs = $DB->get_recordset_sql($sql);
48
    foreach ($rs as $record) {
49
        $record = array_change_key_case((array)$record, CASE_LOWER);
50
        $type = $record['table_type'];
51
        unset($record['table_type']);
52
        $fullname = array_shift($record);
53
 
54
        if ($pregprefix === '') {
55
            $name = $fullname;
56
        } else {
57
            $count = null;
58
            $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count);
59
            if ($count !== 1) {
60
                continue;
61
            }
62
        }
63
 
64
        if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) {
65
            echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem');
66
            continue;
67
        }
68
        if ($type === 'VIEW') {
69
            echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem');
70
            continue;
71
        }
72
        $tables[$name] = $name;
73
    }
74
    $rs->close();
75
 
76
    return $tables;
77
}
78
 
79
/**
80
 * Using data for a single course-module that has groupmembersonly enabled,
81
 * returns the new availability value that incorporates the correct
82
 * groupmembersonly option.
83
 *
84
 * Included as a function so that it can be shared between upgrade and restore,
85
 * and unit-tested.
86
 *
87
 * @param int $groupingid Grouping id for the course-module (0 if none)
88
 * @param string $availability Availability JSON data for the module (null if none)
89
 * @return string New value for availability for the module
90
 */
91
function upgrade_group_members_only($groupingid, $availability) {
92
    // Work out the new JSON object representing this option.
93
    if ($groupingid) {
94
        // Require specific grouping.
95
        $condition = (object)array('type' => 'grouping', 'id' => (int)$groupingid);
96
    } else {
97
        // No grouping specified, so require membership of any group.
98
        $condition = (object)array('type' => 'group');
99
    }
100
 
101
    if (is_null($availability)) {
102
        // If there are no conditions using the new API then just set it.
103
        $tree = (object)array('op' => '&', 'c' => array($condition), 'showc' => array(false));
104
    } else {
105
        // There are existing conditions.
106
        $tree = json_decode($availability);
107
        switch ($tree->op) {
108
            case '&' :
109
                // For & conditions we can just add this one.
110
                $tree->c[] = $condition;
111
                $tree->showc[] = false;
112
                break;
113
            case '!|' :
114
                // For 'not or' conditions we can add this one
115
                // but negated.
116
                $tree->c[] = (object)array('op' => '!&', 'c' => array($condition));
117
                $tree->showc[] = false;
118
                break;
119
            default:
120
                // For the other two (OR and NOT AND) we have to add
121
                // an extra level to the tree.
122
                $tree = (object)array('op' => '&', 'c' => array($tree, $condition),
123
                        'showc' => array($tree->show, false));
124
                // Inner trees do not have a show option, so remove it.
125
                unset($tree->c[0]->show);
126
                break;
127
        }
128
    }
129
 
130
    return json_encode($tree);
131
}
132
 
133
/**
134
 * Marks all courses with changes in extra credit weight calculation
135
 *
136
 * Used during upgrade and in course restore process
137
 *
138
 * This upgrade script is needed because we changed the algorithm for calculating the automatic weights of extra
139
 * credit items and want to prevent changes in the existing student grades.
140
 *
141
 * @param int $onlycourseid
142
 */
143
function upgrade_extra_credit_weightoverride($onlycourseid = 0) {
144
    global $DB;
145
 
146
    // Find all courses that have categories in Natural aggregation method where there is at least one extra credit
147
    // item and at least one item with overridden weight.
148
    $courses = $DB->get_fieldset_sql(
149
        "SELECT DISTINCT gc.courseid
150
          FROM {grade_categories} gc
151
          INNER JOIN {grade_items} gi ON gc.id = gi.categoryid AND gi.weightoverride = :weightoverriden
152
          INNER JOIN {grade_items} gie ON gc.id = gie.categoryid AND gie.aggregationcoef = :extracredit
153
          WHERE gc.aggregation = :naturalaggmethod" . ($onlycourseid ? " AND gc.courseid = :onlycourseid" : ''),
154
        array('naturalaggmethod' => 13,
155
            'weightoverriden' => 1,
156
            'extracredit' => 1,
157
            'onlycourseid' => $onlycourseid,
158
        )
159
    );
160
    foreach ($courses as $courseid) {
161
        $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
162
        if (!$gradebookfreeze) {
163
            set_config('gradebook_calculations_freeze_' . $courseid, 20150619);
164
        }
165
    }
166
}
167
 
168
/**
169
 * Marks all courses that require calculated grade items be updated.
170
 *
171
 * Used during upgrade and in course restore process.
172
 *
173
 * This upgrade script is needed because the calculated grade items were stuck with a maximum of 100 and could be changed.
174
 * This flags the courses that are affected and the grade book is frozen to retain grade integrity.
175
 *
176
 * @param int $courseid Specify a course ID to run this script on just one course.
177
 */
178
function upgrade_calculated_grade_items($courseid = null) {
179
    global $DB, $CFG;
180
 
181
    $affectedcourses = array();
182
    $possiblecourseids = array();
183
    $params = array();
184
    $singlecoursesql = '';
185
    if (isset($courseid)) {
186
        $singlecoursesql = "AND ns.id = :courseid";
187
        $params['courseid'] = $courseid;
188
    }
189
    $siteminmaxtouse = 1;
190
    if (isset($CFG->grade_minmaxtouse)) {
191
        $siteminmaxtouse = $CFG->grade_minmaxtouse;
192
    }
193
    $courseidsql = "SELECT ns.id
194
                      FROM (
195
                        SELECT c.id, coalesce(" . $DB->sql_compare_text('gs.value') . ", :siteminmax) AS gradevalue
196
                          FROM {course} c
197
                          LEFT JOIN {grade_settings} gs
198
                            ON c.id = gs.courseid
199
                           AND ((gs.name = 'minmaxtouse' AND " . $DB->sql_compare_text('gs.value') . " = '2'))
200
                        ) ns
201
                    WHERE " . $DB->sql_compare_text('ns.gradevalue') . " = '2' $singlecoursesql";
202
    $params['siteminmax'] = $siteminmaxtouse;
203
    $courses = $DB->get_records_sql($courseidsql, $params);
204
    foreach ($courses as $course) {
205
        $possiblecourseids[$course->id] = $course->id;
206
    }
207
 
208
    if (!empty($possiblecourseids)) {
1441 ariadna 209
        [$sql, $params] = $DB->get_in_or_equal($possiblecourseids);
1 efrain 210
        // A calculated grade item grade min != 0 and grade max != 100 and the course setting is set to
211
        // "Initial min and max grades".
212
        $coursesql = "SELECT DISTINCT courseid
213
                        FROM {grade_items}
214
                       WHERE calculation IS NOT NULL
215
                         AND itemtype = 'manual'
216
                         AND (grademax <> 100 OR grademin <> 0)
217
                         AND courseid $sql";
218
        $affectedcourses = $DB->get_records_sql($coursesql, $params);
219
    }
220
 
221
    // Check for second type of affected courses.
222
    // If we already have the courseid parameter set in the affectedcourses then there is no need to run through this section.
223
    if (!isset($courseid) || !in_array($courseid, $affectedcourses)) {
224
        $singlecoursesql = '';
225
        $params = array();
226
        if (isset($courseid)) {
227
            $singlecoursesql = "AND courseid = :courseid";
228
            $params['courseid'] = $courseid;
229
        }
230
        $nestedsql = "SELECT id
231
                        FROM {grade_items}
232
                       WHERE itemtype = 'category'
233
                         AND calculation IS NOT NULL $singlecoursesql";
234
        $calculatedgradecategories = $DB->get_records_sql($nestedsql, $params);
235
        $categoryids = array();
236
        foreach ($calculatedgradecategories as $key => $gradecategory) {
237
            $categoryids[$key] = $gradecategory->id;
238
        }
239
 
240
        if (!empty($categoryids)) {
1441 ariadna 241
            [$sql, $params] = $DB->get_in_or_equal($categoryids);
1 efrain 242
            // A category with a calculation where the raw grade min and the raw grade max don't match the grade min and grade max
243
            // for the category.
244
            $coursesql = "SELECT DISTINCT gi.courseid
245
                            FROM {grade_grades} gg, {grade_items} gi
246
                           WHERE gi.id = gg.itemid
247
                             AND (gg.rawgrademax <> gi.grademax OR gg.rawgrademin <> gi.grademin)
248
                             AND gi.id $sql";
249
            $additionalcourses = $DB->get_records_sql($coursesql, $params);
250
            foreach ($additionalcourses as $key => $additionalcourse) {
251
                if (!array_key_exists($key, $affectedcourses)) {
252
                    $affectedcourses[$key] = $additionalcourse;
253
                }
254
            }
255
        }
256
    }
257
 
258
    foreach ($affectedcourses as $affectedcourseid) {
259
        if (isset($CFG->upgrade_calculatedgradeitemsonlyregrade) && !($courseid)) {
260
            $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $affectedcourseid->courseid));
261
        } else {
262
            // Check to see if the gradebook freeze is already in affect.
263
            $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $affectedcourseid->courseid);
264
            if (!$gradebookfreeze) {
265
                set_config('gradebook_calculations_freeze_' . $affectedcourseid->courseid, 20150627);
266
            }
267
        }
268
    }
269
}
270
 
271
/**
272
 * This function creates a default separated/connected scale
1441 ariadna 273
 * so there's something in the database.
1 efrain 274
 *
275
 * @global object
276
 * @return void
277
 */
278
function make_default_scale() {
279
    global $DB;
280
 
281
    $defaultscale = new stdClass();
282
    $defaultscale->courseid = 0;
283
    $defaultscale->userid = 0;
284
    $defaultscale->name  = get_string('separateandconnected');
285
    $defaultscale->description = get_string('separateandconnectedinfo');
1441 ariadna 286
    $defaultscale->scale = get_string('separateandconnected1') . ',' .
287
        get_string('separateandconnected2') . ',' .
288
        get_string('separateandconnected3');
1 efrain 289
    $defaultscale->timemodified = time();
290
 
291
    $defaultscale->id = $DB->insert_record('scale', $defaultscale);
292
    return $defaultscale;
293
}
294
 
295
 
296
/**
297
 * Create another default scale.
298
 *
299
 * @param int $oldversion
300
 * @return bool always true
301
 */
302
function make_competence_scale() {
303
    global $DB;
304
 
305
    $defaultscale = new stdClass();
306
    $defaultscale->courseid = 0;
307
    $defaultscale->userid = 0;
308
    $defaultscale->name  = get_string('defaultcompetencescale');
309
    $defaultscale->description = get_string('defaultcompetencescaledesc');
310
    $defaultscale->scale = get_string('defaultcompetencescalenotproficient').','.
311
                           get_string('defaultcompetencescaleproficient');
312
    $defaultscale->timemodified = time();
313
 
314
    $defaultscale->id = $DB->insert_record('scale', $defaultscale);
315
    return $defaultscale;
316
}
317
 
318
/**
319
 * Marks all courses that require rounded grade items be updated.
320
 *
321
 * Used during upgrade and in course restore process.
322
 *
323
 * This upgrade script is needed because it has been decided that if a grade is rounded up, and it will changed a letter
324
 * grade or satisfy a course completion grade criteria, then it should be set as so, and the letter will be awarded and or
325
 * the course completion grade will be awarded.
326
 *
327
 * @param int $courseid Specify a course ID to run this script on just one course.
328
 */
329
function upgrade_course_letter_boundary($courseid = null) {
330
    global $DB, $CFG;
331
 
332
    $coursesql = '';
333
    $params = array('contextlevel' => CONTEXT_COURSE);
334
    if (!empty($courseid)) {
335
        $coursesql = 'AND c.id = :courseid';
336
        $params['courseid'] = $courseid;
337
    }
338
 
339
    // Check to see if the system letter boundaries are borked.
340
    $systemcontext = context_system::instance();
341
    $systemneedsfreeze = upgrade_letter_boundary_needs_freeze($systemcontext);
342
 
343
    // Check the setting for showing the letter grade in a column (default is false).
344
    $usergradelettercolumnsetting = 0;
345
    if (isset($CFG->grade_report_user_showlettergrade)) {
346
        $usergradelettercolumnsetting = (int)$CFG->grade_report_user_showlettergrade;
347
    }
348
    $lettercolumnsql = '';
349
    if ($usergradelettercolumnsetting) {
350
        // The system default is to show a column with letters (and the course uses the defaults).
351
        $lettercolumnsql = '(gss.value is NULL OR ' . $DB->sql_compare_text('gss.value') .  ' <> \'0\')';
352
    } else {
353
        // The course displays a column with letters.
354
        $lettercolumnsql = $DB->sql_compare_text('gss.value') .  ' = \'1\'';
355
    }
356
 
357
    // 3, 13, 23, 31, and 32 are the grade display types that incorporate showing letters. See lib/grade/constants/php.
358
    $systemusesletters = (int) (isset($CFG->grade_displaytype) && in_array($CFG->grade_displaytype, array(3, 13, 23, 31, 32)));
359
    $systemletters = $systemusesletters || $usergradelettercolumnsetting;
360
 
361
    $contextselect = context_helper::get_preload_record_columns_sql('ctx');
362
 
363
    if ($systemletters && $systemneedsfreeze) {
364
        // Select courses with no grade setting for display and a grade item that is using the default display,
365
        // but have not altered the course letter boundary configuration. These courses are definitely affected.
366
 
367
        $sql = "SELECT DISTINCT c.id AS courseid
368
                  FROM {course} c
369
                  JOIN {grade_items} gi ON c.id = gi.courseid
370
                  JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
371
             LEFT JOIN {grade_settings} gs ON gs.courseid = c.id AND gs.name = 'displaytype'
372
             LEFT JOIN {grade_settings} gss ON gss.courseid = c.id AND gss.name = 'report_user_showlettergrade'
373
             LEFT JOIN {grade_letters} gl ON gl.contextid = ctx.id
374
                 WHERE gi.display = 0
375
                 AND ((gs.value is NULL)
376
                      AND ($lettercolumnsql))
377
                 AND gl.id is NULL $coursesql";
378
        $affectedcourseids = $DB->get_recordset_sql($sql, $params);
379
        foreach ($affectedcourseids as $courseid) {
380
            set_config('gradebook_calculations_freeze_' . $courseid->courseid, 20160518);
381
        }
382
        $affectedcourseids->close();
383
    }
384
 
385
    // If the system letter boundary is okay proceed to check grade item and course grade display settings.
386
    $sql = "SELECT DISTINCT c.id AS courseid, $contextselect
387
              FROM {course} c
388
              JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
389
              JOIN {grade_items} gi ON c.id = gi.courseid
390
         LEFT JOIN {grade_settings} gs ON c.id = gs.courseid AND gs.name = 'displaytype'
391
         LEFT JOIN {grade_settings} gss ON gss.courseid = c.id AND gss.name = 'report_user_showlettergrade'
392
             WHERE
393
                (
394
                    -- A grade item is using letters
395
                    (gi.display IN (3, 13, 23, 31, 32))
396
                    -- OR the course is using letters
397
                    OR (" . $DB->sql_compare_text('gs.value') . " IN ('3', '13', '23', '31', '32')
398
                        -- OR the course using the system default which is letters
399
                        OR (gs.value IS NULL AND $systemusesletters = 1)
400
                    )
401
                    OR ($lettercolumnsql)
402
                )
403
                -- AND the course matches
404
                $coursesql";
405
 
406
    $potentialcourses = $DB->get_recordset_sql($sql, $params);
407
 
408
    foreach ($potentialcourses as $value) {
409
        $gradebookfreeze = 'gradebook_calculations_freeze_' . $value->courseid;
410
 
411
        // Check also if this course id has already been frozen.
412
        // If we already have this course ID then move on to the next record.
413
        if (!property_exists($CFG, $gradebookfreeze)) {
414
            // Check for 57 letter grade issue.
415
            context_helper::preload_from_record($value);
416
            $coursecontext = context_course::instance($value->courseid);
417
            if (upgrade_letter_boundary_needs_freeze($coursecontext)) {
418
                // We have a course with a possible score standardisation problem. Flag for freeze.
419
                // Flag this course as being frozen.
420
                set_config('gradebook_calculations_freeze_' . $value->courseid, 20160518);
421
            }
422
        }
423
    }
424
    $potentialcourses->close();
425
}
426
 
427
/**
428
 * Checks the letter boundary of the provided context to see if it needs freezing.
429
 * Each letter boundary is tested to see if receiving that boundary number will
430
 * result in achieving the cosponsoring letter.
431
 *
432
 * @param object $context Context object
433
 * @return bool if the letter boundary for this context should be frozen.
434
 */
435
function upgrade_letter_boundary_needs_freeze($context) {
436
    global $DB;
437
 
438
    $contexts = $context->get_parent_context_ids();
439
    array_unshift($contexts, $context->id);
440
 
441
    foreach ($contexts as $ctxid) {
442
 
443
        $letters = $DB->get_records_menu('grade_letters', array('contextid' => $ctxid), 'lowerboundary DESC',
444
                'lowerboundary, letter');
445
 
446
        if (!empty($letters)) {
447
            foreach ($letters as $boundary => $notused) {
448
                $standardisedboundary = upgrade_standardise_score($boundary, 0, 100, 0, 100);
449
                if ($standardisedboundary < $boundary) {
450
                    return true;
451
                }
452
            }
453
            // We found letters but we have no boundary problem.
454
            return false;
455
        }
456
    }
457
    return false;
458
}
459
 
460
/**
461
 * Given a float value situated between a source minimum and a source maximum, converts it to the
462
 * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
463
 * for the formula :-)
464
 *
465
 * @param float $rawgrade
466
 * @param float $sourcemin
467
 * @param float $sourcemax
468
 * @param float $targetmin
469
 * @param float $targetmax
470
 * @return float Converted value
471
 */
472
function upgrade_standardise_score($rawgrade, $sourcemin, $sourcemax, $targetmin, $targetmax) {
473
    if (is_null($rawgrade)) {
474
        return null;
475
    }
476
 
477
    if ($sourcemax == $sourcemin or $targetmin == $targetmax) {
478
        // Prevent division by 0.
479
        return $targetmax;
480
    }
481
 
482
    $factor = ($rawgrade - $sourcemin) / ($sourcemax - $sourcemin);
483
    $diff = $targetmax - $targetmin;
484
    $standardisedvalue = $factor * $diff + $targetmin;
485
    return $standardisedvalue;
486
}
487
 
488
/**
489
 * Provides a way to check and update a serialized string that uses the deprecated object class.
490
 *
491
 * @param  string $serializeddata Serialized string which may contain the now deprecated object.
492
 * @return array Returns an array where the first variable is a bool with a status of whether the initial data was changed
493
 * or not. The second variable is the said data.
494
 */
495
function upgrade_fix_serialized_objects($serializeddata) {
496
    $updated = false;
497
    if (strpos($serializeddata, ":6:\"object") !== false) {
498
        $serializeddata = str_replace(":6:\"object", ":8:\"stdClass", $serializeddata);
499
        $updated = true;
500
    }
501
    return [$updated, $serializeddata];
502
}
503
 
504
/**
505
 * Deletes file records which have their repository deleted.
506
 *
507
 */
508
function upgrade_delete_orphaned_file_records() {
509
    global $DB;
510
 
511
    $sql = "SELECT f.id, f.contextid, f.component, f.filearea, f.itemid, fr.id AS referencefileid
512
              FROM {files} f
513
              JOIN {files_reference} fr ON f.referencefileid = fr.id
514
         LEFT JOIN {repository_instances} ri ON fr.repositoryid = ri.id
515
             WHERE ri.id IS NULL";
516
 
517
    $deletedfiles = $DB->get_recordset_sql($sql);
518
 
519
    $deletedfileids = array();
520
 
521
    $fs = get_file_storage();
522
    foreach ($deletedfiles as $deletedfile) {
523
        $fs->delete_area_files($deletedfile->contextid, $deletedfile->component, $deletedfile->filearea, $deletedfile->itemid);
524
        $deletedfileids[] = $deletedfile->referencefileid;
525
    }
526
    $deletedfiles->close();
527
 
528
    $DB->delete_records_list('files_reference', 'id', $deletedfileids);
529
}
530
 
531
/**
532
 * Upgrade core licenses shipped with Moodle.
533
 */
534
function upgrade_core_licenses() {
535
    global $CFG, $DB;
536
 
537
    $expectedlicenses = json_decode(file_get_contents($CFG->dirroot . '/lib/licenses.json'))->licenses;
538
    if (!is_array($expectedlicenses)) {
539
        $expectedlicenses = [];
540
    }
541
    $corelicenses = $DB->get_records('license', ['custom' => 0]);
542
 
543
    // Disable core licenses which are no longer current.
544
    $todisable = array_diff(
545
        array_map(fn ($license) => $license->shortname, $corelicenses),
546
        array_map(fn ($license) => $license->shortname, $expectedlicenses),
547
    );
548
 
549
    // Disable any old *core* license that does not exist in the licenses.json file.
550
    if (count($todisable)) {
551
        [$where, $params] = $DB->get_in_or_equal($todisable, SQL_PARAMS_NAMED);
552
        $DB->set_field_select(
553
            'license',
554
            'enabled',
555
            0,
556
            "shortname {$where}",
557
            $params
558
        );
559
    }
560
 
561
    // Add any new licenses.
562
    foreach ($expectedlicenses as $expectedlicense) {
563
        if (!$expectedlicense->enabled) {
564
            // Skip any license which is no longer enabled.
565
            continue;
566
        }
567
        if (!$DB->record_exists('license', ['shortname' => $expectedlicense->shortname])) {
568
            // If the license replaces an older one, check whether this old license was enabled or not.
569
            $isreplacement = false;
570
            foreach (array_reverse($expectedlicense->replaces ?? []) as $item) {
571
                foreach ($corelicenses as $corelicense) {
572
                    if ($corelicense->shortname === $item) {
573
                        $expectedlicense->enabled = $corelicense->enabled;
574
                        // Also, keep the old sort order.
575
                        $expectedlicense->sortorder = $corelicense->sortorder * 100;
576
                        $isreplacement = true;
577
                        break 2;
578
                    }
579
                }
580
            }
581
            if (!isset($CFG->upgraderunning) || during_initial_install() || $isreplacement) {
582
                // Only install missing core licenses if not upgrading or during initial installation.
583
                $DB->insert_record('license', $expectedlicense);
584
            }
585
        }
586
    }
587
 
588
    // Add/renumber sortorder to all licenses.
589
    $licenses = $DB->get_records('license', null, 'sortorder');
590
    $sortorder = 1;
591
    foreach ($licenses as $license) {
592
        $license->sortorder = $sortorder++;
593
        $DB->update_record('license', $license);
594
    }
595
 
596
    // Set the license config values, used by file repository for rendering licenses at front end.
597
    $activelicenses = $DB->get_records_menu('license', ['enabled' => 1], 'id', 'id, shortname');
598
    set_config('licenses', implode(',', $activelicenses));
599
 
600
    $sitedefaultlicense = get_config('', 'sitedefaultlicense');
601
    if (empty($sitedefaultlicense) || !in_array($sitedefaultlicense, $activelicenses)) {
602
        set_config('sitedefaultlicense', reset($activelicenses));
603
    }
604
}
605
 
606
/**
607
 * Detects if the site may need to get the calendar events fixed or no. With optional output.
608
 *
609
 * @param bool $output true if the function must output information, false if not.
610
 * @return bool true if the site needs to run the fixes, false if not.
611
 */
612
function upgrade_calendar_site_status(bool $output = true): bool {
613
    global $DB;
614
 
615
    // List of upgrade steps where the bug happened.
616
    $badsteps = [
617
        '3.9.5'   => '2020061504.08',
618
        '3.10.2'  => '2020110901.09',
619
        '3.11dev' => '2021022600.02',
620
        '4.0dev'  => '2021052500.65',
621
    ];
622
 
623
    // List of upgrade steps that ran the fixer.
624
    $fixsteps = [
625
        '3.9.6+'  => '2020061506.05',
626
        '3.10.3+' => '2020110903.05',
627
        '3.11dev' => '2021042100.02',
628
        '4.0dev'  => '2021052500.85',
629
    ];
630
 
631
    $targetsteps = array_merge(array_values($badsteps), array_values( $fixsteps));
1441 ariadna 632
    [$insql, $inparams] = $DB->get_in_or_equal($targetsteps);
1 efrain 633
    $foundsteps = $DB->get_fieldset_sql("
634
        SELECT DISTINCT version
635
          FROM {upgrade_log}
636
         WHERE plugin = 'core'
637
           AND version " . $insql . "
638
      ORDER BY version", $inparams);
639
 
640
    // Analyse the found steps, to decide if the site needs upgrading or no.
641
    $badfound = false;
642
    $fixfound = false;
643
    foreach ($foundsteps as $foundstep) {
644
        $badfound = $badfound ?: array_search($foundstep, $badsteps, true);
645
        $fixfound = $fixfound ?: array_search($foundstep, $fixsteps, true);
646
    }
647
    $needsfix = $badfound && !$fixfound;
648
 
649
    // Let's output some textual information if required to.
650
    if ($output) {
651
        mtrace("");
652
        if ($badfound) {
653
            mtrace("This site has executed the problematic upgrade step {$badsteps[$badfound]} present in {$badfound}.");
654
        } else {
655
            mtrace("Problematic upgrade steps were NOT found, site should be safe.");
656
        }
657
        if ($fixfound) {
658
            mtrace("This site has executed the fix upgrade step {$fixsteps[$fixfound]} present in {$fixfound}.");
659
        } else {
660
            mtrace("Fix upgrade steps were NOT found.");
661
        }
662
        mtrace("");
663
        if ($needsfix) {
664
            mtrace("This site NEEDS to run the calendar events fix!");
665
            mtrace('');
666
            mtrace("You can use this CLI tool or upgrade to a version of Moodle that includes");
667
            mtrace("the fix and will be executed as part of the normal upgrade procedure.");
668
            mtrace("The following versions or up are known candidates to upgrade to:");
669
            foreach ($fixsteps as $key => $value) {
670
                mtrace("  - {$key}: {$value}");
671
            }
672
            mtrace("");
673
        }
674
    }
675
    return $needsfix;
676
}
677
 
678
/**
679
 * Detects the calendar events needing to be fixed. With optional output.
680
 *
681
 * @param bool $output true if the function must output information, false if not.
682
 * @return stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
683
 */
684
function upgrade_calendar_events_status(bool $output = true): array {
685
    global $DB;
686
 
687
    // Calculate the list of standard (core) activity plugins.
688
    $plugins = core_plugin_manager::standard_plugins_list('mod');
689
    $coremodules = "modulename IN ('" . implode("', '", $plugins) . "')";
690
 
691
    // Some query parts go here.
692
    $brokenevents = "(userid = 0 AND (eventtype <> 'user' OR priority <> 0))"; // From the original bad upgrade step.
693
    $standardevents = "(eventtype IN ('site', 'category', 'course', 'group', 'user') AND subscriptionid IS NULL)";
694
    $subscriptionevents = "(subscriptionid IS NOT NULL)";
695
    $overrideevents = "({$coremodules} AND priority IS NOT NULL)";
696
    $actionevents = "({$coremodules} AND instance > 0 and priority IS NULL)";
697
    $otherevents = "(NOT ({$standardevents} OR {$subscriptionevents} OR {$overrideevents} OR {$actionevents}))";
698
 
699
    // Detailed query template.
700
    $detailstemplate = "
701
        SELECT ##group## AS groupname, COUNT(1) AS count
702
          FROM {event}
703
         WHERE ##groupconditions##
704
      GROUP BY ##group##";
705
 
706
    // Count total and potentially broken events.
707
    $total = $DB->count_records_select('event', '');
708
    $totalbadsql = $brokenevents;
709
    $totalbad = $DB->count_records_select('event', $totalbadsql);
710
 
711
    // Standard events.
712
    $standard = $DB->count_records_select('event', $standardevents);
713
    $standardbadsql = "{$brokenevents} AND {$standardevents}";
714
    $standardbad = $DB->count_records_select('event', $standardbadsql);
715
    $standarddetails = $DB->get_records_sql(
716
        str_replace(
717
            ['##group##', '##groupconditions##'],
718
            ['eventtype', $standardbadsql],
719
            $detailstemplate
720
        )
721
    );
722
    array_walk($standarddetails, function (&$rec) {
723
        $rec = $rec->groupname . ': ' . $rec->count;
724
    });
725
    $standarddetails = $standarddetails ? '(' . implode(', ', $standarddetails) . ')' : '- all good!';
726
 
727
    // Subscription events.
728
    $subscription = $DB->count_records_select('event', $subscriptionevents);
729
    $subscriptionbadsql = "{$brokenevents} AND {$subscriptionevents}";
730
    $subscriptionbad = $DB->count_records_select('event', $subscriptionbadsql);
731
    $subscriptiondetails = $DB->get_records_sql(
732
        str_replace(
733
            ['##group##', '##groupconditions##'],
734
            ['eventtype', $subscriptionbadsql],
735
            $detailstemplate
736
        )
737
    );
738
    array_walk($subscriptiondetails, function (&$rec) {
739
        $rec = $rec->groupname . ': ' . $rec->count;
740
    });
741
    $subscriptiondetails = $subscriptiondetails ? '(' . implode(', ', $subscriptiondetails) . ')' : '- all good!';
742
 
743
    // Override events.
744
    $override = $DB->count_records_select('event', $overrideevents);
745
    $overridebadsql = "{$brokenevents} AND {$overrideevents}";
746
    $overridebad = $DB->count_records_select('event', $overridebadsql);
747
    $overridedetails = $DB->get_records_sql(
748
        str_replace(
749
            ['##group##', '##groupconditions##'],
750
            ['modulename', $overridebadsql],
751
            $detailstemplate
752
        )
753
    );
754
    array_walk($overridedetails, function (&$rec) {
755
        $rec = $rec->groupname . ': ' . $rec->count;
756
    });
757
    $overridedetails = $overridedetails ? '(' . implode(', ', $overridedetails) . ')' : '- all good!';
758
 
759
    // Action events.
760
    $action = $DB->count_records_select('event', $actionevents);
761
    $actionbadsql = "{$brokenevents} AND {$actionevents}";
762
    $actionbad = $DB->count_records_select('event', $actionbadsql);
763
    $actiondetails = $DB->get_records_sql(
764
        str_replace(
765
            ['##group##', '##groupconditions##'],
766
            ['modulename', $actionbadsql],
767
            $detailstemplate
768
        )
769
    );
770
    array_walk($actiondetails, function (&$rec) {
771
        $rec = $rec->groupname . ': ' . $rec->count;
772
    });
773
    $actiondetails = $actiondetails ? '(' . implode(', ', $actiondetails) . ')' : '- all good!';
774
 
775
    // Other events.
776
    $other = $DB->count_records_select('event', $otherevents);
777
    $otherbadsql = "{$brokenevents} AND {$otherevents}";
778
    $otherbad = $DB->count_records_select('event', $otherbadsql);
779
    $otherdetails = $DB->get_records_sql(
780
        str_replace(
781
            ['##group##', '##groupconditions##'],
782
            ['COALESCE(component, modulename)', $otherbadsql],
783
            $detailstemplate
784
        )
785
    );
786
    array_walk($otherdetails, function (&$rec) {
787
        $rec = ($rec->groupname ?: 'unknown') . ': ' . $rec->count;
788
    });
789
    $otherdetails = $otherdetails ? '(' . implode(', ', $otherdetails) . ')' : '- all good!';
790
 
791
    // Let's output some textual information if required to.
792
    if ($output) {
793
        mtrace("");
794
        mtrace("Totals: {$total} / {$totalbad} (total / wrong)");
795
        mtrace("  - standards events: {$standard} / {$standardbad} {$standarddetails}");
796
        mtrace("  - subscription events: {$subscription} / {$subscriptionbad} {$subscriptiondetails}");
797
        mtrace("  - override events: {$override} / {$overridebad} {$overridedetails}");
798
        mtrace("  - action events: {$action} / {$actionbad} {$actiondetails}");
799
        mtrace("  - other events: {$other} / {$otherbad} {$otherdetails}");
800
        mtrace("");
801
    }
802
 
803
    return [
804
        'total' => (object)['count' => $total, 'bad' => $totalbad, 'sql' => $totalbadsql],
805
        'standard' => (object)['count' => $standard, 'bad' => $standardbad, 'sql' => $standardbadsql],
806
        'subscription' => (object)['count' => $subscription, 'bad' => $subscriptionbad, 'sql' => $subscriptionbadsql],
807
        'override' => (object)['count' => $override, 'bad' => $overridebad, 'sql' => $overridebadsql],
808
        'action' => (object)['count' => $action, 'bad' => $actionbad, 'sql' => $actionbadsql],
809
        'other' => (object)['count' => $other, 'bad' => $otherbad, 'sql' => $otherbadsql],
810
    ];
811
}
812
 
813
/**
814
 * Detects the calendar events needing to be fixed. With optional output.
815
 *
816
 * @param stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
817
 * @param bool $output true if the function must output information, false if not.
818
 * @param int $maxseconds Number of seconds the function will run as max, with zero meaning no limit.
819
 * @return bool true if the function has not finished fixing everything, false if it has finished.
820
 */
821
function upgrade_calendar_events_fix_remaining(array $info, bool $output = true, int $maxseconds = 0): bool {
822
    global $DB;
823
 
824
    upgrade_calendar_events_mtrace('', $output);
825
 
826
    // Initial preparations.
827
    $starttime = time();
828
    $endtime = $maxseconds ? ($starttime + $maxseconds) : 0;
829
 
830
    // No bad events, or all bad events are "other" events, finished.
831
    if ($info['total']->bad == 0 || $info['total']->bad == $info['other']->bad) {
832
        return false;
833
    }
834
 
835
    // Let's fix overriden events first (they are the ones performing worse with the missing userid).
836
    if ($info['override']->bad != 0) {
837
        if (upgrade_calendar_override_events_fix($info['override'], $output, $endtime)) {
838
            return true; // Not finished yet.
839
        }
840
    }
841
 
842
    // Let's fix the subscription events (like standard ones, but with the event_subscriptions table).
843
    if ($info['subscription']->bad != 0) {
844
        if (upgrade_calendar_subscription_events_fix($info['subscription'], $output, $endtime)) {
845
            return true; // Not finished yet.
846
        }
847
    }
848
 
849
    // Let's fix the standard events (site, category, course, group).
850
    if ($info['standard']->bad != 0) {
851
        if (upgrade_calendar_standard_events_fix($info['standard'], $output, $endtime)) {
852
            return true; // Not finished yet.
853
        }
854
    }
855
 
856
    // Let's fix the action events (all them are "general" ones, not user-specific in core).
857
    if ($info['action']->bad != 0) {
858
        if (upgrade_calendar_action_events_fix($info['action'], $output, $endtime)) {
859
            return true; // Not finished yet.
860
        }
861
    }
862
 
863
    // Have arrived here, finished!
864
    return false;
865
}
866
 
867
/**
868
 * Wrapper over mtrace() to allow a few more things to be specified.
869
 *
870
 * @param string $string string to output.
871
 * @param bool $output true to perform the output, false to avoid it.
872
 */
873
function upgrade_calendar_events_mtrace(string $string, bool $output): void {
874
    static $cols = 0;
875
 
876
    // No output, do nothing.
877
    if (!$output) {
878
        return;
879
    }
880
 
881
    // Printing dots... let's output them slightly nicer.
882
    if ($string === '.') {
883
        $cols++;
884
        // Up to 60 cols.
885
        if ($cols < 60) {
886
            mtrace($string, '');
887
        } else {
888
            mtrace($string);
889
            $cols = 0;
890
        }
891
        return;
892
    }
893
 
894
    // Reset cols, have ended printing dots.
895
    if ($cols) {
896
        $cols = 0;
897
        mtrace('');
898
    }
899
 
900
    // Normal output.
901
    mtrace($string);
902
}
903
 
904
/**
905
 * Get a valid editing teacher for a given courseid
906
 *
907
 * @param int $courseid The course to look for editing teachers.
908
 * @return int A user id of an editing teacher or, if missing, the admin userid.
909
 */
910
function upgrade_calendar_events_get_teacherid(int $courseid): int {
911
 
912
    if ($context = context_course::instance($courseid, IGNORE_MISSING)) {
913
        if ($havemanage = get_users_by_capability($context, 'moodle/course:manageactivities', 'u.id')) {
914
            return array_keys($havemanage)[0];
915
        }
916
    }
917
    return get_admin()->id; // Could not find a teacher, default to admin.
918
}
919
 
920
/**
921
 * Detects the calendar standard events needing to be fixed. With optional output.
922
 *
923
 * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
924
 * @param bool $output true if the function must output information, false if not.
925
 * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
926
 * @return bool true if the function has not finished fixing everything, false if it has finished.
927
 */
928
function upgrade_calendar_standard_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
929
    global $DB;
930
 
931
    $return = false; // Let's assume the function is going to finish by default.
932
    $status = "Finished!"; // To decide the message to be presented on return.
933
 
934
    upgrade_calendar_events_mtrace('Processing standard events', $output);
935
 
936
    $rs = $DB->get_recordset_sql("
937
        SELECT DISTINCT eventtype, courseid
938
          FROM {event}
939
         WHERE {$info->sql}");
940
 
941
    foreach ($rs as $record) {
942
        switch ($record->eventtype) {
943
            case 'site':
944
            case 'category':
945
                // These are created by admin.
946
                $DB->set_field('event', 'userid', get_admin()->id, ['eventtype' => $record->eventtype]);
947
                break;
948
            case 'course':
949
            case 'group':
950
                // These are created by course teacher.
951
                $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
952
                    ['eventtype' => $record->eventtype, 'courseid' => $record->courseid]);
953
                break;
954
        }
955
 
956
        // Cutoff time, let's exit.
957
        if ($endtime && $endtime <= time()) {
958
            $status = 'Remaining standard events pending';
959
            $return = true; // Not finished yet.
960
            break;
961
        }
962
        upgrade_calendar_events_mtrace('.', $output);
963
    }
964
    $rs->close();
965
    upgrade_calendar_events_mtrace($status, $output);
966
    upgrade_calendar_events_mtrace('', $output);
967
    return $return;
968
}
969
 
970
/**
971
 * Detects the calendar subscription events needing to be fixed. With optional output.
972
 *
973
 * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
974
 * @param bool $output true if the function must output information, false if not.
975
 * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
976
 * @return bool true if the function has not finished fixing everything, false if it has finished.
977
 */
978
function upgrade_calendar_subscription_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
979
    global $DB;
980
 
981
    $return = false; // Let's assume the function is going to finish by default.
982
    $status = "Finished!"; // To decide the message to be presented on return.
983
 
984
    upgrade_calendar_events_mtrace('Processing subscription events', $output);
985
 
986
    $rs = $DB->get_recordset_sql("
987
        SELECT DISTINCT subscriptionid AS id
988
          FROM {event}
989
         WHERE {$info->sql}");
990
 
991
    foreach ($rs as $subscription) {
992
        // Subscriptions can be site or category level, let's put the admin as userid.
993
        // (note that "user" subscription weren't deleted so there is nothing to recover with them.
994
        $DB->set_field('event_subscriptions', 'userid', get_admin()->id, ['id' => $subscription->id]);
995
        $DB->set_field('event', 'userid', get_admin()->id, ['subscriptionid' => $subscription->id]);
996
 
997
        // Cutoff time, let's exit.
998
        if ($endtime && $endtime <= time()) {
999
            $status = 'Remaining subscription events pending';
1000
            $return = true; // Not finished yet.
1001
            break;
1002
        }
1003
        upgrade_calendar_events_mtrace('.', $output);
1004
    }
1005
    $rs->close();
1006
    upgrade_calendar_events_mtrace($status, $output);
1007
    upgrade_calendar_events_mtrace('', $output);
1008
    return $return;
1009
}
1010
 
1011
/**
1012
 * Detects the calendar action events needing to be fixed. With optional output.
1013
 *
1014
 * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1015
 * @param bool $output true if the function must output information, false if not.
1016
 * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1017
 * @return bool true if the function has not finished fixing everything, false if it has finished.
1018
 */
1019
function upgrade_calendar_action_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1020
    global $DB;
1021
 
1022
    $return = false; // Let's assume the function is going to finish by default.
1023
    $status = "Finished!"; // To decide the message to be presented on return.
1024
 
1025
    upgrade_calendar_events_mtrace('Processing action events', $output);
1026
 
1027
    $rs = $DB->get_recordset_sql("
1028
        SELECT DISTINCT modulename, instance, courseid
1029
          FROM {event}
1030
         WHERE {$info->sql}");
1031
 
1032
    foreach ($rs as $record) {
1033
        // These are created by course teacher.
1034
        $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
1035
            ['modulename' => $record->modulename, 'instance' => $record->instance, 'courseid' => $record->courseid]);
1036
 
1037
        // Cutoff time, let's exit.
1038
        if ($endtime && $endtime <= time()) {
1039
            $status = 'Remaining action events pending';
1040
            $return = true; // Not finished yet.
1041
            break;
1042
        }
1043
        upgrade_calendar_events_mtrace('.', $output);
1044
    }
1045
    $rs->close();
1046
    upgrade_calendar_events_mtrace($status, $output);
1047
    upgrade_calendar_events_mtrace('', $output);
1048
    return $return;
1049
}
1050
 
1051
/**
1052
 * Detects the calendar override events needing to be fixed. With optional output.
1053
 *
1054
 * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1055
 * @param bool $output true if the function must output information, false if not.
1056
 * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1057
 * @return bool true if the function has not finished fixing everything, false if it has finished.
1058
 */
1059
function upgrade_calendar_override_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1060
    global $CFG, $DB;
1061
 
1062
    include_once($CFG->dirroot. '/course/lib.php');
1063
    include_once($CFG->dirroot. '/mod/assign/lib.php');
1064
    include_once($CFG->dirroot. '/mod/assign/locallib.php');
1065
    include_once($CFG->dirroot. '/mod/lesson/lib.php');
1066
    include_once($CFG->dirroot. '/mod/lesson/locallib.php');
1067
    include_once($CFG->dirroot. '/mod/quiz/lib.php');
1068
    include_once($CFG->dirroot. '/mod/quiz/locallib.php');
1069
 
1070
    $return = false; // Let's assume the function is going to finish by default.
1071
    $status = "Finished!"; // To decide the message to be presented on return.
1072
 
1073
    upgrade_calendar_events_mtrace('Processing override events', $output);
1074
 
1075
    $rs = $DB->get_recordset_sql("
1076
        SELECT DISTINCT modulename, instance
1077
          FROM {event}
1078
         WHERE {$info->sql}");
1079
 
1080
    foreach ($rs as $module) {
1081
        // Remove all the records from the events table for the module.
1082
        $DB->delete_records('event', ['modulename' => $module->modulename, 'instance' => $module->instance]);
1083
 
1084
        // Get the activity record.
1085
        if (!$activityrecord = $DB->get_record($module->modulename, ['id' => $module->instance])) {
1086
            // Orphaned calendar event (activity doesn't exists), skip.
1087
            continue;
1088
        }
1089
 
1090
        // Let's rebuild it by calling to each module API.
1091
        switch ($module->modulename) {
1092
            case 'assign';
1093
                if (function_exists('assign_prepare_update_events')) {
1094
                    assign_prepare_update_events($activityrecord);
1095
                }
1096
                break;
1097
            case 'lesson':
1098
                if (function_exists('lesson_update_events')) {
1099
                    lesson_update_events($activityrecord);
1100
                }
1101
                break;
1102
            case 'quiz':
1103
                if (function_exists('quiz_update_events')) {
1104
                    quiz_update_events($activityrecord);
1105
                }
1106
                break;
1107
        }
1108
 
1109
        // Sometimes, some (group) overrides are created without userid, when that happens, they deserve
1110
        // some user (teacher or admin). This doesn't affect to groups calendar events behaviour,
1111
        // but allows counters to detect already processed group overrides and makes things
1112
        // consistent.
1113
        $DB->set_field_select('event', 'userid', upgrade_calendar_events_get_teacherid($activityrecord->course),
1114
            'modulename = ? AND instance = ? and priority != 0 and userid = 0',
1115
            ['modulename' => $module->modulename, 'instance' => $module->instance]);
1116
 
1117
        // Cutoff time, let's exit.
1118
        if ($endtime && $endtime <= time()) {
1119
            $status = 'Remaining override events pending';
1120
            $return = true; // Not finished yet.
1121
            break;
1122
        }
1123
        upgrade_calendar_events_mtrace('.', $output);
1124
    }
1125
    $rs->close();
1126
    upgrade_calendar_events_mtrace($status, $output);
1127
    upgrade_calendar_events_mtrace('', $output);
1128
    return $return;
1129
}
1130
 
1131
/**
1132
 * Add a new item at the end of the usermenu.
1133
 *
1134
 * @param string $menuitem
1135
 */
1136
function upgrade_add_item_to_usermenu(string $menuitem): void {
1137
    global $CFG;
1138
 
1139
    // Get current configuration data.
1140
    $currentcustomusermenuitems = str_replace(["\r\n", "\r"], "\n", $CFG->customusermenuitems);
1141
    $lines = preg_split('/\n/', $currentcustomusermenuitems, -1, PREG_SPLIT_NO_EMPTY);
1142
    $lines = array_map('trim', $lines);
1143
 
1144
    if (!in_array($menuitem, $lines)) {
1145
        // Add the item to the menu.
1146
        $lines[] = $menuitem;
1147
        set_config('customusermenuitems', implode("\n", $lines));
1148
    }
1149
}
1150
 
1151
/**
1152
 * Update all instances of a block shown on a pagetype to a new default region, adding missing block instances where
1153
 * none is found.
1154
 *
1155
 * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where
1156
 * the subpagepattern is a string representation of an integer. If there are any string values this will not work.
1157
 *
1158
 * @param string $blockname The block name, without the block_ frankenstyle component
1159
 * @param string $pagename The type of my-page to match
1160
 * @param string $pagetypepattern The page type pattern to match for the block
1161
 * @param string $newdefaultregion The new region to set
1162
 */
1163
function upgrade_block_set_defaultregion(
1164
    string $blockname,
1165
    string $pagename,
1166
    string $pagetypepattern,
1167
    string $newdefaultregion
1168
): void {
1169
    global $DB;
1170
 
1171
    // The subpagepattern is a string.
1172
    // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a
1173
    // community block to do something different.
1174
    // This function is not suited to those cases.
1175
    $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1176
    $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1177
 
1178
    // If a subquery returns any NULL then the NOT IN returns no results at all.
1179
    // By adding a join in the inner select on my_pages we remove any possible nulls and prevent any need for
1180
    // additional casting to filter out the nulls.
1181
    $sql = <<<EOF
1182
        INSERT INTO {block_instances} (
1183
            blockname,
1184
            parentcontextid,
1185
            showinsubcontexts,
1186
            pagetypepattern,
1187
            subpagepattern,
1188
            defaultregion,
1189
            defaultweight,
1190
            timecreated,
1191
            timemodified
1192
        ) SELECT
1193
            :selectblockname AS blockname,
1194
            c.id AS parentcontextid,
1195
 
1196
            :selectpagetypepattern AS pagetypepattern,
1197
            mp.id AS subpagepattern,
1198
            :selectdefaultregion AS defaultregion,
1199
 
1200
            :selecttimecreated AS timecreated,
1201
            :selecttimemodified AS timemodified
1202
          FROM {my_pages} mp
1203
          JOIN {context} c ON c.instanceid = mp.userid AND c.contextlevel = :contextuser
1204
         WHERE mp.id NOT IN (
1205
            SELECT mpi.id FROM {my_pages} mpi
1206
              JOIN {block_instances} bi
1207
                    ON bi.blockname = :blockname
1208
                   AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1209
                   AND bi.pagetypepattern = :pagetypepattern
1210
                   AND {$subpagepattern} = mpi.id
1211
         )
1212
         AND mp.private = 1
1213
         AND mp.name = :pagename
1214
    EOF;
1215
 
1216
    $DB->execute($sql, [
1217
        'selectblockname' => $blockname,
1218
        'contextuser' => CONTEXT_USER,
1219
        'selectpagetypepattern' => $pagetypepattern,
1220
        'selectdefaultregion' => $newdefaultregion,
1221
        'selecttimecreated' => time(),
1222
        'selecttimemodified' => time(),
1223
        'pagetypepattern' => $pagetypepattern,
1224
        'blockname' => $blockname,
1225
        'pagename' => $pagename,
1226
    ]);
1227
 
1228
    // Update the existing instances.
1229
    $sql = <<<EOF
1230
        UPDATE {block_instances}
1231
           SET defaultregion = :newdefaultregion
1232
         WHERE id IN (
1233
            SELECT * FROM (
1234
                SELECT bi.id
1235
                  FROM {my_pages} mp
1236
                  JOIN {block_instances} bi
1237
                        ON bi.blockname = :blockname
1238
                       AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1239
                       AND bi.pagetypepattern = :pagetypepattern
1240
                       AND {$subpagepattern} = mp.id
1241
                 WHERE mp.private = 1
1242
                   AND mp.name = :pagename
1243
                   AND bi.defaultregion <> :existingnewdefaultregion
1244
            ) bid
1245
         )
1246
    EOF;
1247
 
1248
    $DB->execute($sql, [
1249
        'newdefaultregion' => $newdefaultregion,
1250
        'pagetypepattern' => $pagetypepattern,
1251
        'blockname' => $blockname,
1252
        'existingnewdefaultregion' => $newdefaultregion,
1253
        'pagename' => $pagename,
1254
    ]);
1255
 
1256
    // Note: This can be time consuming!
1257
    \context_helper::create_instances(CONTEXT_BLOCK);
1258
}
1259
 
1260
/**
1261
 * Remove all instances of a block on pages of the specified pagetypepattern.
1262
 *
1263
 * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where
1264
 * the subpagepattern is a string representation of an integer. If there are any string values this will not work.
1265
 *
1266
 * @param string $blockname The block name, without the block_ frankenstyle component
1267
 * @param string $pagename The type of my-page to match
1268
 * @param string $pagetypepattern This is typically used on the 'my-index'
1269
 */
1270
function upgrade_block_delete_instances(
1271
    string $blockname,
1272
    string $pagename,
1273
    string $pagetypepattern
1274
): void {
1275
    global $DB;
1276
 
1277
    $deleteblockinstances = function (string $instanceselect, array $instanceparams) use ($DB) {
1278
        $deletesql = <<<EOF
1279
            SELECT c.id AS cid
1280
              FROM {context} c
1281
              JOIN {block_instances} bi ON bi.id = c.instanceid AND c.contextlevel = :contextlevel
1282
             WHERE {$instanceselect}
1283
        EOF;
1284
        $DB->delete_records_subquery('context', 'id', 'cid', $deletesql, array_merge($instanceparams, [
1285
            'contextlevel' => CONTEXT_BLOCK,
1286
        ]));
1287
 
1288
        $deletesql = <<<EOF
1289
            SELECT bp.id AS bpid
1290
              FROM {block_positions} bp
1291
              JOIN {block_instances} bi ON bi.id = bp.blockinstanceid
1292
             WHERE {$instanceselect}
1293
        EOF;
1294
        $DB->delete_records_subquery('block_positions', 'id', 'bpid', $deletesql, $instanceparams);
1295
 
1296
        $blockhidden = $DB->sql_concat("'block'", 'bi.id', "'hidden'");
1297
        $blockdocked = $DB->sql_concat("'docked_block_instance_'", 'bi.id');
1298
        $deletesql = <<<EOF
1299
            SELECT p.id AS pid
1300
              FROM {user_preferences} p
1301
              JOIN {block_instances} bi ON p.name IN ({$blockhidden}, {$blockdocked})
1302
             WHERE {$instanceselect}
1303
        EOF;
1304
        $DB->delete_records_subquery('user_preferences', 'id', 'pid', $deletesql, $instanceparams);
1305
 
1306
        $deletesql = <<<EOF
1307
            SELECT bi.id AS bid
1308
              FROM {block_instances} bi
1309
             WHERE {$instanceselect}
1310
        EOF;
1311
        $DB->delete_records_subquery('block_instances', 'id', 'bid', $deletesql, $instanceparams);
1312
    };
1313
 
1314
    // Delete the default indexsys version of the block.
1315
    $subpagepattern = $DB->get_record('my_pages', [
1316
        'userid' => null,
1317
        'name' => $pagename,
1318
        'private' => MY_PAGE_PRIVATE,
1319
    ], 'id', IGNORE_MULTIPLE)->id;
1320
 
1321
    $instanceselect = <<<EOF
1322
            blockname = :blockname
1323
        AND pagetypepattern = :pagetypepattern
1324
        AND subpagepattern = :subpagepattern
1325
    EOF;
1326
 
1327
    $params = [
1328
        'blockname' => $blockname,
1329
        'pagetypepattern' => $pagetypepattern,
1330
        'subpagepattern' => $subpagepattern,
1331
    ];
1332
    $deleteblockinstances($instanceselect, $params);
1333
 
1334
    // The subpagepattern is a string.
1335
    // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a
1336
    // community block to do something different.
1337
    // This function is not suited to those cases.
1338
    $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1339
 
1340
    // Look for any and all instances of the block in customised /my pages.
1341
    $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1342
    $instanceselect = <<<EOF
1343
         bi.id IN (
1344
            SELECT * FROM (
1345
                SELECT bi.id
1346
                  FROM {my_pages} mp
1347
                  JOIN {block_instances} bi
1348
                        ON bi.blockname = :blockname
1349
                       AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1350
                       AND bi.pagetypepattern = :pagetypepattern
1351
                       AND {$subpagepattern} = mp.id
1352
                 WHERE mp.private = :private
1353
                   AND mp.name = :pagename
1354
            ) bid
1355
         )
1356
    EOF;
1357
 
1358
    $params = [
1359
        'blockname' => $blockname,
1360
        'pagetypepattern' => $pagetypepattern,
1361
        'pagename' => $pagename,
1362
        'private' => MY_PAGE_PRIVATE,
1363
    ];
1364
 
1365
    $deleteblockinstances($instanceselect, $params);
1366
}
1367
 
1368
/**
1369
 * Update the block instance parentcontext to point to the correct user context id for the specified block on a my page.
1370
 *
1371
 * @param string $blockname
1372
 * @param string $pagename
1373
 * @param string $pagetypepattern
1374
 */
1375
function upgrade_block_set_my_user_parent_context(
1376
    string $blockname,
1377
    string $pagename,
1378
    string $pagetypepattern
1379
): void {
1380
    global $DB;
1381
 
1382
    $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1383
    // Look for any and all instances of the block in customised /my pages.
1384
    $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1385
 
1386
    $dbman = $DB->get_manager();
1387
    $temptablename = 'block_instance_context';
1388
    $xmldbtable = new \xmldb_table($temptablename);
1389
    $xmldbtable->add_field('instanceid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
1390
    $xmldbtable->add_field('contextid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
1391
    $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, ['instanceid']);
1392
    $dbman->create_temp_table($xmldbtable);
1393
 
1394
    $sql = <<<EOF
1395
        INSERT INTO {block_instance_context} (
1396
            instanceid,
1397
            contextid
1398
        ) SELECT
1399
            bi.id as instanceid,
1400
            c.id as contextid
1401
           FROM {my_pages} mp
1402
           JOIN {context} c ON c.instanceid = mp.userid AND c.contextlevel = :contextuser
1403
           JOIN {block_instances} bi
1404
                ON bi.blockname = :blockname
1405
               AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1406
               AND bi.pagetypepattern = :pagetypepattern
1407
               AND {$subpagepattern} = mp.id
1408
          WHERE mp.name = :pagename AND bi.parentcontextid <> c.id
1409
    EOF;
1410
 
1411
    $DB->execute($sql, [
1412
        'blockname' => $blockname,
1413
        'pagetypepattern' => $pagetypepattern,
1414
        'contextuser' => CONTEXT_USER,
1415
        'pagename' => $pagename,
1416
    ]);
1417
 
1418
    $dbfamily = $DB->get_dbfamily();
1419
    if ($dbfamily === 'mysql') {
1420
        // MariaDB and MySQL.
1421
        $sql = <<<EOF
1422
            UPDATE {block_instances} bi, {block_instance_context} bic
1423
               SET bi.parentcontextid = bic.contextid
1424
             WHERE bi.id = bic.instanceid
1425
        EOF;
1426
    } else {
1427
        // Postgres and sqlsrv.
1428
        $sql = <<<EOF
1429
            UPDATE {block_instances}
1430
            SET parentcontextid = bic.contextid
1431
            FROM {block_instance_context} bic
1432
            WHERE {block_instances}.id = bic.instanceid
1433
        EOF;
1434
    }
1435
 
1436
    $DB->execute($sql);
1437
 
1438
    $dbman->drop_table($xmldbtable);
1439
}
1440
 
1441
/**
1442
 * Fix the timestamps for files where their timestamps are older
1443
 * than the directory listing that they are contained in.
1444
 */
1445
function upgrade_fix_file_timestamps() {
1446
    global $DB;
1447
 
1448
    // Due to incompatability in SQL syntax for updates with joins,
1449
    // These will be updated in a select + separate update.
1450
    $sql = "SELECT f.id, f2.timecreated
1451
              FROM {files} f
1452
              JOIN {files} f2
1453
                    ON f2.contextid = f.contextid
1454
                   AND f2.filepath = f.filepath
1455
                   AND f2.component = f.component
1456
                   AND f2.filearea = f.filearea
1457
                   AND f2.itemid = f.itemid
1458
                   AND f2.filename = '.'
1459
             WHERE f2.timecreated > f.timecreated";
1460
 
1461
    $recordset = $DB->get_recordset_sql($sql);
1462
 
1463
    if (!$recordset->valid()) {
1464
        $recordset->close();
1465
        return;
1466
    }
1467
 
1468
    foreach ($recordset as $record) {
1469
        $record->timemodified = $record->timecreated;
1470
        $DB->update_record('files', $record);
1471
    }
1472
 
1473
    $recordset->close();
1474
}
1475
 
1476
/**
1477
 * Upgrade helper to add foreign keys and indexes for MDL-49795
1478
 */
1479
function upgrade_add_foreign_key_and_indexes() {
1480
    global $DB;
1481
 
1482
    $dbman = $DB->get_manager();
1483
    // Define key originalcourseid (foreign) to be added to course.
1484
    $table = new xmldb_table('course');
1485
    $key = new xmldb_key('originalcourseid', XMLDB_KEY_FOREIGN, ['originalcourseid'], 'course', ['id']);
1486
    // Launch add key originalcourseid.
1487
    $dbman->add_key($table, $key);
1488
 
1489
    // Define key roleid (foreign) to be added to enrol.
1490
    $table = new xmldb_table('enrol');
1491
    $key = new xmldb_key('roleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
1492
    // Launch add key roleid.
1493
    $dbman->add_key($table, $key);
1494
 
1495
    // Define key userid (foreign) to be added to scale.
1496
    $table = new xmldb_table('scale');
1497
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1498
    // Launch add key userid.
1499
    $dbman->add_key($table, $key);
1500
 
1501
    // Define key userid (foreign) to be added to scale_history.
1502
    $table = new xmldb_table('scale_history');
1503
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1504
    // Launch add key userid.
1505
    $dbman->add_key($table, $key);
1506
 
1507
    // Define key courseid (foreign) to be added to post.
1508
    $table = new xmldb_table('post');
1509
    $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1510
    // Launch add key courseid.
1511
    $dbman->add_key($table, $key);
1512
 
1513
    // Define key coursemoduleid (foreign) to be added to post.
1514
    $table = new xmldb_table('post');
1515
    $key = new xmldb_key('coursemoduleid', XMLDB_KEY_FOREIGN, ['coursemoduleid'], 'course_modules', ['id']);
1516
    // Launch add key coursemoduleid.
1517
    $dbman->add_key($table, $key);
1518
 
1519
    // Define key questionid (foreign) to be added to question_statistics.
1520
    $table = new xmldb_table('question_statistics');
1521
    $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
1522
    // Launch add key questionid.
1523
    $dbman->add_key($table, $key);
1524
 
1525
    // Define key questionid (foreign) to be added to question_response_analysis.
1526
    $table = new xmldb_table('question_response_analysis');
1527
    $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
1528
    // Launch add key questionid.
1529
    $dbman->add_key($table, $key);
1530
 
1531
    // Define index last_log_id (not unique) to be added to mnet_host.
1532
    $table = new xmldb_table('mnet_host');
1533
    $index = new xmldb_index('last_log_id', XMLDB_INDEX_NOTUNIQUE, ['last_log_id']);
1534
    // Conditionally launch add index last_log_id.
1535
    if (!$dbman->index_exists($table, $index)) {
1536
        $dbman->add_index($table, $index);
1537
    }
1538
 
1539
    // Define key userid (foreign) to be added to mnet_session.
1540
    $table = new xmldb_table('mnet_session');
1541
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1542
    // Launch add key userid.
1543
    $dbman->add_key($table, $key);
1544
 
1545
    // Define key mnethostid (foreign) to be added to mnet_session.
1546
    $table = new xmldb_table('mnet_session');
1547
    $key = new xmldb_key('mnethostid', XMLDB_KEY_FOREIGN, ['mnethostid'], 'mnet_host', ['id']);
1548
    // Launch add key mnethostid.
1549
    $dbman->add_key($table, $key);
1550
 
1551
    // Define key userid (foreign) to be added to grade_import_values.
1552
    $table = new xmldb_table('grade_import_values');
1553
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1554
    // Launch add key userid.
1555
    $dbman->add_key($table, $key);
1556
 
1557
    // Define key tempdataid (foreign) to be added to portfolio_log.
1558
    $table = new xmldb_table('portfolio_log');
1559
    $key = new xmldb_key('tempdataid', XMLDB_KEY_FOREIGN, ['tempdataid'], 'portfolio_tempdata', ['id']);
1560
    // Launch add key tempdataid.
1561
    $dbman->add_key($table, $key);
1562
 
1563
    // Define key usermodified (foreign) to be added to file_conversion.
1564
    $table = new xmldb_table('file_conversion');
1565
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1566
    // Launch add key usermodified.
1567
    $dbman->add_key($table, $key);
1568
 
1569
    // Define key userid (foreign) to be added to repository_instances.
1570
    $table = new xmldb_table('repository_instances');
1571
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1572
    // Launch add key userid.
1573
    $dbman->add_key($table, $key);
1574
 
1575
    // Define key contextid (foreign) to be added to repository_instances.
1576
    $table = new xmldb_table('repository_instances');
1577
    $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1578
    // Launch add key contextid.
1579
    $dbman->add_key($table, $key);
1580
 
1581
    // Define key scaleid (foreign) to be added to rating.
1582
    $table = new xmldb_table('rating');
1583
    $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1584
    // Launch add key scaleid.
1585
    $dbman->add_key($table, $key);
1586
 
1587
    // Define key courseid (foreign) to be added to course_published.
1588
    $table = new xmldb_table('course_published');
1589
    $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1590
    // Launch add key courseid.
1591
    $dbman->add_key($table, $key);
1592
 
1593
    // Define index hubcourseid (not unique) to be added to course_published.
1594
    $table = new xmldb_table('course_published');
1595
    $index = new xmldb_index('hubcourseid', XMLDB_INDEX_NOTUNIQUE, ['hubcourseid']);
1596
    // Conditionally launch add index hubcourseid.
1597
    if (!$dbman->index_exists($table, $index)) {
1598
        $dbman->add_index($table, $index);
1599
    }
1600
 
1601
    // Define key courseid (foreign) to be added to event_subscriptions.
1602
    $table = new xmldb_table('event_subscriptions');
1603
    $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1604
    // Launch add key courseid.
1605
    $dbman->add_key($table, $key);
1606
 
1607
    // Define key userid (foreign) to be added to event_subscriptions.
1608
    $table = new xmldb_table('event_subscriptions');
1609
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1610
    // Launch add key userid.
1611
    $dbman->add_key($table, $key);
1612
 
1613
    // Define key userid (foreign) to be added to task_log.
1614
    $table = new xmldb_table('task_log');
1615
    $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1616
    // Launch add key userid.
1617
    $dbman->add_key($table, $key);
1618
 
1619
    // Define key scaleid (foreign) to be added to competency.
1620
    $table = new xmldb_table('competency');
1621
    $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1622
    // Launch add key scaleid.
1623
    $dbman->add_key($table, $key);
1624
 
1625
    // Define key usermodified (foreign) to be added to competency.
1626
    $table = new xmldb_table('competency');
1627
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1628
    // Launch add key usermodified.
1629
    $dbman->add_key($table, $key);
1630
 
1631
    // Define key usermodified (foreign) to be added to competency_coursecompsetting.
1632
    $table = new xmldb_table('competency_coursecompsetting');
1633
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1634
    // Launch add key usermodified.
1635
    $dbman->add_key($table, $key);
1636
 
1637
    // Define key contextid (foreign) to be added to competency_framework.
1638
    $table = new xmldb_table('competency_framework');
1639
    $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1640
    // Launch add key contextid.
1641
    $dbman->add_key($table, $key);
1642
 
1643
    // Define key scaleid (foreign) to be added to competency_framework.
1644
    $table = new xmldb_table('competency_framework');
1645
    $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1646
    // Launch add key scaleid.
1647
    $dbman->add_key($table, $key);
1648
 
1649
    // Define key usermodified (foreign) to be added to competency_framework.
1650
    $table = new xmldb_table('competency_framework');
1651
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1652
    // Launch add key usermodified.
1653
    $dbman->add_key($table, $key);
1654
 
1655
    // Define key usermodified (foreign) to be added to competency_coursecomp.
1656
    $table = new xmldb_table('competency_coursecomp');
1657
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1658
    // Launch add key usermodified.
1659
    $dbman->add_key($table, $key);
1660
 
1661
    // Define key actionuserid (foreign) to be added to competency_evidence.
1662
    $table = new xmldb_table('competency_evidence');
1663
    $key = new xmldb_key('actionuserid', XMLDB_KEY_FOREIGN, ['actionuserid'], 'user', ['id']);
1664
    // Launch add key actionuserid.
1665
    $dbman->add_key($table, $key);
1666
 
1667
    // Define key contextid (foreign) to be added to competency_evidence.
1668
    $table = new xmldb_table('competency_evidence');
1669
    $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1670
    // Launch add key contextid.
1671
    $dbman->add_key($table, $key);
1672
 
1673
    // Define key usermodified (foreign) to be added to competency_evidence.
1674
    $table = new xmldb_table('competency_evidence');
1675
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1676
    // Launch add key usermodified.
1677
    $dbman->add_key($table, $key);
1678
 
1679
    // Define key usermodified (foreign) to be added to competency_userevidence.
1680
    $table = new xmldb_table('competency_userevidence');
1681
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1682
    // Launch add key usermodified.
1683
    $dbman->add_key($table, $key);
1684
 
1685
    // Define key usermodified (foreign) to be added to competency_plan.
1686
    $table = new xmldb_table('competency_plan');
1687
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1688
    // Launch add key usermodified.
1689
    $dbman->add_key($table, $key);
1690
 
1691
    // Define key usermodified (foreign) to be added to competency_template.
1692
    $table = new xmldb_table('competency_template');
1693
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1694
    // Launch add key usermodified.
1695
    $dbman->add_key($table, $key);
1696
 
1697
    // Define key contextid (foreign) to be added to competency_template.
1698
    $table = new xmldb_table('competency_template');
1699
    $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1700
    // Launch add key contextid.
1701
    $dbman->add_key($table, $key);
1702
 
1703
    // Define key usermodified (foreign) to be added to competency_templatecomp.
1704
    $table = new xmldb_table('competency_templatecomp');
1705
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1706
    // Launch add key usermodified.
1707
    $dbman->add_key($table, $key);
1708
 
1709
    // Define key usermodified (foreign) to be added to competency_templatecohort.
1710
    $table = new xmldb_table('competency_templatecohort');
1711
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1712
    // Launch add key usermodified.
1713
    $dbman->add_key($table, $key);
1714
 
1715
    // Define key competencyid (foreign) to be added to competency_relatedcomp.
1716
    $table = new xmldb_table('competency_relatedcomp');
1717
    $key = new xmldb_key('competencyid', XMLDB_KEY_FOREIGN, ['competencyid'], 'competency', ['id']);
1718
    // Launch add key competencyid.
1719
    $dbman->add_key($table, $key);
1720
 
1721
    // Define key relatedcompetencyid (foreign) to be added to competency_relatedcomp.
1722
    $table = new xmldb_table('competency_relatedcomp');
1723
    $key = new xmldb_key('relatedcompetencyid', XMLDB_KEY_FOREIGN, ['relatedcompetencyid'], 'competency', ['id']);
1724
    // Launch add key relatedcompetencyid.
1725
    $dbman->add_key($table, $key);
1726
 
1727
    // Define key usermodified (foreign) to be added to competency_relatedcomp.
1728
    $table = new xmldb_table('competency_relatedcomp');
1729
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1730
    // Launch add key usermodified.
1731
    $dbman->add_key($table, $key);
1732
 
1733
    // Define key usermodified (foreign) to be added to competency_usercomp.
1734
    $table = new xmldb_table('competency_usercomp');
1735
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1736
    // Launch add key usermodified.
1737
    $dbman->add_key($table, $key);
1738
 
1739
    // Define key usermodified (foreign) to be added to competency_usercompcourse.
1740
    $table = new xmldb_table('competency_usercompcourse');
1741
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1742
    // Launch add key usermodified.
1743
    $dbman->add_key($table, $key);
1744
 
1745
    // Define key usermodified (foreign) to be added to competency_usercompplan.
1746
    $table = new xmldb_table('competency_usercompplan');
1747
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1748
    // Launch add key usermodified.
1749
    $dbman->add_key($table, $key);
1750
 
1751
    // Define key usermodified (foreign) to be added to competency_plancomp.
1752
    $table = new xmldb_table('competency_plancomp');
1753
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1754
    // Launch add key usermodified.
1755
    $dbman->add_key($table, $key);
1756
 
1757
    // Define key usermodified (foreign) to be added to competency_userevidencecomp.
1758
    $table = new xmldb_table('competency_userevidencecomp');
1759
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1760
    // Launch add key usermodified.
1761
    $dbman->add_key($table, $key);
1762
 
1763
    // Define key usermodified (foreign) to be added to competency_modulecomp.
1764
    $table = new xmldb_table('competency_modulecomp');
1765
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1766
    // Launch add key usermodified.
1767
    $dbman->add_key($table, $key);
1768
 
1769
    // Define key usermodified (foreign) to be added to oauth2_endpoint.
1770
    $table = new xmldb_table('oauth2_endpoint');
1771
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1772
    // Launch add key usermodified.
1773
    $dbman->add_key($table, $key);
1774
 
1775
    // Define key usermodified (foreign) to be added to oauth2_system_account.
1776
    $table = new xmldb_table('oauth2_system_account');
1777
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1778
    // Launch add key usermodified.
1779
    $dbman->add_key($table, $key);
1780
 
1781
    // Define key usermodified (foreign) to be added to oauth2_user_field_mapping.
1782
    $table = new xmldb_table('oauth2_user_field_mapping');
1783
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1784
    // Launch add key usermodified.
1785
    $dbman->add_key($table, $key);
1786
 
1787
    // Define key usermodified (foreign) to be added to analytics_models.
1788
    $table = new xmldb_table('analytics_models');
1789
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1790
    // Launch add key usermodified.
1791
    $dbman->add_key($table, $key);
1792
 
1793
    // Define key usermodified (foreign) to be added to analytics_models_log.
1794
    $table = new xmldb_table('analytics_models_log');
1795
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1796
    // Launch add key usermodified.
1797
    $dbman->add_key($table, $key);
1798
 
1799
    // Define key usermodified (foreign) to be added to oauth2_access_token.
1800
    $table = new xmldb_table('oauth2_access_token');
1801
    $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1802
    // Launch add key usermodified.
1803
    $dbman->add_key($table, $key);
1804
 
1805
    // Define key contextid (foreign) to be added to payment_accounts.
1806
    $table = new xmldb_table('payment_accounts');
1807
    $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1808
    // Launch add key contextid.
1809
    $dbman->add_key($table, $key);
1810
}
1441 ariadna 1811
 
1812
/**
1813
 * Upgrade helper to change a binary column to an integer column with a length of 1 in a consistent manner across databases.
1814
 *
1815
 * This function will
1816
 * - rename the existing column to a temporary name,
1817
 * - add a new column with the integer type,
1818
 * - copy the values from the old column to the new column,
1819
 * - and finally, drop the old column.
1820
 *
1821
 * This function will do nothing if the field is already an integer.
1822
 *
1823
 * The new column with the integer type will need to have a default value of 0.
1824
 * This is to avoid breaking the not null constraint, if it's set, especially if there are existing records.
1825
 * Please make sure that the column definition in install.xml also has the `DEFAULT` attribute value set to 0.
1826
 *
1827
 * @param string $tablename The name of the table.
1828
 * @param string $fieldname The name of the field to be converted.
1829
 * @param bool|null $notnull {@see XMLDB_NOTNULL} or null.
1830
 * @param string|null $previous The name of the field that this field should come after.
1831
 * @return bool
1832
 */
1833
function upgrade_change_binary_column_to_int(
1834
    string $tablename,
1835
    string $fieldname,
1836
    ?bool $notnull = null,
1837
    ?string $previous = null,
1838
): bool {
1839
    global $DB;
1840
 
1841
    // Get the information about the field to be converted.
1842
    $columns = $DB->get_columns($tablename);
1843
    $toconvert = $columns[$fieldname];
1844
 
1845
    // Check if the field to be converted is already an integer-type column (`meta_type` property of 'I').
1846
    if ($toconvert->meta_type === 'I') {
1847
        // Nothing to do if the field is already an integer-type.
1848
        return false;
1849
    } else if (!$toconvert->binary) {
1850
        throw new \core\exception\coding_exception(
1851
            'This function is only used to convert XMLDB_TYPE_BINARY fields to XMLDB_TYPE_INTEGER fields. '
1852
            . 'For other field types, please check out \database_manager::change_field_type()'
1853
        );
1854
    }
1855
 
1856
    $dbman = $DB->get_manager();
1857
    $table = new xmldb_table($tablename);
1858
    // Temporary rename the field. We'll drop this later.
1859
    $tmpfieldname = "tmp$fieldname";
1860
    $field = new xmldb_field($fieldname, XMLDB_TYPE_BINARY);
1861
    $dbman->rename_field($table, $field, $tmpfieldname);
1862
 
1863
    // Add the new field wih the integer type.
1864
    $field = new xmldb_field($fieldname, XMLDB_TYPE_INTEGER, '1', null, $notnull, null, '0', $previous);
1865
    $dbman->add_field($table, $field);
1866
 
1867
    // Copy the 'true' values from the old field to the new field.
1868
    $sql = 'UPDATE {' . $tablename . '}
1869
               SET ' . $fieldname . ' = 1
1870
             WHERE ' . $tmpfieldname . ' = ?';
1871
    $DB->execute($sql, [1]);
1872
 
1873
    // Drop the old field.
1874
    $oldfield = new xmldb_field($tmpfieldname);
1875
    $dbman->drop_field($table, $oldfield);
1876
 
1877
    return true;
1878
}
1879
 
1880
/**
1881
 * Upgrade script replacing absolute URLs in defaulthomepage setting with relative URLs
1882
 */
1883
function upgrade_store_relative_url_sitehomepage() {
1884
    global $CFG, $DB;
1885
 
1886
    if (str_starts_with((string)$CFG->defaulthomepage, $CFG->wwwroot . '/')) {
1887
        set_config('defaulthomepage', substr((string)$CFG->defaulthomepage, strlen($CFG->wwwroot)));
1888
    }
1889
 
1890
    $records = $DB->get_records_select('user_preferences', "name = :name AND " . $DB->sql_like('value', ':pattern'),
1891
        ['name' => 'user_home_page_preference', 'pattern' => 'http%']);
1892
    foreach ($records as $record) {
1893
        if (str_starts_with($record->value, $CFG->wwwroot . '/')) {
1894
            $DB->update_record('user_preferences', [
1895
                'id' => $record->id,
1896
                'value' => substr($record->value, strlen($CFG->wwwroot)),
1897
            ]);
1898
        }
1899
    }
1900
}
1901
 
1902
/**
1903
 * Upgrade script to convert existing AI providers to provider instances.
1904
 */
1905
function upgrade_convert_ai_providers_to_instances() {
1906
    global $DB;
1907
    // Start with the azureai provider.
1908
    // Only migrate the provider if it is enabled.
1909
    $azureaiconfig = get_config('aiprovider_azureai');
1910
    if (!empty($azureaiconfig->enabled) || !empty($azureaiconfig->apikey)) {
1911
        // Create the instance config. We don't want everything from the provider config.
1912
        $instanceconfig = [
1913
            'aiprovider' => \aiprovider_azureai\provider::class,
1914
            'name' => get_string('pluginname', 'aiprovider_azureai'),
1915
            'apikey' => $azureaiconfig->apikey ?? '',
1916
            'endpoint' => $azureaiconfig->endpoint ?? '',
1917
            'enableglobalratelimit' => $azureaiconfig->enableglobalratelimit ?? 0,
1918
            'globalratelimit' => $azureaiconfig->globalratelimit ?? 100,
1919
            'enableuserratelimit' => $azureaiconfig->enableuserratelimit ?? 0,
1920
            'userratelimit' => $azureaiconfig->userratelimit ?? 10,
1921
        ];
1922
        $actionconfig = [
1923
            'core_ai\aiactions\generate_text' => [
1924
                'enabled' => $azureaiconfig->generate_text ?? true,
1925
                'settings' => [
1926
                    'deployment' => $azureaiconfig->action_generate_text_deployment ?? '',
1927
                    'apiversion' => $azureaiconfig->action_generate_text_apiversion ?? '2024-06-01',
1928
                    'systeminstruction' => $azureaiconfig->action_generate_text_systeminstruction
1929
                        ?? get_string('action_generate_text_instruction', 'core_ai'),
1930
                ],
1931
            ],
1932
            'core_ai\aiactions\generate_image' => [
1933
                'enabled' => $azureaiconfig->generate_image ?? true,
1934
                'settings' => [
1935
                    'deployment' => $azureaiconfig->action_generate_image_deployment ?? '',
1936
                    'apiversion' => $azureaiconfig->action_generate_image_apiversion ?? '2024-06-01',
1937
                ],
1938
            ],
1939
            'core_ai\aiactions\summarise_text' => [
1940
                'enabled' => $azureaiconfig->summarise_text ?? true,
1941
                'settings' => [
1942
                    'deployment' => $azureaiconfig->action_summarise_text_deployment ?? '',
1943
                    'apiversion' => $azureaiconfig->action_summarise_text_apiversion ?? '2024-06-01',
1944
                    'systeminstruction' => $azureaiconfig->action_generate_text_systeminstruction
1945
                        ?? get_string('action_summarise_text_instruction', 'core_ai'),
1946
                ],
1947
            ],
1948
        ];
1949
 
1950
        // Because of the upgrade code restrictions we insert directly into the database and don't use the AI manager class.
1951
        $record = new stdClass();
1952
        $record->name = get_string('pluginname', 'aiprovider_azureai');
1953
        $record->provider = \aiprovider_azureai\provider::class;
1954
        $record->enabled = $azureaiconfig->enabled ?? false;
1955
        $record->config = json_encode($instanceconfig);
1956
        $record->actionconfig = json_encode($actionconfig);
1957
 
1958
        $DB->insert_record('ai_providers', $record);
1959
    }
1960
 
1961
    // Now do the same for the openai provider.
1962
    $openaiconfig = get_config('aiprovider_openai');
1963
    if (!empty($openaiconfig->enabled) || !empty($openaiconfig->apikey)) {
1964
        // Create the instance config. We don't want everything from the provider config.
1965
        $instanceconfig = [
1966
            'aiprovider' => \aiprovider_openai\provider::class,
1967
            'name' => get_string('pluginname', 'aiprovider_openai'),
1968
            'apikey' => $openaiconfig->apikey ?? '',
1969
            'orgid' => $openaiconfig->orgid ?? '',
1970
            'enableglobalratelimit' => $openaiconfig->enableglobalratelimit ?? 0,
1971
            'globalratelimit' => $openaiconfig->globalratelimit ?? 100,
1972
            'enableuserratelimit' => $openaiconfig->enableuserratelimit ?? 0,
1973
            'userratelimit' => $openaiconfig->userratelimit ?? 10,
1974
        ];
1975
        $actionconfig = [
1976
            'core_ai\aiactions\generate_text' => [
1977
                'enabled' => $openaiconfig->generate_text ?? true,
1978
                'settings' => [
1979
                    'model' => $openaiconfig->action_generate_text_model ?? 'gpt-4o',
1980
                    'endpoint' => $openaiconfig->action_generate_text_endpoint ?? 'https://api.openai.com/v1/chat/completions',
1981
                    'systeminstruction' => $openaiconfig->action_generate_text_systeminstruction
1982
                        ?? get_string('action_generate_text_instruction', 'core_ai'),
1983
                ],
1984
            ],
1985
            'core_ai\aiactions\generate_image' => [
1986
                'enabled' => $openaiconfig->generate_image ?? true,
1987
                'settings' => [
1988
                    'model' => $openaiconfig->action_generate_text_model ?? 'dall-e-3',
1989
                    'endpoint' => $openaiconfig->action_generate_text_endpoint ?? 'https://api.openai.com/v1/images/generations',
1990
                ],
1991
            ],
1992
            'core_ai\aiactions\summarise_text' => [
1993
                'enabled' => $openaiconfig->summarise_text ?? true,
1994
                'settings' => [
1995
                    'model' => $openaiconfig->action_generate_text_model ?? 'gpt-4o',
1996
                    'endpoint' => $openaiconfig->action_generate_text_endpoint ?? 'https://api.openai.com/v1/chat/completions',
1997
                    'systeminstruction' => $openaiconfig->action_generate_text_systeminstruction
1998
                        ?? get_string('action_summarise_text_instruction', 'core_ai'),
1999
                ],
2000
            ],
2001
        ];
2002
 
2003
        $record = new stdClass();
2004
        $record->name = get_string('pluginname', 'aiprovider_openai');
2005
        $record->provider = \aiprovider_openai\provider::class;
2006
        $record->enabled = $openaiconfig->enabled ?? false;
2007
        $record->config = json_encode($instanceconfig);
2008
        $record->actionconfig = json_encode($actionconfig);
2009
 
2010
        $DB->insert_record('ai_providers', $record);
2011
    }
2012
 
2013
    // Finally remove the config settings from the plugin config table.
2014
    $azuresettings = ['enabled', 'apikey', 'endpoint', 'enableglobalratelimit', 'globalratelimit',
2015
        'enableuserratelimit', 'userratelimit', 'generate_text', 'action_generate_text_enabled', 'action_generate_text_deployment',
2016
        'action_generate_text_apiversion', 'action_generate_text_systeminstruction', 'generate_image',
2017
        'action_generate_image_enabled', 'action_generate_image_deployment', 'action_generate_image_apiversion',
2018
        'summarise_text', 'action_summarise_text_enabled', 'action_summarise_text_deployment', 'action_summarise_text_apiversion',
2019
        'action_summarise_text_systeminstruction'];
2020
    array_walk($azuresettings, static fn($setting) => unset_config($setting, 'aiprovider_azureai'));
2021
    $openaisettings = ['enabled', 'apikey', 'orgid', 'enableglobalratelimit', 'globalratelimit',
2022
        'enableuserratelimit', 'userratelimit', 'generate_text', 'action_generate_text_enabled', 'action_generate_text_model',
2023
        'action_generate_text_endpoint', 'action_generate_text_systeminstruction', 'generate_image',
2024
        'action_generate_image_enabled', 'action_generate_image_model', 'action_generate_image_endpoint',
2025
        'summarise_text', 'action_summarise_text_enabled', 'action_summarise_text_model', 'action_summarise_text_endpoint',
2026
        'action_summarise_text_systeminstruction'];
2027
    array_walk($openaisettings, static fn($setting) => unset_config($setting, 'aiprovider_openai'));
2028
}
2029
 
2030
/**
2031
 * Upgrade script to get all current AI providers and update their action config to include explain.
2032
 */
2033
function upgrade_add_explain_action_to_ai_providers() {
2034
    global $DB;
2035
    $currentrecords = $DB->get_recordset('ai_providers');
2036
 
2037
    foreach ($currentrecords as $currentrecord) {
2038
        $actionconfig = json_decode($currentrecord->actionconfig, true);
2039
        if ($currentrecord->provider === 'aiprovider_openai\provider') {
2040
            $explainconfig = [
2041
                'enabled' => true,
2042
                'settings' => [
2043
                    'model' => 'gpt-4o',
2044
                    'endpoint' => 'https://api.openai.com/v1/chat/completions',
2045
                    'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),
2046
                ],
2047
            ];
2048
        } else if ($currentrecord->provider === 'aiprovider_azureai\provider') {
2049
            $explainconfig = [
2050
                'enabled' => true,
2051
                'settings' => [
2052
                    'deployment' => '',
2053
                    'apiversion' => '2024-06-01',
2054
                    'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),
2055
                ],
2056
            ];
2057
        }
2058
 
2059
        // Update the record with the changes.
2060
        if (!empty($explainconfig)) {
2061
            $actionconfig['core_ai\aiactions\explain_text'] = $explainconfig;
2062
            $currentrecord->actionconfig = json_encode($actionconfig);
2063
            $DB->update_record('ai_providers', $currentrecord);
2064
        }
2065
    }
2066
 
2067
    $currentrecords->close();
2068
}
2069
 
2070
/**
2071
 * Creates a new ad-hoc task to upgrade the mime-type of files asynchronously.
2072
 * Thus, we can considerably reduce the time an upgrade takes.
2073
 *
2074
 * @param string $mimetype the desired mime-type
2075
 * @param string[] $extensions a list of file extensions, without the leading dot
2076
 * @return void
2077
 */
2078
function upgrade_create_async_mimetype_upgrade_task(string $mimetype, array $extensions): void {
2079
    global $DB;
2080
 
2081
    // Create adhoc task for upgrading of existing files. Due to a code restriction on the upgrade, invoking any core
2082
    // functions is not permitted. Thus we craft our own ad-hoc task that will process all existing files.
2083
    $record = new \stdClass();
2084
    $record->classname = '\core_files\task\asynchronous_mimetype_upgrade_task';
2085
    $record->component = 'core';
2086
    $record->customdata = json_encode([
2087
        'mimetype' => $mimetype,
2088
        'extensions' => $extensions,
2089
    ]);
2090
 
2091
    // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
2092
    $clock = \core\di::get(\core\clock::class);
2093
    $nextruntime = $clock->time() - 1;
2094
    $record->nextruntime = $nextruntime;
2095
 
2096
    $DB->insert_record('task_adhoc', $record);
2097
}