Proyectos de Subversion Moodle

Rev

| 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
defined('MOODLE_INTERNAL') || die();
18
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
19
 
20
/**
21
 * Copy helper class.
22
 *
23
 * @package    core_backup
24
 * @copyright  2022 Catalyst IT Australia Pty Ltd
25
 * @author     Cameron Ball <cameron@cameron1729.xyz>
26
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
final class copy_helper {
29
 
30
    /**
31
     * Process raw form data from copy_form.
32
     *
33
     * @param \stdClass $formdata Raw formdata
34
     * @return \stdClass Processed data for use with create_copy
35
     */
36
    public static function process_formdata(\stdClass $formdata): \stdClass {
37
        $requiredfields = [
38
            'courseid',  // Course id integer.
39
            'fullname', // Fullname of the destination course.
40
            'shortname', // Shortname of the destination course.
41
            'category', // Category integer ID that contains the destination course.
42
            'visible', // Integer to detrmine of the copied course will be visible.
43
            'startdate', // Integer timestamp of the start of the destination course.
44
            'enddate', // Integer timestamp of the end of the destination course.
45
            'idnumber', // ID of the destination course.
46
            'userdata', // Integer to determine if the copied course will contain user data.
47
        ];
48
 
49
        $missingfields = array_diff($requiredfields, array_keys((array)$formdata));
50
        if ($missingfields) {
51
            throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, implode(", ", $missingfields));
52
        }
53
 
54
        // Remove any extra stuff in the form data.
55
        $processed = (object)array_intersect_key((array)$formdata, array_flip($requiredfields));
56
        $processed->keptroles = [];
57
 
58
        // Extract roles from the form data and add to keptroles.
59
        foreach ($formdata as $key => $value) {
60
            if ((substr($key, 0, 5) === 'role_') && ($value != 0)) {
61
                $processed->keptroles[] = $value;
62
            }
63
        }
64
 
65
        return $processed;
66
    }
67
 
68
    /**
69
     * Creates a course copy.
70
     * Sets up relevant controllers and adhoc task.
71
     *
72
     * @param \stdClass $copydata Course copy data from process_formdata
73
     * @return array $copyids The backup and restore controller ids
74
     */
75
    public static function create_copy(\stdClass $copydata): array {
76
        global $USER;
77
        $copyids = [];
78
 
79
        // Create the initial backupcontoller.
80
        $bc = new \backup_controller(\backup::TYPE_1COURSE, $copydata->courseid, \backup::FORMAT_MOODLE,
81
            \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES);
82
        $copyids['backupid'] = $bc->get_backupid();
83
 
84
        // Create the initial restore contoller.
85
        list($fullname, $shortname) = \restore_dbops::calculate_course_names(
86
            0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup'));
87
        $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $copydata->category);
88
        $rc = new \restore_controller($copyids['backupid'], $newcourseid, \backup::INTERACTIVE_NO,
89
            \backup::MODE_COPY, $USER->id, \backup::TARGET_NEW_COURSE, null,
90
            \backup::RELEASESESSION_NO, $copydata);
91
        $copyids['restoreid'] = $rc->get_restoreid();
92
 
93
        $bc->set_status(\backup::STATUS_AWAITING);
94
        $bc->get_status();
95
        $rc->save_controller();
96
 
97
        // Create the ad-hoc task to perform the course copy.
98
        $asynctask = new \core\task\asynchronous_copy_task();
99
        $asynctask->set_custom_data($copyids);
100
        \core\task\manager::queue_adhoc_task($asynctask);
101
 
102
        // Clean up the controller.
103
        $bc->destroy();
104
 
105
        return $copyids;
106
    }
107
 
108
    /**
109
     * Get the in progress course copy operations for a user.
110
     *
111
     * @param int $userid User id to get the course copies for.
112
     * @param int|null $courseid The optional source course id to get copies for.
113
     * @return array $copies Details of the inprogress copies.
114
     */
