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 * Library of interface functions and constants.
19 *
20 * @package     mod_h5pactivity
21 * @copyright   2020 Ferran Recio <ferran@moodle.com>
22 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27use mod_h5pactivity\local\manager;
28use mod_h5pactivity\local\grader;
29
30/**
31 * Checks if H5P activity supports a specific feature.
32 *
33 * @uses FEATURE_GROUPS
34 * @uses FEATURE_GROUPINGS
35 * @uses FEATURE_MOD_INTRO
36 * @uses FEATURE_SHOW_DESCRIPTION
37 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
38 * @uses FEATURE_COMPLETION_HAS_RULES
39 * @uses FEATURE_MODEDIT_DEFAULT_COMPLETION
40 * @uses FEATURE_GRADE_HAS_GRADE
41 * @uses FEATURE_GRADE_OUTCOMES
42 * @uses FEATURE_BACKUP_MOODLE2
43 * @param string $feature FEATURE_xx constant for requested feature
44 * @return mixed True if module supports feature, false if not, null if doesn't know
45 */
46function h5pactivity_supports(string $feature): ?bool {
47    switch($feature) {
48        case FEATURE_GROUPS:
49            return true;
50        case FEATURE_GROUPINGS:
51            return true;
52        case FEATURE_MOD_INTRO:
53            return true;
54        case FEATURE_SHOW_DESCRIPTION:
55            return true;
56        case FEATURE_COMPLETION_TRACKS_VIEWS:
57            return true;
58        case FEATURE_MODEDIT_DEFAULT_COMPLETION:
59            return true;
60        case FEATURE_GRADE_HAS_GRADE:
61            return true;
62        case FEATURE_GRADE_OUTCOMES:
63            return true;
64        case FEATURE_BACKUP_MOODLE2:
65            return true;
66        default:
67            return null;
68    }
69}
70
71/**
72 * Saves a new instance of the mod_h5pactivity into the database.
73 *
74 * Given an object containing all the necessary data, (defined by the form
75 * in mod_form.php) this function will create a new instance and return the id
76 * number of the instance.
77 *
78 * @param stdClass $data An object from the form.
79 * @param mod_h5pactivity_mod_form $mform The form.
80 * @return int The id of the newly inserted record.
81 */
82function h5pactivity_add_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): int {
83    global $DB;
84
85    $data->timecreated = time();
86    $data->timemodified = $data->timecreated;
87    $cmid = $data->coursemodule;
88
89    $data->id = $DB->insert_record('h5pactivity', $data);
90
91    // We need to use context now, so we need to make sure all needed info is already in db.
92    $DB->set_field('course_modules', 'instance', $data->id, ['id' => $cmid]);
93    h5pactivity_set_mainfile($data);
94
95    // Extra fields required in grade related functions.
96    $data->cmid = $data->coursemodule;
97    h5pactivity_grade_item_update($data);
98    return $data->id;
99}
100
101/**
102 * Updates an instance of the mod_h5pactivity in the database.
103 *
104 * Given an object containing all the necessary data (defined in mod_form.php),
105 * this function will update an existing instance with new data.
106 *
107 * @param stdClass $data An object from the form in mod_form.php.
108 * @param mod_h5pactivity_mod_form $mform The form.
109 * @return bool True if successful, false otherwise.
110 */
111function h5pactivity_update_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): bool {
112    global $DB;
113
114    $data->timemodified = time();
115    $data->id = $data->instance;
116
117    h5pactivity_set_mainfile($data);
118
119    // Update gradings if grading method or tracking are modified.
120    $data->cmid = $data->coursemodule;
121    $moduleinstance = $DB->get_record('h5pactivity', ['id' => $data->id]);
122    if (($moduleinstance->grademethod != $data->grademethod)
123            || $data->enabletracking != $moduleinstance->enabletracking) {
124        h5pactivity_update_grades($data);
125    } else {
126        h5pactivity_grade_item_update($data);
127    }
128
129    return $DB->update_record('h5pactivity', $data);
130}
131
132/**
133 * Removes an instance of the mod_h5pactivity from the database.
134 *
135 * @param int $id Id of the module instance.
136 * @return bool True if successful, false on failure.
137 */
138function h5pactivity_delete_instance(int $id): bool {
139    global $DB;
140
141    $activity = $DB->get_record('h5pactivity', ['id' => $id]);
142    if (!$activity) {
143        return false;
144    }
145
146    $DB->delete_records('h5pactivity', ['id' => $id]);
147
148    h5pactivity_grade_item_delete($activity);
149
150    return true;
151}
152
153/**
154 * Checks if scale is being used by any instance of mod_h5pactivity.
155 *
156 * This is used to find out if scale used anywhere.
157 *
158 * @param int $scaleid ID of the scale.
159 * @return bool True if the scale is used by any mod_h5pactivity instance.
160 */
161function h5pactivity_scale_used_anywhere(int $scaleid): bool {
162    global $DB;
163
164    if ($scaleid and $DB->record_exists('h5pactivity', ['grade' => -$scaleid])) {
165        return true;
166    } else {
167        return false;
168    }
169}
170
171/**
172 * Creates or updates grade item for the given mod_h5pactivity instance.
173 *
174 * Needed by {@link grade_update_mod_grades()}.
175 *
176 * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property.
177 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
178 * @return int int 0 if ok, error code otherwise
179 */
180function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades = null): int {
181    $idnumber = $moduleinstance->idnumber ?? '';
182    $grader = new grader($moduleinstance, $idnumber);
183    return $grader->grade_item_update($grades);
184}
185
186/**
187 * Delete grade item for given mod_h5pactivity instance.
188 *
189 * @param stdClass $moduleinstance Instance object.
190 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
191 */
192function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int {
193    $idnumber = $moduleinstance->idnumber ?? '';
194    $grader = new grader($moduleinstance, $idnumber);
195    return $grader->grade_item_delete();
196}
197
198/**
199 * Update mod_h5pactivity grades in the gradebook.
200 *
201 * Needed by {@link grade_update_mod_grades()}.
202 *
203 * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property.
204 * @param int $userid Update grade of specific user only, 0 means all participants.
205 */
206function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): void {
207    $idnumber = $moduleinstance->idnumber ?? '';
208    $grader = new grader($moduleinstance, $idnumber);
209    $grader->update_grades($userid);
210}
211
212/**
213 * Rescale all grades for this activity and push the new grades to the gradebook.
214 *
215 * @param stdClass $course Course db record
216 * @param stdClass $cm Course module db record
217 * @param float $oldmin
218 * @param float $oldmax
219 * @param float $newmin
220 * @param float $newmax
221 * @return bool true if reescale is successful
222 */
223function h5pactivity_rescale_activity_grades(stdClass $course, stdClass $cm, float $oldmin,
224        float $oldmax, float $newmin, float $newmax): bool {
225
226    $manager = manager::create_from_coursemodule($cm);
227    $grader = $manager->get_grader();
228    $grader->update_grades();
229    return true;
230}
231
232/**
233 * Implementation of the function for printing the form elements that control
234 * whether the course reset functionality affects the H5P activity.
235 *
236 * @param object $mform form passed by reference
237 */
238function h5pactivity_reset_course_form_definition(&$mform): void {
239    $mform->addElement('header', 'h5pactivityheader', get_string('modulenameplural', 'mod_h5pactivity'));
240    $mform->addElement('advcheckbox', 'reset_h5pactivity', get_string('deleteallattempts', 'mod_h5pactivity'));
241}
242
243/**
244 * Course reset form defaults.
245 *
246 * @param stdClass $course the course object
247 * @return array
248 */
249function h5pactivity_reset_course_form_defaults(stdClass $course): array {
250    return ['reset_h5pactivity' => 1];
251}
252
253
254/**
255 * This function is used by the reset_course_userdata function in moodlelib.
256 *
257 * This function will remove all H5P attempts in the database
258 * and clean up any related data.
259 *
260 * @param stdClass $data the data submitted from the reset course.
261 * @return array of reseting status
262 */
263function h5pactivity_reset_userdata(stdClass $data): array {
264    global $CFG, $DB;
265    $componentstr = get_string('modulenameplural', 'mod_h5pactivity');
266    $status = [];
267    if (!empty($data->reset_h5pactivity)) {
268        $params = ['courseid' => $data->courseid];
269        $sql = "SELECT a.id FROM {h5pactivity} a WHERE a.course=:courseid";
270        if ($activities = $DB->get_records_sql($sql, $params)) {
271            foreach ($activities as $activity) {
272                $cm = get_coursemodule_from_instance('h5pactivity',
273                                                     $activity->id,
274                                                     $data->courseid,
275                                                     false,
276                                                     MUST_EXIST);
277                mod_h5pactivity\local\attempt::delete_all_attempts ($cm);
278            }
279        }
280        // Remove all grades from gradebook.
281        if (empty($data->reset_gradebook_grades)) {
282            h5pactivity_reset_gradebook($data->courseid, 'reset');
283        }
284        $status[] = [
285            'component' => $componentstr,
286            'item' => get_string('deleteallattempts', 'mod_h5pactivity'),
287            'error' => false,
288        ];
289    }
290    return $status;
291}
292
293/**
294 * Removes all grades from gradebook
295 *
296 * @param int $courseid Coude ID
297 * @param string $type optional type (default '')
298 */
299function h5pactivity_reset_gradebook(int $courseid, string $type=''): void {
300    global $DB;
301
302    $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid
303              FROM {h5pactivity} a, {course_modules} cm, {modules} m
304             WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=a.id AND a.course=?";
305
306    if ($activities = $DB->get_records_sql($sql, [$courseid])) {
307        foreach ($activities as $activity) {
308            h5pactivity_grade_item_update($activity, 'reset');
309        }
310    }
311}
312
313/**
314 * Return a list of page types
315 *
316 * @param string $pagetype current page type
317 * @param stdClass|null $parentcontext Block's parent context
318 * @param stdClass $currentcontext Current context of block
319 * @return array array of page types and it's names
320 */
321function h5pactivity_page_type_list(string $pagetype, ?stdClass $parentcontext, stdClass $currentcontext): array {
322    $modulepagetype = [
323        'mod-h5pactivity-*' => get_string('page-mod-h5pactivity-x', 'h5pactivity'),
324    ];
325    return $modulepagetype;
326}
327
328/**
329 * Check if the module has any update that affects the current user since a given time.
330 *
331 * @param  cm_info $cm course module data
332 * @param  int $from the time to check updates from
333 * @param  array $filter  if we need to check only specific updates
334 * @return stdClass an object with the different type of areas indicating if they were updated or not
335 */
336function h5pactivity_check_updates_since(cm_info $cm, int $from, array $filter = []): stdClass {
337    global $DB, $USER;
338
339    $updates = course_check_module_updates_since($cm, $from, ['package'], $filter);
340
341    $updates->tracks = (object) ['updated' => false];
342    $select = 'h5pactivityid = ? AND userid = ? AND timemodified > ?';
343    $params = [$cm->instance, $USER->id, $from];
344    $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id');
345    if (!empty($tracks)) {
346        $updates->tracks->updated = true;
347        $updates->tracks->itemids = array_keys($tracks);
348    }
349
350    // Now, teachers should see other students updates.
351    if (has_capability('mod/h5pactivity:reviewattempts', $cm->context)) {
352        $select = 'h5pactivityid = ? AND timemodified > ?';
353        $params = [$cm->instance, $from];
354
355        if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
356            $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
357            if (empty($groupusers)) {
358                return $updates;
359            }
360            list($insql, $inparams) = $DB->get_in_or_equal($groupusers);
361            $select .= ' AND userid ' . $insql;
362            $params = array_merge($params, $inparams);
363        }
364
365        $updates->usertracks = (object) ['updated' => false];
366        $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id');
367        if (!empty($tracks)) {
368            $updates->usertracks->updated = true;
369            $updates->usertracks->itemids = array_keys($tracks);
370        }
371    }
372    return $updates;
373}
374
375/**
376 * Returns the lists of all browsable file areas within the given module context.
377 *
378 * The file area 'intro' for the activity introduction field is added automatically
379 * by {@link file_browser::get_file_info_context_module()}.
380 *
381 * @param stdClass $course course object
382 * @param stdClass $cm course module object
383 * @param stdClass $context context object
384 * @return string[] array of pair file area => human file area name
385 */
386function h5pactivity_get_file_areas(stdClass $course, stdClass $cm, stdClass $context): array {
387    $areas = [];
388    $areas['package'] = get_string('areapackage', 'mod_h5pactivity');
389    return $areas;
390}
391
392/**
393 * File browsing support for data module.
394 *
395 * @param file_browser $browser
396 * @param array $areas
397 * @param stdClass $course
398 * @param stdClass $cm
399 * @param context $context
400 * @param string $filearea
401 * @param int|null $itemid
402 * @param string|null $filepath
403 * @param string|null $filename
404 * @return file_info_stored|null file_info_stored instance or null if not found
405 */
406function h5pactivity_get_file_info(file_browser $browser, array $areas, stdClass $course,
407            stdClass $cm, context $context, string $filearea, ?int $itemid = null,
408            ?string $filepath = null, ?string $filename = null): ?file_info_stored {
409    global $CFG;
410
411    if (!has_capability('moodle/course:managefiles', $context)) {
412        return null;
413    }
414
415    $fs = get_file_storage();
416
417    if ($filearea === 'package') {
418        $filepath = is_null($filepath) ? '/' : $filepath;
419        $filename = is_null($filename) ? '.' : $filename;
420
421        $urlbase = $CFG->wwwroot.'/pluginfile.php';
422        if (!$storedfile = $fs->get_file($context->id, 'mod_h5pactivity', 'package', 0, $filepath, $filename)) {
423            if ($filepath === '/' and $filename === '.') {
424                $storedfile = new virtual_root_file($context->id, 'mod_h5pactivity', 'package', 0);
425            } else {
426                // Not found.
427                return null;
428            }
429        }
430        return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false);
431    }
432    return null;
433}
434
435/**
436 * Serves the files from the mod_h5pactivity file areas.
437 *
438 * @param mixed $course course or id of the course
439 * @param mixed $cm course module or id of the course module
440 * @param context $context
441 * @param string $filearea
442 * @param array $args
443 * @param bool $forcedownload
444 * @param array $options additional options affecting the file serving
445 * @return bool false if file not found, does not return if found - just send the file
446 */
447function h5pactivity_pluginfile($course, $cm, context $context,
448            string $filearea, array $args, bool $forcedownload, array $options = []): bool {
449    if ($context->contextlevel != CONTEXT_MODULE) {
450        return false;
451    }
452
453    require_login($course, true, $cm);
454
455    $fullpath = '';
456
457    if ($filearea === 'package') {
458        $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
459        $relativepath = implode('/', $args);
460        $fullpath = "/$context->id/mod_h5pactivity/package/0/$relativepath";
461    }
462    if (empty($fullpath)) {
463        return false;
464    }
465    $fs = get_file_storage();
466    $file = $fs->get_file_by_hash(sha1($fullpath));
467    if (empty($file)) {
468        return false;
469    }
470    send_stored_file($file, $lifetime, 0, false, $options);
471}
472
473/**
474 * Saves draft files as the activity package.
475 *
476 * @param stdClass $data an object from the form
477 */
478function h5pactivity_set_mainfile(stdClass $data): void {
479    $fs = get_file_storage();
480    $cmid = $data->coursemodule;
481    $context = context_module::instance($cmid);
482
483    if (!empty($data->packagefile)) {
484        $fs = get_file_storage();
485        $fs->delete_area_files($context->id, 'mod_h5pactivity', 'package');
486        file_save_draft_area_files($data->packagefile, $context->id, 'mod_h5pactivity', 'package',
487            0, ['subdirs' => 0, 'maxfiles' => 1]);
488    }
489}
490
491/**
492 * Register the ability to handle drag and drop file uploads
493 * @return array containing details of the files / types the mod can handle
494 */
495function h5pactivity_dndupload_register(): array {
496    return [
497        'files' => [
498            [
499                'extension' => 'h5p',
500                'message' => get_string('dnduploadh5pactivity', 'h5pactivity')
501            ]
502        ]
503    ];
504}
505
506/**
507 * Handle a file that has been uploaded
508 * @param object $uploadinfo details of the file / content that has been uploaded
509 * @return int instance id of the newly created mod
510 */
511function h5pactivity_dndupload_handle($uploadinfo): int {
512    global $CFG;
513
514    $context = context_module::instance($uploadinfo->coursemodule);
515    file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_h5pactivity', 'package', 0);
516    $fs = get_file_storage();
517    $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false);
518    $file = reset($files);
519
520    // Create a default h5pactivity object to pass to h5pactivity_add_instance()!
521    $h5p = get_config('h5pactivity');
522    $h5p->intro = '';
523    $h5p->introformat = FORMAT_HTML;
524    $h5p->course = $uploadinfo->course->id;
525    $h5p->coursemodule = $uploadinfo->coursemodule;
526    $h5p->grade = $CFG->gradepointdefault;
527
528    // Add some special handling for the H5P options checkboxes.
529    $factory = new \core_h5p\factory();
530    $core = $factory->get_core();
531    if (isset($uploadinfo->displayopt)) {
532        $config = (object) $uploadinfo->displayopt;
533    } else {
534        $config = \core_h5p\helper::decode_display_options($core);
535    }
536    $h5p->displayoptions = \core_h5p\helper::get_display_options($core, $config);
537
538    $h5p->cmidnumber = '';
539    $h5p->name = $uploadinfo->displayname;
540    $h5p->reference = $file->get_filename();
541
542    return h5pactivity_add_instance($h5p, null);
543}
544