MDL-84440 backup: Add configurable default backup filename format

This commit is contained in:
Matthew Hilton
2025-11-06 09:21:36 +10:00
parent 543bf96cb7
commit ee4bbaf2cd
6 changed files with 919 additions and 66 deletions
+57
View File
@@ -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()];
}
}
/**
+37
View File
@@ -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';
+20
View File
@@ -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
View File
@@ -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