115
    public static function get_copies(int $userid, ?int $courseid = null): array {
116
        global $DB;
117
        $copies = [];
118
        [$insql, $inparams] = $DB->get_in_or_equal([\backup::STATUS_FINISHED_OK, \backup::STATUS_FINISHED_ERR]);
119
        $params = [
120
            $userid,
121
            \backup::EXECUTION_DELAYED,
122
            \backup::MODE_COPY,
123
            \backup::OPERATION_BACKUP,
124
            \backup::STATUS_FINISHED_OK,
125
            \backup::OPERATION_RESTORE
126
        ];
127
 
128
        // We exclude backups that finished with OK. Therefore if a backup is missing,
129
        // we can assume it finished properly.
130
        //
131
        // We exclude both failed and successful restores because both of those indicate that the whole
132
        // operation has completed.
133
        $sql = 'SELECT backupid, itemid, operation, status, timecreated, purpose
134
                  FROM {backup_controllers}
135
                 WHERE userid = ?
136
                       AND execution = ?
137
                       AND purpose = ?
138
                       AND ((operation = ? AND status <> ?) OR (operation = ? AND status NOT ' . $insql .'))
139
              ORDER BY timecreated DESC';
140
 
141
        $copyrecords = $DB->get_records_sql($sql, array_merge($params, $inparams));
142
        $idtorc = self::map_backupids_to_restore_controller($copyrecords);
143
 
144
        // Our SQL only gets controllers that have not finished successfully.
145
        // So, no restores => all restores have finished (either failed or OK) => all backups have too
146
        // Therefore there are no in progress copy operations, return early.
147
        if (empty($idtorc)) {
148
            return [];
149
        }
150
 
151
        foreach ($copyrecords as $copyrecord) {
152
            try {
153
                $isbackup = $copyrecord->operation == \backup::OPERATION_BACKUP;
154
 
155
                // The mapping is guaranteed to exist for restore controllers, but not
156
                // backup controllers.
157
                //
158
                // When processing backups we don't actually need it, so we just coalesce
159
                // to null.
160
                $rc = $idtorc[$copyrecord->backupid] ?? null;
161
 
162
                $cid = $isbackup ? $copyrecord->itemid : $rc->get_copy()->courseid;
163
                $course = get_course($cid);
164
                $copy = clone ($copyrecord);
165
                $copy->backupid = $isbackup ? $copyrecord->backupid : null;
166
                $copy->restoreid = $rc ? $rc->get_restoreid() : null;
167
                $copy->destination = $rc ? $rc->get_copy()->shortname : null;
168
                $copy->source = $course->shortname;
169
                $copy->sourceid = $course->id;
170
            } catch (\Exception $e) {
171
                continue;
172
            }
173
 
174
            // Filter out anything that's not relevant.
175
            if ($courseid) {
176
                if ($isbackup && $copyrecord->itemid != $courseid) {
177
                    continue;
178
                }
179
 
180
                if (!$isbackup && $rc->get_copy()->courseid != $courseid) {
181
                    continue;
182
                }
183
            }
184
 
185
            // A backup here means that the associated restore controller has not started.
186
            //
187
            // There's a few situations to consider:
188
            //
189
            // 1. The backup is waiting or in progress
190
            // 2. The backup failed somehow
191
            // 3. Something went wrong (e.g., solar flare) and the backup controller saved, but the restore controller didn't
192
            // 4. The restore hasn't been created yet (race condition)
193
            //
194
            // In the case of 1, we add it to the return list. In the case of 2, 3 and 4 we just ignore it and move on.
195
            // The backup cleanup task will take care of updating/deleting invalid controllers.
196
            if ($isbackup) {
197
                if ($copyrecord->status != \backup::STATUS_FINISHED_ERR && !is_null($rc)) {
198
                    $copies[] = $copy;
199
                }
200
 
201
                continue;
202
            }
203
 
204
            // A backup in copyrecords, indicates that the associated backup has not
205
            // successfully finished. We shouldn't do anything with this restore record.
206
            if ($copyrecords[$rc->get_tempdir()] ?? null) {
207
                continue;
208
            }
209
 
210
            // This is a restore record, and the backup has finished. Return it.
211
            $copies[] = $copy;
212
        }
213
 
214
        return $copies;
215
    }
