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 * This file contains backup and restore output renderers
19 *
20 * @package   core_backup
21 * @copyright 2010 Sam Hemelryk
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die;
26
27global $CFG;
28require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
31
32/**
33 * The primary renderer for the backup.
34 *
35 * Can be retrieved with the following code:
36 * <?php
37 * $renderer = $PAGE->get_renderer('core', 'backup');
38 * ?>
39 *
40 * @package   core_backup
41 * @copyright 2010 Sam Hemelryk
42 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 */
44class core_backup_renderer extends plugin_renderer_base {
45
46    /**
47     * Same site notification display.
48     *
49     * @var string
50     */
51    private $samesitenotification = '';
52
53    /**
54     * Renderers a progress bar for the backup or restore given the items that make it up.
55     *
56     * @param array $items An array of items
57     * @return string
58     */
59    public function progress_bar(array $items) {
60        foreach ($items as &$item) {
61            $text = $item['text'];
62            unset($item['text']);
63            if (array_key_exists('link', $item)) {
64                $link = $item['link'];
65                unset($item['link']);
66                $item = html_writer::link($link, $text, $item);
67            } else {
68                $item = html_writer::tag('span', $text, $item);
69            }
70        }
71        return html_writer::tag('div', join(get_separator(), $items), array('class' => 'backup_progress clearfix'));
72    }
73
74    /**
75     * The backup and restore pages may display a log (if any) in a scrolling box.
76     *
77     * @param string $loghtml Log content in HTML format
78     * @return string HTML content that shows the log
79     */
80    public function log_display($loghtml) {
81        $out = html_writer::start_div('backup_log');
82        $out .= $this->output->heading(get_string('backuplog', 'backup'));
83        $out .= html_writer::start_div('backup_log_contents');
84        $out .= $loghtml;
85        $out .= html_writer::end_div();
86        $out .= html_writer::end_div();
87        return $out;
88    }
89
90    /**
91     * Set the same site backup notification.
92     *
93     */
94    public function set_samesite_notification() {
95        $this->samesitenotification = $this->output->notification(get_string('samesitenotification', 'backup'), 'info');
96    }
97
98    /**
99     * Get the same site backup notification.
100     *
101     */
102    public function get_samesite_notification() {
103        return $this->samesitenotification;
104    }
105
106    /**
107     * Prints a dependency notification
108     *
109     * @param string $message
110     * @return string
111     */
112    public function dependency_notification($message) {
113        return html_writer::tag('div', $message, array('class' => 'notification dependencies_enforced'));
114    }
115
116    /**
117     * Displays the details of a backup file
118     *
119     * @param stdClass $details
120     * @param moodle_url $nextstageurl
121     * @return string
122     */
123    public function backup_details($details, $nextstageurl) {
124        $yestick = $this->output->pix_icon('i/valid', get_string('yes'));
125        $notick = $this->output->pix_icon('i/invalid', get_string('no'));
126
127        $html  = html_writer::start_tag('div', array('class' => 'backup-restore'));
128
129        $html .= html_writer::start_tag('div', ['class' => 'backup-section',
130            'role' => 'table', 'aria-labelledby' => 'backupdetailsheader']);
131        $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, 'header', 'backupdetailsheader');
132        $html .= $this->backup_detail_pair(get_string('backuptype', 'backup'), get_string('backuptype'.$details->type, 'backup'));
133        $html .= $this->backup_detail_pair(get_string('backupformat', 'backup'), get_string('backupformat'.$details->format, 'backup'));
134        $html .= $this->backup_detail_pair(get_string('backupmode', 'backup'), get_string('backupmode'.$details->mode, 'backup'));
135        $html .= $this->backup_detail_pair(get_string('backupdate', 'backup'), userdate($details->backup_date));
136        $html .= $this->backup_detail_pair(get_string('moodleversion', 'backup'),
137                html_writer::tag('span', $details->moodle_release, array('class' => 'moodle_release')).
138                html_writer::tag('span', '['.$details->moodle_version.']', array('class' => 'moodle_version sub-detail')));
139        $html .= $this->backup_detail_pair(get_string('backupversion', 'backup'),
140                html_writer::tag('span', $details->backup_release, array('class' => 'moodle_release')).
141                html_writer::tag('span', '['.$details->backup_version.']', array('class' => 'moodle_version sub-detail')));
142        $html .= $this->backup_detail_pair(get_string('originalwwwroot', 'backup'),
143                html_writer::tag('span', $details->original_wwwroot, array('class' => 'originalwwwroot')).
144                html_writer::tag('span', '['.$details->original_site_identifier_hash.']', array('class' => 'sitehash sub-detail')));
145        if (!empty($details->include_file_references_to_external_content)) {
146            $message = '';
147            if (backup_general_helper::backup_is_samesite($details)) {
148                $message = $yestick . ' ' . get_string('filereferencessamesite', 'backup');
149            } else {
150                $message = $notick . ' ' . get_string('filereferencesnotsamesite', 'backup');
151            }
152            $html .= $this->backup_detail_pair(get_string('includefilereferences', 'backup'), $message);
153        }
154
155        $html .= html_writer::end_tag('div');
156
157        $html .= html_writer::start_tag('div', ['class' => 'backup-section settings-section',
158            'role' => 'table', 'aria-labelledby' => 'backupsettingsheader']);
159        $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, 'header', 'backupsettingsheader');
160        foreach ($details->root_settings as $label => $value) {
161            if ($label == 'filename' or $label == 'user_files') {
162                continue;
163            }
164            $html .= $this->backup_detail_pair(get_string('rootsetting'.str_replace('_', '', $label), 'backup'), $value ? $yestick : $notick);
165        }
166        $html .= html_writer::end_tag('div');
167
168        if ($details->type === 'course') {
169            $html .= html_writer::start_tag('div', ['class' => 'backup-section',
170                    'role' => 'table', 'aria-labelledby' => 'backupcoursedetailsheader']);
171            $html .= $this->output->heading(get_string('backupcoursedetails', 'backup'), 2, 'header', 'backupcoursedetailsheader');
172            $html .= $this->backup_detail_pair(get_string('coursetitle', 'backup'), $details->course->title);
173            $html .= $this->backup_detail_pair(get_string('courseid', 'backup'), $details->course->courseid);
174
175            // Warning users about front page backups.
176            if ($details->original_course_format === 'site') {
177                $html .= $this->backup_detail_pair(get_string('type_format', 'plugin'), get_string('sitecourseformatwarning', 'backup'));
178            }
179            $html .= html_writer::start_tag('div', array('class' => 'backup-sub-section'));
180            $html .= $this->output->heading(get_string('backupcoursesections', 'backup'), 3, array('class' => 'subheader'));
181            foreach ($details->sections as $key => $section) {
182                $included = $key.'_included';
183                $userinfo = $key.'_userinfo';
184                if ($section->settings[$included] && $section->settings[$userinfo]) {
185                    $value = get_string('sectionincanduser', 'backup');
186                } else if ($section->settings[$included]) {
187                    $value = get_string('sectioninc', 'backup');
188                } else {
189                    continue;
190                }
191                $html .= $this->backup_detail_pair(get_string('backupcoursesection', 'backup', $section->title), $value);
192                $table = null;
193                foreach ($details->activities as $activitykey => $activity) {
194                    if ($activity->sectionid != $section->sectionid) {
195                        continue;
196                    }
197                    if (empty($table)) {
198                        $table = new html_table();
199                        $table->head = array(get_string('module', 'backup'), get_string('title', 'backup'), get_string('userinfo', 'backup'));
200                        $table->colclasses = array('modulename', 'moduletitle', 'userinfoincluded');
201                        $table->align = array('left', 'left', 'center');
202                        $table->attributes = array('class' => 'activitytable generaltable');
203                        $table->data = array();
204                    }
205                    $name = get_string('pluginname', $activity->modulename);
206                    $icon = new image_icon('icon', '', $activity->modulename, ['class' => 'iconlarge icon-pre']);
207                    $table->data[] = array(
208                        $this->output->render($icon).$name,
209                        $activity->title,
210                        ($activity->settings[$activitykey.'_userinfo']) ? $yestick : $notick,
211                    );
212                }
213                if (!empty($table)) {
214                    $html .= $this->backup_detail_pair(get_string('sectionactivities', 'backup'), html_writer::table($table));
215                }
216
217            }
218            $html .= html_writer::end_tag('div');
219            $html .= html_writer::end_tag('div');
220        }
221
222        $html .= $this->continue_button($nextstageurl, 'post');
223        $html .= html_writer::end_tag('div');
224
225        return $html;
226    }
227
228    /**
229     * Displays the general information about a backup file with non-standard format
230     *
231     * @param moodle_url $nextstageurl URL to send user to
232     * @param array $details basic info about the file (format, type)
233     * @return string HTML code to display
234     */
235    public function backup_details_nonstandard($nextstageurl, array $details) {
236
237        $html  = html_writer::start_tag('div', array('class' => 'backup-restore nonstandardformat'));
238        $html .= html_writer::start_tag('div', array('class' => 'backup-section'));
239        $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, 'header');
240        $html .= $this->output->box(get_string('backupdetailsnonstandardinfo', 'backup'), 'noticebox');
241        $html .= $this->backup_detail_pair(
242            get_string('backupformat', 'backup'),
243            get_string('backupformat'.$details['format'], 'backup'));
244        $html .= $this->backup_detail_pair(
245            get_string('backuptype', 'backup'),
246            get_string('backuptype'.$details['type'], 'backup'));
247        $html .= html_writer::end_tag('div');
248        $html .= $this->continue_button($nextstageurl, 'post');
249        $html .= html_writer::end_tag('div');
250
251        return $html;
252    }
253
254    /**
255     * Displays the general information about a backup file with unknown format
256     *
257     * @param moodle_url $nextstageurl URL to send user to
258     * @return string HTML code to display
259     */
260    public function backup_details_unknown(moodle_url $nextstageurl) {
261
262        $html  = html_writer::start_div('unknownformat');
263        $html .= $this->output->heading(get_string('errorinvalidformat', 'backup'), 2);
264        $html .= $this->output->notification(get_string('errorinvalidformatinfo', 'backup'), 'notifyproblem');
265        $html .= $this->continue_button($nextstageurl, 'post');
266        $html .= html_writer::end_div();
267
268        return $html;
269    }
270
271    /**
272     * Displays a course selector for restore
273     *
274     * @param moodle_url $nextstageurl
275     * @param bool $wholecourse true if we are restoring whole course (as with backup::TYPE_1COURSE), false otherwise
276     * @param restore_category_search $categories
277     * @param restore_course_search $courses
278     * @param int $currentcourse
279     * @return string
280     */
281    public function course_selector(moodle_url $nextstageurl, $wholecourse = true, restore_category_search $categories = null,
282                                    restore_course_search $courses = null, $currentcourse = null) {
283        global $CFG;
284        require_once($CFG->dirroot.'/course/lib.php');
285
286        // These variables are used to check if the form using this function was submitted.
287        $target = optional_param('target', false, PARAM_INT);
288        $targetid = optional_param('targetid', null, PARAM_INT);
289
290        // Check if they submitted the form but did not provide all the data we need.
291        $missingdata = false;
292        if ($target and is_null($targetid)) {
293            $missingdata = true;
294        }
295
296        $nextstageurl->param('sesskey', sesskey());
297
298        $form = html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring(),
299            'class' => 'mform'));
300        foreach ($nextstageurl->params() as $key => $value) {
301            $form .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $key, 'value' => $value));
302        }
303
304        $hasrestoreoption = false;
305
306        $html  = html_writer::start_tag('div', array('class' => 'backup-course-selector backup-restore'));
307        if ($wholecourse && !empty($categories) && ($categories->get_count() > 0 || $categories->get_search())) {
308            // New course.
309            $hasrestoreoption = true;
310            $html .= $form;
311            $html .= html_writer::start_tag('div', array('class' => 'bcs-new-course backup-section'));
312            $html .= $this->output->heading(get_string('restoretonewcourse', 'backup'), 2, array('class' => 'header'));
313            $html .= $this->backup_detail_input(get_string('restoretonewcourse', 'backup'), 'radio', 'target',
314                backup::TARGET_NEW_COURSE, array('checked' => 'checked'));
315            $selectacategoryhtml = $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories));
316            // Display the category selection as required if the form was submitted but this data was not supplied.
317            if ($missingdata && $target == backup::TARGET_NEW_COURSE) {
318                $html .= html_writer::span(get_string('required'), 'error');
319                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
320                $html .= $selectacategoryhtml;
321                $html .= html_writer::end_tag('fieldset');
322            } else {
323                $html .= $selectacategoryhtml;
324            }
325            $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
326            $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs));
327            $html .= html_writer::end_tag('div');
328            $html .= html_writer::end_tag('form');
329        }
330
331        if ($wholecourse && !empty($currentcourse)) {
332            // Current course.
333            $hasrestoreoption = true;
334            $html .= $form;
335            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'targetid', 'value' => $currentcourse));
336            $html .= html_writer::start_tag('div', array('class' => 'bcs-current-course backup-section'));
337            $html .= $this->output->heading(get_string('restoretocurrentcourse', 'backup'), 2, array('class' => 'header'));
338            $html .= $this->backup_detail_input(get_string('restoretocurrentcourseadding', 'backup'), 'radio', 'target',
339                backup::TARGET_CURRENT_ADDING, array('checked' => 'checked'));
340            $html .= $this->backup_detail_input(get_string('restoretocurrentcoursedeleting', 'backup'), 'radio', 'target',
341                backup::TARGET_CURRENT_DELETING);
342            $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
343            $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs));
344            $html .= html_writer::end_tag('div');
345            $html .= html_writer::end_tag('form');
346        }
347
348        // If we are restoring an activity, then include the current course.
349        if (!$wholecourse) {
350            $courses->invalidate_results(); // Clean list of courses.
351            $courses->set_include_currentcourse();
352        }
353        if (!empty($courses) && ($courses->get_count() > 0 || $courses->get_search())) {
354            // Existing course.
355            $hasrestoreoption = true;
356            $html .= $form;
357            $html .= html_writer::start_tag('div', array('class' => 'bcs-existing-course backup-section'));
358            $html .= $this->output->heading(get_string('restoretoexistingcourse', 'backup'), 2, array('class' => 'header'));
359            if ($wholecourse) {
360                $html .= $this->backup_detail_input(get_string('restoretoexistingcourseadding', 'backup'), 'radio', 'target',
361                    backup::TARGET_EXISTING_ADDING, array('checked' => 'checked'));
362                $html .= $this->backup_detail_input(get_string('restoretoexistingcoursedeleting', 'backup'), 'radio', 'target',
363                    backup::TARGET_EXISTING_DELETING);
364            } else {
365                $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'target', 'value' => backup::TARGET_EXISTING_ADDING));
366            }
367            $selectacoursehtml = $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
368            // Display the course selection as required if the form was submitted but this data was not supplied.
369            if ($missingdata && $target == backup::TARGET_EXISTING_ADDING) {
370                $html .= html_writer::span(get_string('required'), 'error');
371                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
372                $html .= $selectacoursehtml;
373                $html .= html_writer::end_tag('fieldset');
374            } else {
375                $html .= $selectacoursehtml;
376            }
377            $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
378            $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs));
379            $html .= html_writer::end_tag('div');
380            $html .= html_writer::end_tag('form');
381        }
382
383        if (!$hasrestoreoption) {
384            echo $this->output->notification(get_string('norestoreoptions', 'backup'));
385        }
386
387        $html .= html_writer::end_tag('div');
388        return $html;
389    }
390
391    /**
392     * Displays the import course selector
393     *
394     * @param moodle_url $nextstageurl
395     * @param import_course_search $courses
396     * @return string
397     */
398    public function import_course_selector(moodle_url $nextstageurl, import_course_search $courses = null) {
399        $html  = html_writer::start_tag('div', array('class' => 'import-course-selector backup-restore'));
400        $html .= html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring()));
401        foreach ($nextstageurl->params() as $key => $value) {
402            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $key, 'value' => $value));
403        }
404        // We only allow import adding for now. Enforce it here.
405        $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'target', 'value' => backup::TARGET_CURRENT_ADDING));
406        $html .= html_writer::start_tag('div', array('class' => 'ics-existing-course backup-section'));
407        $html .= $this->output->heading(get_string('importdatafrom'), 2, array('class' => 'header'));
408        $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
409        $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
410        $html .= html_writer::start_tag('div', array('class' => 'mt-3'));
411        $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs));
412        $html .= html_writer::end_tag('div');
413        $html .= html_writer::end_tag('div');
414        $html .= html_writer::end_tag('form');
415        $html .= html_writer::end_tag('div');
416        return $html;
417    }
418
419    /**
420     * Creates a detailed pairing (key + value)
421     *
422     * @staticvar int $count
423     * @param string $label
424     * @param string $value
425     * @return string
426     */
427    protected function backup_detail_pair($label, $value) {
428        static $count = 0;
429        $count ++;
430        $html  = html_writer::start_tag('div', ['class' => 'detail-pair', 'role' => 'row']);
431        $html .= html_writer::tag('div', $label, ['class' => 'detail-pair-label mb-2', 'role' => 'cell']);
432        $html .= html_writer::tag('div', $value, ['class' => 'detail-pair-value pl-2', 'role' => 'cell']);
433        $html .= html_writer::end_tag('div');
434        return $html;
435    }
436
437    /**
438     * Creates a unique id string by appending an incremental number to the prefix.
439     *
440     * @param string $prefix To be used as the left part of the id string.
441     * @return string
442     */
443    protected function make_unique_id(string $prefix): string {
444        static $count = 0;
445
446        return $prefix . '-' . $count++;
447    }
448
449    /**
450     * Created a detailed pairing with an input
451     *
452     * @param string $label
453     * @param string $type
454     * @param string $name
455     * @param string $value
456     * @param array $attributes
457     * @param string|null $description
458     * @return string
459     */
460    protected function backup_detail_input($label, $type, $name, $value, array $attributes = array(), $description = null) {
461        if (!empty($description)) {
462            $description = html_writer::tag('span', $description, array('class' => 'description'));
463        } else {
464            $description = '';
465        }
466        $id = $this->make_unique_id('detail-pair-value');
467        return $this->backup_detail_pair(
468            html_writer::label($label, $id),
469            html_writer::empty_tag('input', $attributes + ['id' => $id, 'name' => $name, 'type' => $type, 'value' => $value]) .
470                $description
471        );
472    }
473
474    /**
475     * Creates a detailed pairing with a select
476     *
477     * @param string $label
478     * @param string $name
479     * @param array $options
480     * @param string $selected
481     * @param bool $nothing
482     * @param array $attributes
483     * @param string|null $description
484     * @return string
485     */
486    protected function backup_detail_select($label, $name, $options, $selected = '', $nothing = false, array $attributes = array(), $description = null) {
487        if (!empty ($description)) {
488            $description = html_writer::tag('span', $description, array('class' => 'description'));
489        } else {
490            $description = '';
491        }
492        return $this->backup_detail_pair($label, html_writer::select($options, $name, $selected, false, $attributes).$description);
493    }
494
495    /**
496     * Displays precheck notices
497     *
498     * @param array $results
499     * @return string
500     */
501    public function precheck_notices($results) {
502        $output = html_writer::start_tag('div', array('class' => 'restore-precheck-notices'));
503        if (array_key_exists('errors', $results)) {
504            foreach ($results['errors'] as $error) {
505                $output .= $this->output->notification($error);
506            }
507        }
508        if (array_key_exists('warnings', $results)) {
509            foreach ($results['warnings'] as $warning) {
510                $output .= $this->output->notification($warning, 'notifyproblem');
511            }
512        }
513        return $output.html_writer::end_tag('div');
514    }
515
516    /**
517     * Displays substage buttons
518     *
519     * @param bool $haserrors
520     * @return string
521     */
522    public function substage_buttons($haserrors) {
523        $output  = html_writer::start_tag('div', array('continuebutton'));
524        if (!$haserrors) {
525            $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
526            $output .= html_writer::empty_tag('input', $attrs);
527        }
528        $attrs = array('type' => 'submit', 'name' => 'cancel', 'value' => get_string('cancel'), 'class' => 'btn btn-secondary');
529        $output .= html_writer::empty_tag('input', $attrs);
530        $output .= html_writer::end_tag('div');
531        return $output;
532    }
533
534    /**
535     * Displays a role mapping interface
536     *
537     * @param array $rolemappings
538     * @param array $roles
539     * @return string
540     */
541    public function role_mappings($rolemappings, $roles) {
542        $roles[0] = get_string('none');
543        $output  = html_writer::start_tag('div', array('class' => 'restore-rolemappings'));
544        $output .= $this->output->heading(get_string('restorerolemappings', 'backup'), 2);
545        foreach ($rolemappings as $id => $mapping) {
546            $label = $mapping->name;
547            $name = 'mapping'.$id;
548            $selected = $mapping->targetroleid;
549            $output .= $this->backup_detail_select($label, $name, $roles, $mapping->targetroleid, false, array(), $mapping->description);
550        }
551        $output .= html_writer::end_tag('div');
552        return $output;
553    }
554
555    /**
556     * Displays a continue button
557     *
558     * @param string|moodle_url $url
559     * @param string $method
560     * @return string
561     */
562    public function continue_button($url, $method = 'post') {
563        if (!($url instanceof moodle_url)) {
564            $url = new moodle_url($url);
565        }
566        if ($method != 'post') {
567            $method = 'get';
568        }
569        $url->param('sesskey', sesskey());
570        $button = new single_button($url, get_string('continue'), $method, true);
571        $button->class = 'continuebutton';
572        return $this->render($button);
573    }
574    /**
575     * Print a backup files tree
576     * @param array $options
577     * @return string
578     */
579    public function backup_files_viewer(array $options = null) {
580        $files = new backup_files_viewer($options);
581        return $this->render($files);
582    }
583
584    /**
585     * Generate the status indicator markup for display in the
586     * backup restore file area UI.
587     *
588     * @param int $statuscode The status code of the backup.
589     * @param string $backupid The backup record id.
590     * @return string|boolean $status The status indicator for the operation.
591     */
592    public function get_status_display($statuscode, $backupid, $restoreid=null, $operation='backup') {
593        if ($statuscode == backup::STATUS_AWAITING
594            || $statuscode == backup::STATUS_EXECUTING
595            || $statuscode == backup::STATUS_REQUIRE_CONV) {  // In progress.
596            $progresssetup = array(
597                'backupid' => $backupid,
598                'restoreid' => $restoreid,
599                'operation' => $operation,
600                'width' => '100'
601            );
602            $status = $this->render_from_template('core/async_backup_progress', $progresssetup);
603        } else if ($statuscode == backup::STATUS_FINISHED_ERR) { // Error.
604            $icon = $this->output->render(new \pix_icon('i/delete', get_string('failed', 'backup')));
605            $status = \html_writer::span($icon, 'action-icon');
606        } else if ($statuscode == backup::STATUS_FINISHED_OK) { // Complete.
607            $icon = $this->output->render(new \pix_icon('i/checked', get_string('successful', 'backup')));
608            $status = \html_writer::span($icon, 'action-icon');
609        }
610
611        return $status;
612    }
613
614    /**
615     * Displays a backup files viewer
616     *
617     * @global stdClass $USER
618     * @param backup_files_viewer $viewer
619     * @return string
620     */
621    public function render_backup_files_viewer(backup_files_viewer $viewer) {
622        global $CFG;
623        $files = $viewer->files;
624
625        $async = async_helper::is_async_enabled();
626
627        $tablehead = array(
628                get_string('filename', 'backup'),
629                get_string('time'),
630                get_string('size'),
631                get_string('download'),
632                get_string('restore'));
633        if ($async) {
634            $tablehead[] = get_string('status', 'backup');
635        }
636
637        $table = new html_table();
638        $table->attributes['class'] = 'backup-files-table generaltable';
639        $table->head = $tablehead;
640        $table->width = '100%';
641        $table->data = array();
642
643        // First add in progress asynchronous backups.
644        // Only if asynchronous backups are enabled.
645        // Also only render async status in correct area. Courese OR activity (not both).
646        if ($async
647                && (($viewer->filearea == 'course' && $viewer->currentcontext->contextlevel == CONTEXT_COURSE)
648                || ($viewer->filearea == 'activity' && $viewer->currentcontext->contextlevel == CONTEXT_MODULE))
649                ) {
650                    $table->data = \async_helper::get_async_backups($this, $viewer->currentcontext->instanceid);
651        }
652
653        // Add completed backups.
654        foreach ($files as $file) {
655            if ($file->is_directory()) {
656                continue;
657            }
658            $fileurl = moodle_url::make_pluginfile_url(
659                $file->get_contextid(),
660                $file->get_component(),
661                $file->get_filearea(),
662                null,
663                $file->get_filepath(),
664                $file->get_filename(),
665                true
666            );
667            $params = array();
668            $params['action'] = 'choosebackupfile';
669            $params['filename'] = $file->get_filename();
670            $params['filepath'] = $file->get_filepath();
671            $params['component'] = $file->get_component();
672            $params['filearea'] = $file->get_filearea();
673            $params['filecontextid'] = $file->get_contextid();
674            $params['contextid'] = $viewer->currentcontext->id;
675            $params['itemid'] = $file->get_itemid();
676            $restoreurl = new moodle_url('/backup/restorefile.php', $params);
677            $restorelink = html_writer::link($restoreurl, get_string('restore'));
678            $downloadlink = html_writer::link($fileurl, get_string('download'));
679
680            // Conditional display of the restore and download links, initially only for the 'automated' filearea.
681            if ($params['filearea'] == 'automated') {
682                if (!has_capability('moodle/restore:viewautomatedfilearea', $viewer->currentcontext)) {
683                    $restorelink = '';
684                }
685                if (!can_download_from_backup_filearea($params['filearea'], $viewer->currentcontext)) {
686                    $downloadlink = '';
687                }
688            }
689            $tabledata = array(
690                $file->get_filename(),
691                userdate ($file->get_timemodified()),
692                display_size ($file->get_filesize()),
693                $downloadlink,
694                $restorelink
695            );
696            if ($async) {
697                $tabledata[] = $this->get_status_display(backup::STATUS_FINISHED_OK, null);
698            }
699
700            $table->data[] = $tabledata;
701        }
702
703        $html = html_writer::table($table);
704
705        // For automated backups, the ability to manage backup files is controlled by the ability to download them.
706        // All files must be from the same file area in a backup_files_viewer.
707        $canmanagebackups = true;
708        if ($viewer->filearea == 'automated') {
709            if (!can_download_from_backup_filearea($viewer->filearea, $viewer->currentcontext)) {
710                $canmanagebackups = false;
711            }
712        }
713
714        if ($canmanagebackups) {
715            $html .= $this->output->single_button(
716                new moodle_url('/backup/backupfilesedit.php', array(
717                        'currentcontext' => $viewer->currentcontext->id,
718                        'contextid' => $viewer->filecontext->id,
719                        'filearea' => $viewer->filearea,
720                        'component' => $viewer->component,
721                        'returnurl' => $this->page->url->out())
722                ),
723                get_string('managefiles', 'backup'),
724                'post'
725            );
726        }
727
728        return $html;
729    }
730
731    /**
732     * Renders a restore course search object
733     *
734     * @param restore_course_search $component
735     * @return string
736     */
737    public function render_restore_course_search(restore_course_search $component) {
738        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
739        $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
740
741        $table = new html_table();
742        $table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse'));
743        $table->data = array();
744        if ($component->get_count() !== 0) {
745            foreach ($component->get_results() as $course) {
746                $row = new html_table_row();
747                $row->attributes['class'] = 'rcs-course';
748                if (!$course->visible) {
749                    $row->attributes['class'] .= ' dimmed';
750                }
751                $id = $this->make_unique_id('restore-course');
752                $row->cells = [
753                    html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'targetid', 'value' => $course->id,
754                        'id' => $id]),
755                    html_writer::label(
756                        format_string($course->shortname, true, ['context' => context_course::instance($course->id)]),
757                        $id,
758                        true,
759                        ['class' => 'd-block']
760                    ),
761                    format_string($course->fullname, true, ['context' => context_course::instance($course->id)])
762                ];
763                $table->data[] = $row;
764            }
765            if ($component->has_more_results()) {
766                $cell = new html_table_cell(get_string('moreresults', 'backup'));
767                $cell->colspan = 3;
768                $cell->attributes['class'] = 'notifyproblem';
769                $row = new html_table_row(array($cell));
770                $row->attributes['class'] = 'rcs-course';
771                $table->data[] = $row;
772            }
773        } else {
774            $cell = new html_table_cell(get_string('nomatchingcourses', 'backup'));
775            $cell->colspan = 3;
776            $cell->attributes['class'] = 'notifyproblem';
777            $row = new html_table_row(array($cell));
778            $row->attributes['class'] = 'rcs-course';
779            $table->data[] = $row;
780        }
781        $output .= html_writer::table($table);
782        $output .= html_writer::end_tag('div');
783
784        $data = [
785            'inform' => true,
786            'extraclasses' => 'rcs-search mb-3 w-25',
787            'inputname' => restore_course_search::$VAR_SEARCH,
788            'searchstring' => get_string('searchcourses'),
789            'query' => $component->get_search(),
790        ];
791        $output .= $this->output->render_from_template('core/search_input', $data);
792
793        $output .= html_writer::end_tag('div');
794        return $output;
795    }
796
797    /**
798     * Renders an import course search object
799     *
800     * @param import_course_search $component
801     * @return string
802     */
803    public function render_import_course_search(import_course_search $component) {
804        $output = html_writer::start_tag('div', array('class' => 'import-course-search'));
805        if ($component->get_count() === 0) {
806            $output .= $this->output->notification(get_string('nomatchingcourses', 'backup'));
807
808            $output .= html_writer::start_tag('div', array('class' => 'ics-search form-inline'));
809            $attrs = array(
810                'type' => 'text',
811                'name' => restore_course_search::$VAR_SEARCH,
812                'value' => $component->get_search(),
813                'aria-label' => get_string('searchcourses'),
814                'placeholder' => get_string('searchcourses'),
815                'class' => 'form-control'
816            );
817            $output .= html_writer::empty_tag('input', $attrs);
818            $attrs = array(
819                'type' => 'submit',
820                'name' => 'searchcourses',
821                'value' => get_string('search'),
822                'class' => 'btn btn-secondary ml-1'
823            );
824            $output .= html_writer::empty_tag('input', $attrs);
825            $output .= html_writer::end_tag('div');
826
827            $output .= html_writer::end_tag('div');
828            return $output;
829        }
830
831        $countstr = '';
832        if ($component->has_more_results()) {
833            $countstr = get_string('morecoursesearchresults', 'backup', $component->get_count());
834        } else {
835            $countstr = get_string('totalcoursesearchresults', 'backup', $component->get_count());
836        }
837
838        $output .= html_writer::tag('div', $countstr, array('class' => 'ics-totalresults'));
839        $output .= html_writer::start_tag('div', array('class' => 'ics-results'));
840
841        $table = new html_table();
842        $table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse'));
843        $table->data = array();
844        foreach ($component->get_results() as $course) {
845            $row = new html_table_row();
846            $row->attributes['class'] = 'ics-course';
847            if (!$course->visible) {
848                $row->attributes['class'] .= ' dimmed';
849            }
850            $id = $this->make_unique_id('import-course');
851            $row->cells = [
852                html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'importid', 'value' => $course->id,
853                    'id' => $id]),
854                html_writer::label(
855                    format_string($course->shortname, true, ['context' => context_course::instance($course->id)]),
856                    $id,
857                    true,
858                    ['class' => 'd-block']
859                ),
860                format_string($course->fullname, true, ['context' => context_course::instance($course->id)])
861            ];
862            $table->data[] = $row;
863        }
864        if ($component->has_more_results()) {
865            $cell = new html_table_cell(get_string('moreresults', 'backup'));
866            $cell->colspan = 3;
867            $cell->attributes['class'] = 'notifyproblem';
868            $row = new html_table_row(array($cell));
869            $row->attributes['class'] = 'rcs-course';
870            $table->data[] = $row;
871        }
872        $output .= html_writer::table($table);
873        $output .= html_writer::end_tag('div');
874
875        $output .= html_writer::start_tag('div', array('class' => 'ics-search form-inline'));
876        $attrs = array(
877            'type' => 'text',
878            'name' => restore_course_search::$VAR_SEARCH,
879            'value' => $component->get_search(),
880            'aria-label' => get_string('searchcourses'),
881            'placeholder' => get_string('searchcourses'),
882            'class' => 'form-control');
883        $output .= html_writer::empty_tag('input', $attrs);
884        $attrs = array(
885            'type' => 'submit',
886            'name' => 'searchcourses',
887            'value' => get_string('search'),
888            'class' => 'btn btn-secondary ml-1'
889        );
890        $output .= html_writer::empty_tag('input', $attrs);
891        $output .= html_writer::end_tag('div');
892
893        $output .= html_writer::end_tag('div');
894        return $output;
895    }
896
897    /**
898     * Renders a restore category search object
899     *
900     * @param restore_category_search $component
901     * @return string
902     */
903    public function render_restore_category_search(restore_category_search $component) {
904        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
905        $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
906
907        $table = new html_table();
908        $table->head = array('', get_string('name'), get_string('description'));
909        $table->data = array();
910
911        if ($component->get_count() !== 0) {
912            foreach ($component->get_results() as $category) {
913                $row = new html_table_row();
914                $row->attributes['class'] = 'rcs-course';
915                if (!$category->visible) {
916                    $row->attributes['class'] .= ' dimmed';
917                }
918                $context = context_coursecat::instance($category->id);
919                $id = $this->make_unique_id('restore-category');
920                $row->cells = [
921                    html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'targetid', 'value' => $category->id,
922                        'id' => $id]),
923                    html_writer::label(
924                        format_string($category->name, true, ['context' => context_coursecat::instance($category->id)]),
925                        $id,
926                        true,
927                        ['class' => 'd-block']
928                    ),
929                    format_text(file_rewrite_pluginfile_urls($category->description, 'pluginfile.php', $context->id,
930                        'coursecat', 'description', null), $category->descriptionformat, ['overflowdiv' => true])
931                ];
932                $table->data[] = $row;
933            }
934            if ($component->has_more_results()) {
935                $cell = new html_table_cell(get_string('moreresults', 'backup'));
936                $cell->attributes['class'] = 'notifyproblem';
937                $cell->colspan = 3;
938                $row = new html_table_row(array($cell));
939                $row->attributes['class'] = 'rcs-course';
940                $table->data[] = $row;
941            }
942        } else {
943            $cell = new html_table_cell(get_string('nomatchingcourses', 'backup'));
944            $cell->colspan = 3;
945            $cell->attributes['class'] = 'notifyproblem';
946            $row = new html_table_row(array($cell));
947            $row->attributes['class'] = 'rcs-course';
948            $table->data[] = $row;
949        }
950        $output .= html_writer::table($table);
951        $output .= html_writer::end_tag('div');
952
953        $data = [
954            'inform' => true,
955            'extraclasses' => 'rcs-search mb-3 w-25',
956            'inputname' => restore_category_search::$VAR_SEARCH,
957            'searchstring' => get_string('searchcoursecategories'),
958            'query' => $component->get_search(),
959        ];
960        $output .= $this->output->render_from_template('core/search_input', $data);
961
962        $output .= html_writer::end_tag('div');
963        return $output;
964    }
965
966    /**
967     * Get markup to render table for all of a users async
968     * in progress restores.
969     *
970     * @param int $userid The Moodle user id.
971     * @param \context $context The Moodle context for these restores.
972     * @return string $html The table HTML.
973     */
974    public function restore_progress_viewer ($userid, $context) {
975        $tablehead = array(get_string('course'), get_string('time'), get_string('status', 'backup'));
976
977        $table = new html_table();
978        $table->attributes['class'] = 'backup-files-table generaltable';
979        $table->head = $tablehead;
980        $tabledata = array();
981
982        // Get all in progress async restores for this user.
983        $restores = \async_helper::get_async_restores($userid);
984
985        // For each backup get, new item name, time restore created and progress.
986        foreach ($restores as $restore) {
987
988            $restorename = \async_helper::get_restore_name($context);
989            $timecreated = $restore->timecreated;
990            $status = $this->get_status_display($restore->status, $restore->backupid, $restore->backupid, null, 'restore');
991
992            $tablerow = array($restorename, userdate($timecreated), $status);
993            $tabledata[] = $tablerow;
994        }
995
996        $table->data = $tabledata;
997        $html = html_writer::table($table);
998
999        return $html;
1000    }
1001
1002    /**
1003     * Get markup to render table for all of a users course copies.
1004     *
1005     * @param int $userid The Moodle user id.
1006     * @param int $courseid The id of the course to get the backups for.
1007     * @return string $html The table HTML.
1008     */
1009    public function copy_progress_viewer(int $userid, int $courseid): string {
1010        $tablehead = array(
1011            get_string('copysource', 'backup'),
1012            get_string('copydest', 'backup'),
1013            get_string('time'),
1014            get_string('copyop', 'backup'),
1015            get_string('status', 'backup')
1016        );
1017
1018        $table = new html_table();
1019        $table->attributes['class'] = 'backup-files-table generaltable';
1020        $table->head = $tablehead;
1021
1022        $tabledata = array();
1023
1024        // Get all in progress course copies for this user.
1025        $copies = \core_backup\copy\copy::get_copies($userid, $courseid);
1026
1027        foreach ($copies as $copy) {
1028            $sourceurl = new \moodle_url('/course/view.php', array('id' => $copy->sourceid));
1029
1030            $tablerow = array(
1031                html_writer::link($sourceurl, $copy->source),
1032                $copy->destination,
1033                userdate($copy->time),
1034                get_string($copy->operation),
1035                $this->get_status_display($copy->status, $copy->backupid, $copy->restoreid, $copy->operation)
1036            );
1037            $tabledata[] = $tablerow;
1038        }
1039
1040        $table->data = $tabledata;
1041        $html = html_writer::table($table);
1042
1043        return $html;
1044    }
1045}
1046
1047/**
1048 * Data structure representing backup files viewer
1049 *
1050 * @copyright 2010 Dongsheng Cai
1051 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1052 * @since     Moodle 2.0
1053 */
1054class backup_files_viewer implements renderable {
1055
1056    /**
1057     * @var array
1058     */
1059    public $files;
1060
1061    /**
1062     * @var context
1063     */
1064    public $filecontext;
1065
1066    /**
1067     * @var string
1068     */
1069    public $component;
1070
1071    /**
1072     * @var string
1073     */
1074    public $filearea;
1075
1076    /**
1077     * @var context
1078     */
1079    public $currentcontext;
1080
1081    /**
1082     * Constructor of backup_files_viewer class
1083     * @param array $options
1084     */
1085    public function __construct(array $options = null) {
1086        global $CFG, $USER;
1087        $fs = get_file_storage();
1088        $this->currentcontext = $options['currentcontext'];
1089        $this->filecontext    = $options['filecontext'];
1090        $this->component      = $options['component'];
1091        $this->filearea       = $options['filearea'];
1092        $files = $fs->get_area_files($this->filecontext->id, $this->component, $this->filearea, false, 'timecreated');
1093        $this->files = array_reverse($files);
1094    }
1095}
1096