17
17
namespace qtype_questionpy \local \files ;
18
18
19
19
use coding_exception ;
20
+ use context ;
20
21
use context_user ;
21
22
use DateTimeImmutable ;
22
23
use file_exception ;
23
24
use moodle_exception ;
24
25
use moodle_url ;
26
+ use qtype_questionpy \local \form \elements \file_upload_element ;
27
+ use qtype_questionpy \local \form \elements \file_upload_options ;
25
28
use qtype_questionpy_question ;
26
29
use stored_file ;
27
30
use stored_file_creation_exception ;
@@ -44,6 +47,7 @@ class options_file_service implements handles_qpy_url_type {
44
47
/** @var string */
45
48
public const FILEAREA_UPLOADS = 'options ' ;
46
49
50
+
47
51
/**
48
52
* Saves all the files in the given draft item to the permanent file area for the given question.
49
53
*
@@ -168,9 +172,85 @@ public function get_qpy_files_metadata_from_draftitem(int $userid, int $draftite
168
172
size: $ file ->get_filesize (),
169
173
);
170
174
}
175
+
171
176
return $ metadata ;
172
177
}
173
178
179
+ /**
180
+ * Checks that `maxfiles`, `maxbytestotal` and `maxbytesperfile` (or whatever the LMS overrides it with) are fulfilled.
181
+ *
182
+ * Breaking any of the three by accident should be impossible through UI design or checked by earlier parts of the Moodle code.
183
+ * It might be possible if intentional, so we also check it here, but just throw unhelpful {@see coding_exception}s.
184
+ *
185
+ * `minfiles` is _not_ checked. Since that option isn't really security-relevant, it's left to the package/SDK.
186
+ *
187
+ * @param file_upload_options|file_upload_element $options
188
+ * @param int $contextid
189
+ * @param int|null $questionid Question being edited, or `null` if a new question is being created.
190
+ * @param int $draftitemid
191
+ * @param file_metadata[] $draftfilemetas
192
+ * @return void An exception is thrown if the restrictions are not fulfilled.
193
+ * @throws coding_exception
194
+ */
195
+ public function check_upload_restrictions (
196
+ file_upload_options |file_upload_element $ options ,
197
+ int $ contextid , ?int $ questionid , int $ draftitemid , array $ draftfilemetas
198
+ ): void {
199
+ global $ CFG ;
200
+ $ fs = get_file_storage ();
201
+
202
+ if ($ options ->maxfiles !== null && count ($ draftfilemetas ) > $ options ->maxfiles ) {
203
+ throw new coding_exception ("Draft area $ draftitemid exceeds limit of $ options ->maxfiles files. " );
204
+ }
205
+
206
+ if (file_is_draft_area_limit_reached ($ draftitemid , $ options ->maxbytestotal ?? FILE_AREA_MAX_BYTES_UNLIMITED )) {
207
+ throw new coding_exception ("Draft area $ draftitemid exceeds limit of $ options ->maxbytestotal bytes. " );
208
+ }
209
+
210
+ $ coursemaxbytes = 0 ;
211
+ if (!empty ($ PAGE ->course ->maxbytes )) {
212
+ $ coursemaxbytes = $ PAGE ->course ->maxbytes ;
213
+ }
214
+
215
+ $ effectivemaxbytes = get_user_max_upload_file_size (
216
+ context::instance_by_id ($ contextid ),
217
+ $ CFG ->maxbytes ,
218
+ $ coursemaxbytes ,
219
+ $ options ->maxbytesperfile ?? FILE_AREA_MAX_BYTES_UNLIMITED
220
+ );
221
+
222
+ if ($ effectivemaxbytes === USER_CAN_IGNORE_FILE_SIZE_LIMITS ) {
223
+ // No need to check then.
224
+ return ;
225
+ }
226
+
227
+ $ existingfiles = $ questionid ? $ fs ->get_area_files (
228
+ $ contextid ,
229
+ 'qtype_questionpy ' ,
230
+ self ::FILEAREA_UPLOADS ,
231
+ $ questionid ,
232
+ includedirs: false
233
+ ) : [];
234
+ $ existingfilerefs = array_map (fn ($ file ) => $ file ->get_filename (), $ existingfiles );
235
+
236
+ foreach ($ draftfilemetas as $ draftfilemeta ) {
237
+ if (in_array ($ draftfilemeta ->fileref , $ existingfilerefs )) {
238
+ // The file (a file with the same content and metadata) had already been successfully uploaded.
239
+ // The current user might have changed its path or filename or even moved it to a different upload element, but
240
+ // since the original uploader must've been allowed to upload it, we don't check whether the current user would be
241
+ // allowed to upload it again.
242
+ // TODO: Catch when a file is copied/moved by the package to a different element with a lower file size limit.
243
+ // That would probably need to happen at import (when loading the form data from the package), not here.
244
+ continue ;
245
+ }
246
+
247
+ if ($ draftfilemeta ->size > $ effectivemaxbytes ) {
248
+ throw new coding_exception ("File ' {$ draftfilemeta ->path }{$ draftfilemeta ->filename }' exceeds limit of "
249
+ . " $ effectivemaxbytes bytes. " );
250
+ }
251
+ }
252
+ }
253
+
174
254
/**
175
255
* Retrieves a saved file from the file area belonging to the given question.
176
256
*
0 commit comments