216
 
217
    /**
218
     * Returns a mapping between copy controller IDs and the restore controller.
219
     * For example if there exists a copy with backup ID abc and restore ID 123
220
     * then this mapping will map both keys abc and 123 to the same (instantiated)
221
     * restore controller.
222
     *
223
     * @param array $backuprecords An array of records from {backup_controllers}
224
     * @return array An array of mappings between backup ids and restore controllers
225
     */
226
    private static function map_backupids_to_restore_controller(array $backuprecords): array {
227
        // Needed for PHP 7.3 - array_merge only accepts 0 parameters in PHP >= 7.4.
228
        if (empty($backuprecords)) {
229
            return [];
230
        }
231
 
232
        return array_merge(
233
            ...array_map(
234
                function (\stdClass $backuprecord): array {
235
                    $iscopyrestore = $backuprecord->operation == \backup::OPERATION_RESTORE &&
236
                            $backuprecord->purpose == \backup::MODE_COPY;
237
                    $isfinished = $backuprecord->status == \backup::STATUS_FINISHED_OK;
238
 
239
                    if (!$iscopyrestore || $isfinished) {
240
                        return [];
241
                    }
242
 
243
                    $rc = \restore_controller::load_controller($backuprecord->backupid);
244
                    return [$backuprecord->backupid => $rc, $rc->get_tempdir() => $rc];
245
                },
246
                array_values($backuprecords)
247
            )
248
        );
249
    }
250
 
251
    /**
252
     * Detects and deletes/fails controllers associated with a course copy that are
253
     * in an invalid state.
254
     *
255
     * @param array $backuprecords An array of records from {backup_controllers}
256
     * @param int $age How old a controller needs to be (in seconds) before its considered for cleaning
257
     * @return void
258
     */
259
    public static function cleanup_orphaned_copy_controllers(array $backuprecords, int $age = MINSECS): void {
260
        global $DB;
261
 
262
        $idtorc = self::map_backupids_to_restore_controller($backuprecords);
263
 
264
        // Helpful to test if a backup exists in $backuprecords.
265
        $bidstorecord = array_combine(
266
            array_column($backuprecords, 'backupid'),
267
            $backuprecords
268
        );
269
 
270
        foreach ($backuprecords as $record) {
271
            if ($record->purpose != \backup::MODE_COPY || $record->status == \backup::STATUS_FINISHED_OK) {
272
                continue;
273
            }
274
 
275
            $isbackup = $record->operation == \backup::OPERATION_BACKUP;
276
            $restoreexists = isset($idtorc[$record->backupid]);
277
            $nsecondsago = time() - $age;
278
 
279
            if ($isbackup) {
280
                // Sometimes the backup controller gets created, ""something happens"" (like a solar flare)
281
                // and the restore controller (and hence adhoc task) don't.
282
                //
283
                // If more than one minute has passed and the restore controller doesn't exist, it's likely that
284
                // this backup controller is orphaned, so we should remove it as the adhoc task to process it will
285
                // never be created.
286
                if (!$restoreexists && $record->timecreated <= $nsecondsago) {
287
                    // It would be better to mark the backup as failed by loading the controller
288
                    // and marking it as failed with $bc->set_status(), but we can't: MDL-74711.
289
                    //
290
                    // Deleting it isn't ideal either as maybe we want to inspect the backup
291
                    // for debugging. So manually updating the column seems to be the next best.
292
                    $record->status = \backup::STATUS_FINISHED_ERR;
293
                    $DB->update_record('backup_controllers', $record);
294
                }
295
                continue;
296
            }
297
 
298
            if ($rc = $idtorc[$record->backupid] ?? null) {
299
                $backuprecord = $bidstorecord[$rc->get_tempdir()] ?? null;
300
 
301
                // Check the status of the associated backup. If it's failed, then mark this
302
                // restore as failed too.
303
                if ($backuprecord && $backuprecord->status == \backup::STATUS_FINISHED_ERR) {
304
                    $rc->set_status(\backup::STATUS_FINISHED_ERR);
305
                }
306
            }
307
        }
308
    }
309
}