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