MDL-84440 backup: Add configurable default backup filename format
This commit is contained in:
@@ -416,6 +416,63 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_legacyfiles',
|
||||
new lang_string('generallegacyfiles', 'backup'),
|
||||
new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0)));
|
||||
|
||||
// Backup filename defaults.
|
||||
// Until MDL-83618 is fixed, this must be required as it will not be autoloaded.
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
$temp->add(
|
||||
new admin_setting_heading(
|
||||
'defaultbackupfilenamesettings',
|
||||
new lang_string('defaultbackupfilenamesettings', 'backup'),
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
$temp->add(
|
||||
new admin_setting_description(
|
||||
'defaultbackupfilenamesettings_help',
|
||||
'',
|
||||
new lang_string('defaultbackupfilenamesettings_help', 'backup'),
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
$temp->add(
|
||||
new admin_setting_configbackupfilenamemustachetemplate(
|
||||
'backup/backup_default_filename_template_course',
|
||||
new lang_string('defaultbackupfilenamecourse', 'backup'),
|
||||
new lang_string('defaultbackupfilenamecourse_desc', 'backup'),
|
||||
backup_plan_dbops::DEFAULT_FILENAME_TEMPLATE_COURSE,
|
||||
PARAM_TEXT,
|
||||
'60',
|
||||
'3'
|
||||
)
|
||||
);
|
||||
|
||||
$temp->add(
|
||||
new admin_setting_configbackupfilenamemustachetemplate(
|
||||
'backup/backup_default_filename_template_section',
|
||||
new lang_string('defaultbackupfilenamesection', 'backup'),
|
||||
new lang_string('defaultbackupfilenamesection_desc', 'backup'),
|
||||
backup_plan_dbops::DEFAULT_FILENAME_TEMPLATE_SECTION,
|
||||
PARAM_TEXT,
|
||||
'60',
|
||||
'3'
|
||||
)
|
||||
);
|
||||
|
||||
$temp->add(
|
||||
new admin_setting_configbackupfilenamemustachetemplate(
|
||||
'backup/backup_default_filename_template_activity',
|
||||
new lang_string('defaultbackupfilenameactivity', 'backup'),
|
||||
new lang_string('defaultbackupfilenameactivity_desc', 'backup'),
|
||||
backup_plan_dbops::DEFAULT_FILENAME_TEMPLATE_ACTIVITY,
|
||||
PARAM_TEXT,
|
||||
'60',
|
||||
'3'
|
||||
)
|
||||
);
|
||||
|
||||
$ADMIN->add('backups', $temp);
|
||||
|
||||
// Create a page for general import configuration and defaults.
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_backup;
|
||||
|
||||
use advanced_testcase;
|
||||
use backup;
|
||||
use backup_plan_dbops;
|
||||
use core\exception\coding_exception;
|
||||
use core_courseformat\local\sectionactions;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
|
||||
/**
|
||||
* Tests related to the default backup filename feature.
|
||||
*
|
||||
* @package core_backup
|
||||
* @copyright 2025 Matthew Hilton <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class default_backup_filename_test extends advanced_testcase {
|
||||
/**
|
||||
* Provides backup filename scenarios.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_default_backup_filename_provider(): array {
|
||||
return [
|
||||
// This first block of tests are tests that confirm the new mustache
|
||||
// rendering method internally produces the same output as the original function.
|
||||
'moodle format, course, with users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, course, without users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => false,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-19700101-0800-nu.mbz',
|
||||
],
|
||||
'moodle format, course, ith users, not anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => false,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-19700101-0800.mbz',
|
||||
],
|
||||
'moodle format, course, with users, anonymised, use id only, without files' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => false,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-19700101-0800-an-nf.mbz',
|
||||
],
|
||||
'moodle format, course, with users, anonymised, not id only, with files' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-abc123-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, section, with users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'abc123',
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, section, without users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'abc123',
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => false,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-19700101-0800-nu.mbz',
|
||||
],
|
||||
'moodle format, section, with users, not anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'abc123',
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => false,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-19700101-0800.mbz',
|
||||
],
|
||||
'moodle format, section, with users, anonymised, without use id only (with section name), with files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'abc123',
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
// Where section has name, it uses the name.
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-abc123-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, section, with users, anonymised, without use id only (no section name), with files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
// Section has no name, it uses the number instead.
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-9-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, section, with users, anonymised, with use id only, without files' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'abc123',
|
||||
'section' => 9,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => false,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-section-{{sectionid}}-19700101-0800-an-nf.mbz',
|
||||
],
|
||||
'moodle format, activity, with users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-activity-{{activitycmid}}-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, activity, without users, anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => false,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-activity-{{activitycmid}}-19700101-0800-nu.mbz',
|
||||
],
|
||||
'moodle format, activity, with users, not anonymised, use id only, with files' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => true,
|
||||
'anonymised' => false,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-activity-{{activitycmid}}-19700101-0800.mbz',
|
||||
],
|
||||
'moodle format, activity, with users, anonymised, without use id only, with files' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-activity-{{activitycmid}}-page{{activitycmid}}-19700101-0800-an.mbz',
|
||||
],
|
||||
'moodle format, activity, with users, anonymised, with id only, without files' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => false,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-activity-{{activitycmid}}-19700101-0800-an-nf.mbz',
|
||||
],
|
||||
|
||||
// This second block tests custom template functions being used.
|
||||
'custom template - course' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'shortcourse',
|
||||
'fullname' => 'fullcourse',
|
||||
'startdate' => 5000,
|
||||
'enddate' => 10000,
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' =>
|
||||
'{{course.shortname}}-{{course.fullname}}-{{id}}-{{date}}-{{course.startdate}}-{{course.enddate}}',
|
||||
],
|
||||
'expectedfilename' => 'shortcourse-fullcourse-{{courseid}}-19700101-0800-19700101-0923-19700101-1046.mbz',
|
||||
],
|
||||
'custom template - section' => [
|
||||
'generate' => [
|
||||
'section' => [
|
||||
'name' => 'section123',
|
||||
'section' => '1',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1SECTION,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_section' => '{{section.name}}-{{section.section}}-{{id}}-{{date}}',
|
||||
],
|
||||
'expectedfilename' => 'section123-1-{{sectionid}}-19700101-0800.mbz',
|
||||
],
|
||||
'custom template - activity' => [
|
||||
'generate' => [
|
||||
'activity' => [
|
||||
'name' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1ACTIVITY,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_activity' => '{{activity.modname}}-{{activity.name}}-{{id}}-{{date}}',
|
||||
],
|
||||
'expectedfilename' => 'page-abc123-{{activitycmid}}-19700101-0800.mbz',
|
||||
],
|
||||
|
||||
// This third block tests various edge cases.
|
||||
'spaces in context values and custom template are replaced' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'a b c',
|
||||
'fullname' => 'x y z',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' => '{{course.shortname}} {{course.fullname}}',
|
||||
],
|
||||
'expectedfilename' => 'a_b_c___x_y_z.mbz',
|
||||
],
|
||||
'spaces in context values without custom template are replaced' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'a b c',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-a_b_c-19700101-0800-an.mbz',
|
||||
],
|
||||
'whitespace trimmed from context values with custom template' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => " abc \n",
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' => '{{course.shortname}}',
|
||||
],
|
||||
'expectedfilename' => 'abc.mbz',
|
||||
],
|
||||
'whitespace trimmed from context values without custom template' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => " abc \n",
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-abc-19700101-0800-an.mbz',
|
||||
],
|
||||
'format string applied to context values with custom template' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
// Format_string will remove the link and newline before using.
|
||||
'shortname' => "<a href='test'/>\nabc",
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' => '{{course.shortname}}',
|
||||
],
|
||||
'expectedfilename' => 'abc.mbz',
|
||||
],
|
||||
'format string applied to context values without custom template' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
// Format_string will remove the link and newline before using.
|
||||
'shortname' => "<a href='test'/>\nabc",
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => false,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-abc-19700101-0800-an.mbz',
|
||||
],
|
||||
'generated name > 251 chars is truncated' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " .
|
||||
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " .
|
||||
"nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor.",
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' => '{{course.shortname}}',
|
||||
],
|
||||
'expectedfilename' => 'Lorem_ipsum_dolor_sit_amet,_consectetur_adipiscing_elit,_sed_do_eiusmod_' .
|
||||
'tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_' .
|
||||
'exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dol.mbz',
|
||||
],
|
||||
'course custom template is invalid, falls back to default' => [
|
||||
'generate' => [
|
||||
'course' => [
|
||||
'shortname' => 'abc123',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'format' => backup::FORMAT_MOODLE,
|
||||
'type' => backup::TYPE_1COURSE,
|
||||
'users' => true,
|
||||
'anonymised' => true,
|
||||
'useidonly' => true,
|
||||
'files' => true,
|
||||
],
|
||||
'customtemplates' => [
|
||||
'backup_default_filename_template_course' => '{{',
|
||||
],
|
||||
'expectedfilename' => 'backup-moodle2-course-{{courseid}}-19700101-0800-an.mbz',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests get_default_backup_filename.
|
||||
*
|
||||
* @param array $generate array of resources to generate (courses, sections, activities).
|
||||
* @param array $params parameters to pass into get_default_backup_filename.
|
||||
* @param array $customtemplates array of key value pairs of config values, for setting the custom template config.
|
||||
* @param string $expectedfilename the filename expected to be generated.
|
||||
* @dataProvider get_default_backup_filename_provider
|
||||
* @covers \backup_plan_dbops::get_default_backup_filename
|
||||
*/
|
||||
public function test_get_default_backup_filename(
|
||||
array $generate,
|
||||
array $params,
|
||||
array $customtemplates,
|
||||
string $expectedfilename
|
||||
): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
foreach ($customtemplates as $config => $value) {
|
||||
set_config($config, $value, 'backup');
|
||||
}
|
||||
|
||||
// All types need a course.
|
||||
$course = $this->getDataGenerator()->create_course($generate['course'] ?? null);
|
||||
|
||||
switch ($params['type']) {
|
||||
case backup::TYPE_1COURSE:
|
||||
$params['id'] = $course->id;
|
||||
$expectedfilename = str_replace('{{courseid}}', $course->id, $expectedfilename);
|
||||
break;
|
||||
case backup::TYPE_1SECTION:
|
||||
$sectioninfo = $this->getDataGenerator()->create_course_section(['course' => $course, 'section' =>
|
||||
$generate['section']['section']]);
|
||||
$actions = new sectionactions($course);
|
||||
$actions->update($sectioninfo, $generate['section'] ?? []);
|
||||
$params['id'] = $sectioninfo->id;
|
||||
$sectioninfo = get_fast_modinfo($course)->get_section_info($sectioninfo->sectionnum);
|
||||
$expectedfilename = str_replace('{{sectionid}}', $sectioninfo->id, $expectedfilename);
|
||||
break;
|
||||
case backup::TYPE_1ACTIVITY:
|
||||
$activity = $this->getDataGenerator()->create_module(
|
||||
'page',
|
||||
array_merge(['course' => $course->id], $generate['activity'])
|
||||
);
|
||||
$params['id'] = $activity->cmid;
|
||||
$expectedfilename = str_replace('{{activitycmid}}', $activity->cmid, $expectedfilename);
|
||||
break;
|
||||
default:
|
||||
throw new coding_exception("Unhandled backup type " . $params['type']);
|
||||
}
|
||||
|
||||
$defaultfilename = backup_plan_dbops::get_default_backup_filename(
|
||||
$params['format'],
|
||||
$params['type'],
|
||||
$params['id'],
|
||||
$params['users'],
|
||||
$params['anonymised'],
|
||||
$params['useidonly'],
|
||||
$params['files'],
|
||||
0
|
||||
);
|
||||
$this->assertEquals($expectedfilename, $defaultfilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests getting syntax errors in template.
|
||||
*
|
||||
* @covers \backup_plan_dbops::get_default_backup_filename_template_syntax_errors
|
||||
*/
|
||||
public function test_get_default_backup_filename_syntax_errors(): void {
|
||||
$this->assertEmpty(backup_plan_dbops::get_default_backup_filename_template_syntax_errors("this is ok {{test}}"));
|
||||
$this->assertNotEmpty(backup_plan_dbops::get_default_backup_filename_template_syntax_errors("this is invalid {{"));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core\context\course;
|
||||
use core\context\module;
|
||||
use core\exception\coding_exception;
|
||||
use core\output\mustache_engine;
|
||||
use core\output\mustache_string_helper;
|
||||
|
||||
/**
|
||||
* Non instantiable helper class providing DB support to the @backup_plan class
|
||||
*
|
||||
@@ -31,6 +37,26 @@
|
||||
* TODO: Finish phpdocs
|
||||
*/
|
||||
abstract class backup_plan_dbops extends backup_dbops {
|
||||
/**
|
||||
* @var string Default template for course backups
|
||||
*/
|
||||
public const DEFAULT_FILENAME_TEMPLATE_COURSE = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}-' .
|
||||
'{{course.shortname}}{{/useidonly}}-{{date}}{{^users}}-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}{{/anonymised}}' .
|
||||
'{{^files}}-nf{{/files}}';
|
||||
|
||||
/**
|
||||
* @var string Default template for section backups
|
||||
*/
|
||||
public const DEFAULT_FILENAME_TEMPLATE_SECTION = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}' .
|
||||
'{{#section.name}}-{{section.name}}{{/section.name}}{{^section.name}}-{{section.section}}{{/section.name}}{{/useidonly}}-' .
|
||||
'{{date}}{{^users}}-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}{{/anonymised}}{{^files}}-nf{{/files}}';
|
||||
|
||||
/**
|
||||
* @var string Default template for activity backups
|
||||
*/
|
||||
public const DEFAULT_FILENAME_TEMPLATE_ACTIVITY = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}' .
|
||||
'-{{activity.modname}}{{id}}{{/useidonly}}-{{date}}{{^users}}-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}' .
|
||||
'{{/anonymised}}{{^files}}-nf{{/files}}';
|
||||
|
||||
/**
|
||||
* Given one course module id, return one array with all the block intances that belong to it
|
||||
@@ -194,81 +220,174 @@ abstract class backup_plan_dbops extends backup_dbops {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default backup filename, based in passed params.
|
||||
*
|
||||
* Default format is (see MDL-22145)
|
||||
* backup word - format - type - name - date - info . mbz
|
||||
* where name is variable (course shortname, section name/id, activity modulename + cmid)
|
||||
* and info can be (nu = no user info, an = anonymized). The last param $useidasname,
|
||||
* defaulting to false, allows to replace the course shortname by the course id (used
|
||||
* by automated backups, to avoid non-ascii chars in OS filesystem)
|
||||
*
|
||||
* @param string $format One of backup::FORMAT_
|
||||
* @param string $type One of backup::TYPE_
|
||||
* @param int $courseid/$sectionid/$cmid
|
||||
* @param bool $users Should be true is users were included in the backup
|
||||
* @param bool $anonymised Should be true is user information was anonymized.
|
||||
* @param bool $useidonly only use the ID in the file name
|
||||
* @return string The filename to use
|
||||
*/
|
||||
public static function get_default_backup_filename($format, $type, $id, $users, $anonymised,
|
||||
$useidonly = false, $files = true) {
|
||||
* Returns the default backup filename, based in passed params.
|
||||
*
|
||||
* Default format is (see MDL-22145)
|
||||
* backup word - format - type - name - date - info . mbz
|
||||
* where name is variable (course shortname, section name/id, activity modulename + cmid)
|
||||
* and info can be (nu = no user info, an = anonymized). The last param $useidonly,
|
||||
* defaulting to false, allows to replace the course shortname by the course id (used
|
||||
* by automated backups, to avoid non-ascii chars in OS filesystem)
|
||||
*
|
||||
* @param string $format One of backup::FORMAT_
|
||||
* @param string $type One of backup::TYPE_
|
||||
* @param int $id course id, section id, or course module id
|
||||
* @param bool $users Should be true is users were included in the backup
|
||||
* @param bool $anonymised Should be true is user information was anonymized
|
||||
* @param bool $useidonly only use the ID in the file name
|
||||
* @param bool $files if files are included
|
||||
* @param int|null $time time to use in any dates, if not given uses current time
|
||||
* @return string The filename to use
|
||||
*/
|
||||
public static function get_default_backup_filename(
|
||||
string $format,
|
||||
string $type,
|
||||
int $id,
|
||||
bool $users,
|
||||
bool $anonymised,
|
||||
bool $useidonly = false,
|
||||
bool $files = true,
|
||||
?int $time = null
|
||||
): string {
|
||||
global $DB;
|
||||
|
||||
// Calculate backup word
|
||||
$backupword = str_replace(' ', '_', core_text::strtolower(get_string('backupfilename')));
|
||||
$backupword = trim(clean_filename($backupword), '_');
|
||||
|
||||
// Not $useidonly, lets fetch the name
|
||||
$shortname = '';
|
||||
if (!$useidonly) {
|
||||
// Calculate proper name element (based on type)
|
||||
switch ($type) {
|
||||
case backup::TYPE_1COURSE:
|
||||
$shortname = $DB->get_field('course', 'shortname', array('id' => $id));
|
||||
$context = context_course::instance($id);
|
||||
$shortname = format_string($shortname, true, array('context' => $context));
|
||||
break;
|
||||
case backup::TYPE_1SECTION:
|
||||
if (!$shortname = $DB->get_field('course_sections', 'name', array('id' => $id))) {
|
||||
$shortname = $DB->get_field('course_sections', 'section', array('id' => $id));
|
||||
}
|
||||
break;
|
||||
case backup::TYPE_1ACTIVITY:
|
||||
$cm = get_coursemodule_from_id(null, $id);
|
||||
$shortname = $cm->modname . $id;
|
||||
break;
|
||||
}
|
||||
$shortname = str_replace(' ', '_', $shortname);
|
||||
$shortname = core_text::strtolower(trim(clean_filename($shortname), '_'));
|
||||
if ($time === null) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
// The name will always contain the ID, but we append the course short name if requested.
|
||||
$name = $id;
|
||||
if (!$useidonly && $shortname != '') {
|
||||
$name .= '-' . $shortname;
|
||||
}
|
||||
|
||||
// Calculate date
|
||||
$backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig'));
|
||||
$date = userdate(time(), $backupdateformat, 99, false);
|
||||
$date = core_text::strtolower(trim(clean_filename($date), '_'));
|
||||
$formatdate = function (int $date) use ($backupdateformat): string {
|
||||
$date = userdate($date, $backupdateformat, 99, false);
|
||||
return core_text::strtolower(trim(clean_filename($date), '_'));
|
||||
};
|
||||
|
||||
// Calculate info
|
||||
$info = '';
|
||||
if (!$users) {
|
||||
$info = '-nu';
|
||||
} else if ($anonymised) {
|
||||
$info = '-an';
|
||||
$mustachecontext = [
|
||||
'format' => $format,
|
||||
'type' => $type,
|
||||
'id' => $id,
|
||||
'users' => $users,
|
||||
'anonymised' => $anonymised,
|
||||
'files' => $files,
|
||||
'useidonly' => $useidonly,
|
||||
'time' => $time,
|
||||
'date' => $formatdate($time),
|
||||
];
|
||||
|
||||
// Add extra context based on the type of backup.
|
||||
// It is important to use array and not stdClass here, otherwise array_walk_recursive will not work.
|
||||
// Additionally get the moodle context of an item, which is used for format_string.
|
||||
$itemcontext = null;
|
||||
switch ($type) {
|
||||
case backup::TYPE_1COURSE:
|
||||
$mustachecontext['course'] = (array) $DB->get_record(
|
||||
'course',
|
||||
['id' => $id],
|
||||
'shortname,fullname,startdate,enddate',
|
||||
MUST_EXIST
|
||||
);
|
||||
$mustachecontext['course']['startdate'] = $formatdate($mustachecontext['course']['startdate']);
|
||||
$mustachecontext['course']['enddate'] = $formatdate($mustachecontext['course']['enddate']);
|
||||
|
||||
$itemcontext = course::instance($id);
|
||||
break;
|
||||
case backup::TYPE_1SECTION:
|
||||
$mustachecontext['section'] = (array) $DB->get_record('course_sections', ['id' => $id], 'name,section', MUST_EXIST);
|
||||
|
||||
// A section is still course context, but needs an extra step to find the course id.
|
||||
$courseid = $DB->get_field('course_sections', 'course', ['id' => $id], MUST_EXIST);
|
||||
$itemcontext = course::instance($courseid);
|
||||
break;
|
||||
case backup::TYPE_1ACTIVITY:
|
||||
$cm = get_coursemodule_from_id(null, $id, 0, false, MUST_EXIST);
|
||||
$mustachecontext['activity'] = [
|
||||
'modname' => $cm->modname,
|
||||
'name' => $cm->name,
|
||||
];
|
||||
|
||||
$itemcontext = module::instance($id);
|
||||
break;
|
||||
default:
|
||||
throw new coding_exception('Unknown backup type ' . $type);
|
||||
}
|
||||
|
||||
// Indicate if backup doesn't contain files.
|
||||
if (!$files) {
|
||||
$info .= '-nf';
|
||||
// Recursively format all the strings and trim any extra whitespace.
|
||||
array_walk_recursive($mustachecontext, function (&$item) use ($itemcontext) {
|
||||
if (is_string($item)) {
|
||||
// Update by reference.
|
||||
$item = trim(format_string($item, true, ['context' => $itemcontext]));
|
||||
}
|
||||
});
|
||||
|
||||
// List of templates in order (if one fails, go to next) for each type.
|
||||
$templates = [
|
||||
backup::TYPE_1COURSE => [
|
||||
get_config('backup', 'backup_default_filename_template_course'),
|
||||
self::DEFAULT_FILENAME_TEMPLATE_COURSE,
|
||||
],
|
||||
backup::TYPE_1SECTION => [
|
||||
get_config('backup', 'backup_default_filename_template_section'),
|
||||
self::DEFAULT_FILENAME_TEMPLATE_SECTION,
|
||||
],
|
||||
backup::TYPE_1ACTIVITY => [
|
||||
get_config('backup', 'backup_default_filename_template_activity'),
|
||||
self::DEFAULT_FILENAME_TEMPLATE_ACTIVITY,
|
||||
],
|
||||
];
|
||||
|
||||
$mustache = self::get_mustache_for_filename_generation();
|
||||
|
||||
// Render the templates until one succeeds.
|
||||
foreach ($templates[$type] as $possibletemplate) {
|
||||
try {
|
||||
$new = @$mustache->render($possibletemplate, $mustachecontext);
|
||||
|
||||
// Clean as filename, remove spaces, and trim to max 251 chars (filename limit, 255 including .mbz extension).
|
||||
$cleaned = substr(str_replace(' ', '_', clean_filename($new)), 0, 251);
|
||||
|
||||
// Success - this template rendered - return it.
|
||||
return $cleaned . '.mbz';
|
||||
} catch (Throwable $e) {
|
||||
// Skip and try the next.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $backupword . '-' . $format . '-' . $type . '-' .
|
||||
$name . '-' . $date . $info . '.mbz';
|
||||
// At a minumum the fallback default filenames should have rendered correctly.
|
||||
// If we reached here it means this did not happen and that something is very wrong.
|
||||
throw new coding_exception("No backup filename templates rendered correctly");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mustache engine instance to be used in filename generation.
|
||||
* @return mustache_engine
|
||||
*/
|
||||
private static function get_mustache_for_filename_generation(): mustache_engine {
|
||||
return new mustache_engine([
|
||||
'helpers' => [
|
||||
'str' => [new mustache_string_helper(), 'str'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given backup filename template is syntatically valid.
|
||||
*
|
||||
* Used mainly for form validation.
|
||||
* @param string $template mustache template
|
||||
* @return array array of string error messages, if empty then there are no errors and it is valid
|
||||
*/
|
||||
public static function get_default_backup_filename_template_syntax_errors(string $template): array {
|
||||
try {
|
||||
// Render without any context, if it is syntatically invalid,
|
||||
// this will throw an exception.
|
||||
// This also outputs warnings if invalid, so we just ignore them using '@'.
|
||||
@self::get_mustache_for_filename_generation()->render($template);
|
||||
|
||||
// No exceptions thrown - is valid!
|
||||
return [];
|
||||
} catch (Throwable $e) {
|
||||
return [$e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -194,6 +194,43 @@ $string['currentstage2'] = 'Schema settings';
|
||||
$string['currentstage4'] = 'Confirmation and review';
|
||||
$string['currentstage8'] = 'Perform backup';
|
||||
$string['currentstage16'] = 'Complete';
|
||||
$string['defaultbackupfilenameactivity'] = 'Activity backup default filename';
|
||||
$string['defaultbackupfilenameactivity_desc'] = 'Additional context for activity backups:
|
||||
<ul>
|
||||
<li><code>activity.name</code> - string - activity name</li>
|
||||
<li><code>activity.modname</code> - integer - module name</li>
|
||||
</ul>
|
||||
';
|
||||
$string['defaultbackupfilenamecourse'] = 'Course backup default filename';
|
||||
$string['defaultbackupfilenamecourse_desc'] = 'Additional context for course backups:
|
||||
<ul>
|
||||
<li><code>course.shortname</code> - string - course shortname</li>
|
||||
<li><code>course.fullname</code> - string - course fullname</li>
|
||||
<li><code>course.startdate</code> - string - course start date in format specified by the <code>backupnameformat</code> lang string</li>
|
||||
<li><code>course.endddate</code> - string - course end date in format specified by the <code>backupnameformat</code> lang string</li>
|
||||
</ul>
|
||||
';
|
||||
$string['defaultbackupfilenamesection'] = 'Section backup default filename';
|
||||
$string['defaultbackupfilenamesection_desc'] = 'Additional context for section backups:
|
||||
<ul>
|
||||
<li><code>section.name</code> - string - section name</li>
|
||||
<li><code>section.section</code> - integer - section number</li>
|
||||
</ul>
|
||||
';
|
||||
$string['defaultbackupfilenamesettings'] = 'Default backup filenames';
|
||||
$string['defaultbackupfilenamesettings_help'] = 'Mustache templates that are evaluated to provide a default filename for a backup.
|
||||
Every backup template has the following context available:
|
||||
<ul>
|
||||
<li><code>format</code> - string - backup format, usually moodle2</li>
|
||||
<li><code>type</code> - string - one of course, section or activity</li>
|
||||
<li><code>id</code> - integer - the item\'s database ID</li>
|
||||
<li><code>useidonly</code> - bool - if the backup was created without the <code>backup_shortname</code> config enabled</li>
|
||||
<li><code>date</code> - string - the date in format specified by the <code>backupnameformat</code> lang string</li>
|
||||
<li><code>users</code> - bool - if user data is included</li>
|
||||
<li><code>anonymised</code> - bool - if user data is anonymised</li>
|
||||
<li><code>files</code> - bool - if files are included</li>
|
||||
</ul>
|
||||
In addition to each of the placeholder values specified, you may also use the {{#str}} helper to add lang strings. The .mbz extension is always added. The filename is truncated to 251 chars.';
|
||||
$string['enableasyncbackup'] = 'Enable asynchronous backups';
|
||||
$string['enableasyncbackup_help'] = 'If enabled, backup and restore operations will be done asynchronously. This provides a better user experience allowing users to do other operations while a backup or restore is in progress. This does not affect imports and exports.';
|
||||
$string['enterasearch'] = 'Enter a search';
|
||||
|
||||
@@ -2704,6 +2704,26 @@ class admin_setting_configtextarea extends admin_setting_configtext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text area for entering backup filename mustache templates, which are validated after submission.
|
||||
*/
|
||||
class admin_setting_configbackupfilenamemustachetemplate extends admin_setting_configtextarea {
|
||||
/**
|
||||
* Validates submitted data.
|
||||
* @param string $data
|
||||
* @return string|true string if error, else true if ok
|
||||
*/
|
||||
public function validate($data) {
|
||||
$errors = backup_plan_dbops::get_default_backup_filename_template_syntax_errors($data);
|
||||
|
||||
if (!empty($errors)) {
|
||||
return get_string('validateerror', 'admin');
|
||||
}
|
||||
|
||||
return parent::validate($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General text area with html editor.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2025103000.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2025103000.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.2dev (Build: 20251030)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user