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 * Helper functions for asynchronous backups and restores.
19 *
20 * @package    core
21 * @copyright  2019 Matt Porritt <mattp@catalyst-au.net>
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27require_once($CFG->dirroot . '/user/lib.php');
28
29/**
30 * Helper functions for asynchronous backups and restores.
31 *
32 * @package     core
33 * @copyright   2019 Matt Porritt <mattp@catalyst-au.net>
34 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36class async_helper  {
37
38    /**
39     * @var string $type The type of async operation.
40     */
41    protected $type = 'backup';
42
43    /**
44     * @var string $backupid The id of the backup or restore.
45     */
46    protected $backupid;
47
48    /**
49     * @var object $user The user who created the backup record.
50     */
51    protected $user;
52
53    /**
54     * @var object $backuprec The backup controller record from the database.
55     */
56    protected $backuprec;
57
58    /**
59     * Class constructor.
60     *
61     * @param string $type The type of async operation.
62     * @param string $id The id of the backup or restore.
63     */
64    public function __construct($type, $id) {
65        $this->type = $type;
66        $this->backupid = $id;
67        $this->backuprec = self::get_backup_record($id);
68        $this->user = $this->get_user();
69    }
70
71    /**
72     * Given a backup id return a the record from the database.
73     * We use this method rather than 'load_controller' as the controller may
74     * not exist if this backup/restore has completed.
75     *
76     * @param int $id The backup id to get.
77     * @return object $backuprec The backup controller record.
78     */
79    static public function get_backup_record($id) {
80        global $DB;
81
82        $backuprec = $DB->get_record('backup_controllers', array('backupid' => $id), '*', MUST_EXIST);
83
84        return $backuprec;
85    }
86
87    /**
88     * Given a user id return a user object.
89     *
90     * @return object $user The limited user record.
91     */
92    private function get_user() {
93        $userid = $this->backuprec->userid;
94        $user = core_user::get_user($userid, '*', MUST_EXIST);
95
96        return $user;
97    }
98
99    /**
100     * Callback for preg_replace_callback.
101     * Replaces message placeholders with real values.
102     *
103     * @param array $matches The match array from from preg_replace_callback.
104     * @return string $match The replaced string.
105     */
106    private function lookup_message_variables($matches) {
107        $options = array(
108                'operation' => $this->type,
109                'backupid' => $this->backupid,
110                'user_username' => $this->user->username,
111                'user_email' => $this->user->email,
112                'user_firstname' => $this->user->firstname,
113                'user_lastname' => $this->user->lastname,
114                'link' => $this->get_resource_link(),
115        );
116
117        $match = $options[$matches[1]] ?? $matches[1];
118
119        return $match;
120    }
121
122    /**
123     * Get the link to the resource that is being backuped or restored.
124     *
125     * @return moodle_url $url The link to the resource.
126     */
127    private function get_resource_link() {
128        // Get activity context only for backups.
129        if ($this->backuprec->type == 'activity' && $this->type == 'backup') {
130            $context = context_module::instance($this->backuprec->itemid);
131        } else { // Course or Section which have the same context getter.
132            $context = context_course::instance($this->backuprec->itemid);
133        }
134
135        // Generate link based on operation type.
136        if ($this->type == 'backup') {
137            // For backups simply generate link to restore file area UI.
138            $url = new moodle_url('/backup/restorefile.php', array('contextid' => $context->id));
139        } else {
140            // For restore generate link to the item itself.
141            $url = $context->get_url();
142        }
143
144        return $url;
145    }
146
147    /**
148     * Sends a confirmation message for an aynchronous process.
149     *
150     * @return int $messageid The id of the sent message.
151     */
152    public function send_message() {
153        global $USER;
154
155        $subjectraw = get_config('backup', 'backup_async_message_subject');
156        $subjecttext = preg_replace_callback(
157                '/\{([-_A-Za-z0-9]+)\}/u',
158                array('async_helper', 'lookup_message_variables'),
159                $subjectraw);
160
161        $messageraw = get_config('backup', 'backup_async_message');
162        $messagehtml = preg_replace_callback(
163                '/\{([-_A-Za-z0-9]+)\}/u',
164                array('async_helper', 'lookup_message_variables'),
165                $messageraw);
166        $messagetext = html_to_text($messagehtml);
167
168        $message = new \core\message\message();
169        $message->component = 'moodle';
170        $message->name = 'asyncbackupnotification';
171        $message->userfrom          = $USER;
172        $message->userto            = $this->user;
173        $message->subject           = $subjecttext;
174        $message->fullmessage       = $messagetext;
175        $message->fullmessageformat = FORMAT_HTML;
176        $message->fullmessagehtml   = $messagehtml;
177        $message->smallmessage      = '';
178        $message->notification      = '1';
179
180        $messageid = message_send($message);
181
182        return $messageid;
183    }
184
185    /**
186     * Check if asynchronous backup and restore mode is
187     * enabled at system level.
188     *
189     * @return bool $async True if async mode enabled false otherwise.
190     */
191    static public function is_async_enabled() {
192        global $CFG;
193
194        $async = false;
195        if (!empty($CFG->enableasyncbackup)) {
196            $async = true;
197        }
198
199        return $async;
200    }
201
202    /**
203     * Check if there is a pending async operation for given details.
204     *
205     * @param int $id The item id to check in the backup record.
206     * @param string $type The type of operation: course, activity or section.
207     * @param string $operation Operation backup or restore.
208     * @return boolean $asyncpedning Is there a pending async operation.
209     */
210    public static function is_async_pending($id, $type, $operation) {
211        global $DB, $USER, $CFG;
212        $asyncpending = false;
213
214        // Only check for pending async operations if async mode is enabled.
215        require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php');
216        require_once($CFG->dirroot . '/backup/backup.class.php');
217
218        $select = 'userid = ? AND itemid = ? AND type = ? AND operation = ? AND execution = ? AND status < ? AND status > ?';
219        $params = array(
220            $USER->id,
221            $id,
222            $type,
223            $operation,
224            backup::EXECUTION_DELAYED,
225            backup::STATUS_FINISHED_ERR,
226            backup::STATUS_NEED_PRECHECK
227        );
228
229        $asyncrecord= $DB->get_record_select('backup_controllers', $select, $params);
230
231        if ((self::is_async_enabled() && $asyncrecord) || ($asyncrecord && $asyncrecord->purpose == backup::MODE_COPY)) {
232            $asyncpending = true;
233        }
234        return $asyncpending;
235    }
236
237    /**
238     * Get the size, url and restore url for a backup file.
239     *
240     * @param string $filename The name of the file to get info for.
241     * @param string $filearea The file area for the file.
242     * @param int $contextid The context ID of the file.
243     * @return array $results The result array containing the size, url and restore url of the file.
244     */
245    public static function get_backup_file_info($filename, $filearea, $contextid) {
246        $fs = get_file_storage();
247        $file = $fs->get_file($contextid, 'backup', $filearea, 0, '/', $filename);
248        $filesize = display_size ($file->get_filesize());
249        $fileurl = moodle_url::make_pluginfile_url(
250            $file->get_contextid(),
251            $file->get_component(),
252            $file->get_filearea(),
253            null,
254            $file->get_filepath(),
255            $file->get_filename(),
256            true
257            );
258
259        $params = array();
260        $params['action'] = 'choosebackupfile';
261        $params['filename'] = $file->get_filename();
262        $params['filepath'] = $file->get_filepath();
263        $params['component'] = $file->get_component();
264        $params['filearea'] = $file->get_filearea();
265        $params['filecontextid'] = $file->get_contextid();
266        $params['contextid'] = $contextid;
267        $params['itemid'] = $file->get_itemid();
268        $restoreurl = new moodle_url('/backup/restorefile.php', $params);
269        $filesize = display_size ($file->get_filesize());
270
271        $results = array(
272            'filesize' => $filesize,
273            'fileurl' => $fileurl->out(false),
274            'restoreurl' => $restoreurl->out(false));
275
276        return $results;
277    }
278
279    /**
280     * Get the url of a restored backup item based on the backup ID.
281     *
282     * @param string $backupid The backup ID to get the restore location url.
283     * @return array $urlarray The restored item URL as an array.
284     */
285    public static function get_restore_url($backupid) {
286        global $DB;
287
288        $backupitemid = $DB->get_field('backup_controllers', 'itemid', array('backupid' => $backupid), MUST_EXIST);
289        $newcontext = context_course::instance($backupitemid);
290
291        $restoreurl = $newcontext->get_url()->out();
292        $urlarray = array('restoreurl' => $restoreurl);
293
294        return $urlarray;
295    }
296
297    /**
298     * Get markup for in progress async backups,
299     * to use in backup table UI.
300     *
301     * @param \core_backup_renderer $renderer The backup renderer object.
302     * @param integer $instanceid The context id to get backup data for.
303     * @return array $tabledata the rows of table data.
304     */
305    public static function get_async_backups($renderer, $instanceid) {
306        global $DB;
307
308        $tabledata = array();
309
310        // Get relevant backup ids based on context instance id.
311        $select = 'itemid = :itemid AND execution = :execution AND status < :status1 AND status > :status2 ' .
312            'AND operation = :operation';
313        $params = [
314            'itemid' => $instanceid,
315            'execution' => backup::EXECUTION_DELAYED,
316            'status1' => backup::STATUS_FINISHED_ERR,
317            'status2' => backup::STATUS_NEED_PRECHECK,
318            'operation' => 'backup',
319        ];
320
321        $backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
322        foreach ($backups as $backup) {
323            $bc = \backup_controller::load_controller($backup->backupid);  // Get the backup controller.
324            $filename = $bc->get_plan()->get_setting('filename')->get_value();
325            $timecreated = $backup->timecreated;
326            $status = $renderer->get_status_display($bc->get_status(), $bc->get_backupid());
327            $bc->destroy();
328
329            $tablerow = array($filename, userdate($timecreated), '-', '-', '-', $status);
330            $tabledata[] = $tablerow;
331        }
332
333        return $tabledata;
334    }
335
336    /**
337     * Get the course name of the resource being restored.
338     *
339     * @param \context $context The Moodle context for the restores.
340     * @return string $coursename The full name of the course.
341     */
342    public static function get_restore_name(\context $context) {
343        global $DB;
344        $instanceid = $context->instanceid;
345
346        if ($context->contextlevel == CONTEXT_MODULE) {
347            // For modules get the course name and module name.
348            $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
349            $coursename = $DB->get_field('course', 'fullname', array('id' => $cm->course));
350            $itemname = $coursename . ' - ' . $cm->name;
351        } else {
352            $itemname = $DB->get_field('course', 'fullname', array('id' => $context->instanceid));
353
354        }
355
356        return $itemname;
357    }
358
359    /**
360     * Get all the current in progress async restores for a user.
361     *
362     * @param int $userid Moodle user id.
363     * @return array $restores List of current restores in progress.
364     */
365    public static function get_async_restores($userid) {
366        global $DB;
367
368        $select = 'userid = ? AND execution = ? AND status < ? AND status > ? AND operation = ?';
369        $params = array($userid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK, 'restore');
370        $restores = $DB->get_records_select(
371            'backup_controllers',
372            $select,
373            $params,
374            'timecreated DESC',
375            'id, backupid, status, itemid, timecreated');
376
377            return $restores;
378    }
379
380}
381
382