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
+ namespace core \backup ;
18
+
19
+ use core \context \course ;
20
+ use backup ;
21
+ use core \context \module ;
22
+ use core \exception \coding_exception ;
23
+ use core \output \mustache_engine ;
24
+ use core \output \mustache_string_helper ;
25
+ use core_text ;
26
+ use Throwable ;
27
+
28
+ /**
29
+ * Backup filename helper.
30
+ *
31
+ * @package core
32
+ * @subpackage backup
33
+ * @copyright 2025 Matthew Hilton <[email protected] >
34
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
+ */
36
+ class backup_filename_helper {
37
+ /**
38
+ * @var string Default template for course backups
39
+ */
40
+ public const DEFAULT_FILENAME_TEMPLATE_COURSE = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}-{{course.shortname}}{{/useidonly}}-{{date}} ' .
41
+ '{{^users}}-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}{{/anonymised}}{{^files}}-nf{{/files}} ' ;
42
+
43
+ /**
44
+ * @var string Default template for section backups
45
+ */
46
+ public const DEFAULT_FILENAME_TEMPLATE_SECTION = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}{{#section.name}}-{{section.name}} ' .
47
+ '{{/section.name}}{{^section.name}}-{{section.section}}{{/section.name}}{{/useidonly}}-{{date}}{{^users}} ' .
48
+ '-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}{{/anonymised}}{{^files}}-nf{{/files}} ' ;
49
+
50
+ /**
51
+ * @var string Default template for activity backups
52
+ */
53
+ public const DEFAULT_FILENAME_TEMPLATE_ACTIVITY = '{{#str}}backupfilename{{/str}}-{{format}}-{{type}}-{{id}}{{^useidonly}}-{{activity.modname}}{{id}}{{/useidonly}} ' .
54
+ '-{{date}}{{^users}}-nu{{/users}}{{#anonymised}}{{#users}}-an{{/users}}{{/anonymised}}{{^files}}-nf{{/files}} ' ;
55
+
56
+ /**
57
+ * Get mustache engine instance
58
+ * @return mustache_engine
59
+ */
60
+ private function get_mustache (): mustache_engine {
61
+ return new mustache_engine ([
62
+ 'helpers ' => [
63
+ 'str ' => [new mustache_string_helper (), 'str ' ],
64
+ ],
65
+ ]);
66
+ }
67
+
68
+ /**
69
+ * Returns the default backup filename, based in passed params.
70
+ *
71
+ * @param string $format One of backup::FORMAT_
72
+ * @param string $type One of backup::TYPE_
73
+ * @param int $id course id, section id, or course module id
74
+ * @param bool $users Should be true is users were included in the backup
75
+ * @param bool $anonymised Should be true is user information was anonymized
76
+ * @param bool $useidonly only use the ID in the file name
77
+ * @param bool $files if files are included
78
+ * @param int|null $time time to use in any dates, if not given uses current time
79
+ * @return string The filename to use
80
+ */
81
+ public function get_default_backup_filename (string $ format , string $ type , int $ id , bool $ users , bool $ anonymised ,
82
+ bool $ useidonly = false , bool $ files = true , ?int $ time = null ): string {
83
+ global $ DB ;
84
+
85
+ if ($ time === null ) {
86
+ $ time = time ();
87
+ }
88
+
89
+ $ backupdateformat = str_replace (' ' , '_ ' , get_string ('backupnameformat ' , 'langconfig ' ));
90
+ $ formatdate = function (int $ date ) use ($ backupdateformat ): string {
91
+ $ date = userdate ($ date , $ backupdateformat , 99 , false );
92
+ return core_text::strtolower (trim (clean_filename ($ date ), '_ ' ));
93
+ };
94
+
95
+ $ mustachecontext = [
96
+ 'format ' => $ format ,
97
+ 'type ' => $ type ,
98
+ 'id ' => $ id ,
99
+ 'users ' => $ users ,
100
+ 'anonymised ' => $ anonymised ,
101
+ 'files ' => $ files ,
102
+ 'useidonly ' => $ useidonly ,
103
+ 'time ' => $ time ,
104
+ 'date ' => $ formatdate ($ time ),
105
+ ];
106
+
107
+ // Add extra context based on the type of backup.
108
+ // It is important to use array and not stdClass here, otherwise array_walk_recursive will not work.
109
+ switch ($ type ) {
110
+ case backup::TYPE_1COURSE :
111
+ $ mustachecontext ['course ' ] = (array ) $ DB ->get_record ('course ' , ['id ' => $ id ],
112
+ 'shortname,fullname,startdate,enddate ' , MUST_EXIST );
113
+ $ mustachecontext ['course ' ]['startdate ' ] = $ formatdate ($ mustachecontext ['course ' ]['startdate ' ]);
114
+ $ mustachecontext ['course ' ]['enddate ' ] = $ formatdate ($ mustachecontext ['course ' ]['enddate ' ]);
115
+ break ;
116
+ case backup::TYPE_1SECTION :
117
+ $ mustachecontext ['section ' ] = (array ) $ DB ->get_record ('course_sections ' , ['id ' => $ id ], 'name,section ' , MUST_EXIST );
118
+ break ;
119
+ case backup::TYPE_1ACTIVITY :
120
+ $ cm = get_coursemodule_from_id (null , $ id , 0 , false , MUST_EXIST );
121
+ $ mustachecontext ['activity ' ] = [
122
+ 'modname ' => $ cm ->modname ,
123
+ 'name ' => $ cm ->name ,
124
+ ];
125
+ break ;
126
+ default :
127
+ throw new coding_exception ('Unknown backup type - cannot get template context ' );
128
+ }
129
+
130
+ // Get the relevant context for the item, which is used for format_string.
131
+ $ itemcontext = null ;
132
+ switch ($ type ) {
133
+ case backup::TYPE_1COURSE :
134
+ $ itemcontext = course::instance ($ id );
135
+ break ;
136
+ case backup::TYPE_1SECTION :
137
+ // A section is still course context, but needs an extra step to find the course id.
138
+ $ courseid = $ DB ->get_field ('course_sections ' , 'course ' , ['id ' => $ id ], MUST_EXIST );
139
+ $ itemcontext = course::instance ($ courseid );
140
+ break ;
141
+ case backup::TYPE_1ACTIVITY :
142
+ $ itemcontext = module::instance ($ id );
143
+ break ;
144
+ default :
145
+ throw new coding_exception ('Unknown backup type - cannot get instance context ' );
146
+ }
147
+
148
+ // Recursively format all the strings and trim any extra whitespace.
149
+ array_walk_recursive ($ mustachecontext , function (&$ item ) use ($ itemcontext ) {
150
+ if (is_string ($ item )) {
151
+ // Update by reference.
152
+ $ item = trim (format_string ($ item , true , ['context ' => $ itemcontext ]));
153
+ }
154
+ });
155
+
156
+ // List of templates in order (if one fails, go to next) for each type.
157
+ $ templates = [
158
+ backup::TYPE_1COURSE => [
159
+ get_config ('backup ' , 'backup_default_filename_template_course ' ),
160
+ self ::DEFAULT_FILENAME_TEMPLATE_COURSE ,
161
+ ],
162
+ backup::TYPE_1SECTION => [
163
+ get_config ('backup ' , 'backup_default_filename_template_section ' ),
164
+ self ::DEFAULT_FILENAME_TEMPLATE_SECTION ,
165
+ ],
166
+ backup::TYPE_1ACTIVITY => [
167
+ get_config ('backup ' , 'backup_default_filename_template_activity ' ),
168
+ self ::DEFAULT_FILENAME_TEMPLATE_ACTIVITY
169
+ ],
170
+ ];
171
+
172
+ $ mustache = $ this ->get_mustache ();
173
+
174
+ // Render the templates until one succeeds.
175
+ foreach ($ templates [$ type ] as $ possibletemplate ) {
176
+ try {
177
+ $ new = @$ mustache ->render ($ possibletemplate , $ mustachecontext );
178
+
179
+ // Clean as filename, remove spaces, and trim to max 251 chars (filename limit, 255 including .mbz extension).
180
+ $ cleaned = substr (str_replace (' ' , '_ ' , clean_filename ($ new )), 0 , 251 );
181
+
182
+ // Success - this template rendered - return it.
183
+ return $ cleaned . '.mbz ' ;
184
+ } catch (Throwable $ e ) {
185
+ // Skip and try the next.
186
+ continue ;
187
+ }
188
+ }
189
+
190
+ // The only way to reach here would be if the constant default templates
191
+ // became broken, which indicates a coding error.
192
+ throw new coding_exception ("No backup filename templates rendered correctly " );
193
+ }
194
+
195
+ /**
196
+ * Validates the given template renders correctly.
197
+ *
198
+ * Used mainly for form validation.
199
+ * @param string $template mustache template
200
+ * @return array array of errors, if empty then there are no errors
201
+ */
202
+ public function validate (string $ template ): array {
203
+ try {
204
+ // Rnder without any context, if it is syntatically invalid,
205
+ // this will throw an exception.
206
+ // this also outputs warnings if invalid, so we just ignore them using '@'.
207
+ @$ this ->get_mustache ()->render ($ template );
208
+
209
+ // All good - is valid!
210
+ return [];
211
+ } catch (Throwable $ e ) {
212
+ return [$ e ->getMessage ()];
213
+ }
214
+ }
215
+ }
0 commit comments