MDL-75316 core: Add activity packager to MoodleNet API

This makes it possible to package a single activity as a Moodle backup
file, excluding user data so it is ready to be shared as a MoodleNet
resource.

Co-authored-by: Michael Hawkins <[email protected]>

Originally implemented as MDL-75932
This commit is contained in:
raortegar
2023-04-18 17:03:45 +08:00
committed by Michael Hawkins
parent d6c4c0fbbd
commit 9bc769a610
2 changed files with 325 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
<?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\moodlenet;
use backup;
use backup_controller;
use backup_root_task;
use cm_info;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Packager to prepare appropriate backup of an activity to share to MoodleNet.
*
* @package core
* @copyright 2023 Raquel Ortega <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_packager {
/** @var backup_controller $controller */
protected $controller;
/**
* Constructor.
*
* @param cm_info $cminfo context module information about the resource being packaged.
* @param int $userid The ID of the user performing the packaging.
*/
public function __construct(
protected cm_info $cminfo,
protected int $userid
) {
// Check backup/restore support.
if (!plugin_supports('mod', $cminfo->modname , FEATURE_BACKUP_MOODLE2)) {
throw new \coding_exception("Cannot backup module $cminfo->modname. This module doesn't support the backup feature.");
}
$this->cminfo = $cminfo;
$this->controller = new backup_controller (
backup::TYPE_1ACTIVITY,
$cminfo->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$userid
);
}
/**
* Prepare the backup file using appropriate setting overrides and return relevant information.
*
* @return array Array containing packaged file and stored_file object describing it.
* Array in the format [storedfile => stored_file object, filecontents => raw file].
*/
public function get_package(): array {
$alltasksettings = $this->get_all_task_settings();
// Override relevant settings to remove user data when packaging to share to MoodleNet.
$this->override_task_setting($alltasksettings, 'setting_root_users', 0);
$this->override_task_setting($alltasksettings, 'setting_root_role_assignments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_blocks', 0);
$this->override_task_setting($alltasksettings, 'setting_root_comments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_badges', 0);
$this->override_task_setting($alltasksettings, 'setting_root_userscompletion', 0);
$this->override_task_setting($alltasksettings, 'setting_root_logs', 0);
$this->override_task_setting($alltasksettings, 'setting_root_grade_histories', 0);
$this->override_task_setting($alltasksettings, 'setting_root_groups', 0);
return $this->package();
}
/**
* Get all backup settings available for override.
*
* @return array the associative array of taskclass => settings instances.
*/
protected function get_all_task_settings(): array {
$tasksettings = [];
foreach ($this->controller->get_plan()->get_tasks() as $task) {
$taskclass = get_class($task);
$tasksettings[$taskclass] = $task->get_settings();
}
return $tasksettings;
}
/**
* Override a backup task setting with a given value.
*
* @param array $alltasksettings All task settings.
* @param string $settingname The name of the setting to be overridden (task class name format).
* @param int $settingvalue Value to be given to the setting.
* @return void
*/
protected function override_task_setting(array $alltasksettings, string $settingname, int $settingvalue): void {
if (empty($rootsettings = $alltasksettings[backup_root_task::class])) {
return;
}
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $settingname && $settingvalue != $setting->get_value()) {
$setting->set_value($settingvalue);
return;
}
}
}
/**
* Package the activity identified by CMID.
*
* @return array Array containing packaged file and stored_file object describing it.
* Array in the format [storedfile => stored_file object, filecontents => raw file].
* @throws \moodle_exception.
*/
protected function package(): array {
// Execute the backup and fetch the result.
$this->controller->execute_plan();
$result = $this->controller->get_results();
// Controller no longer required.
$this->controller->destroy();
if (!isset($result['backup_destination'])) {
throw new \moodle_exception('Failed to package activity.');
}
$backupfile = $result['backup_destination'];
if (!$backupfile->get_contenthash()) {
throw new \moodle_exception('Failed to package activity (invalid file).');
}
// Create the location we want to copy this file to.
$time = time();
$fr = [
'contextid' => \context_course::instance($this->cminfo->course)->id,
'component' => 'core',
'filearea' => 'moodlenet_resource',
'filename' => $this->cminfo->modname . '_backup.mbz',
// Add timestamp to itemid to make it unique, to avoid any collisions.
'itemid' => $this->cminfo->id . $time,
'timemodified' => $time,
];
// Create the local file based on the backup.
$fs = get_file_storage();
$packagedfiledata = [
'storedfile' => $fs->create_file_from_storedfile($fr, $backupfile),
];
// Delete the backup now it has been created in the file area.
$backupfile->delete();
if (!$packagedfiledata['storedfile']) {
throw new \moodle_exception("Failed to copy backup file to moodlenet_activity area.");
}
// Ensure we can handle files at the upper end of the limit supported by MoodleNet.
raise_memory_limit('1070000000');
// Get the actual file content.
$packagedfiledata['filecontents'] = $packagedfiledata['storedfile']->get_content();
return $packagedfiledata;
}
}
@@ -0,0 +1,143 @@
<?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\moodlenet;
/**
* Unit tests for {@see activity_packager}.
*
* @coversDefaultClass \core\moodlenet\activity_packager
* @package core
* @copyright 2023 Michael Hawkins <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_packager_test extends \advanced_testcase {
/**
* Test fetching and overriding a backup task setting.
*
* @covers ::override_task_setting
* @covers ::get_all_task_settings
* @return void
*/
public function test_override_task_setting() {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$assigndata = [
'course' => $course->id,
'name' => 'Extremely interesting assignment',
'intro' => 'A great assignment to share',
];
$assign = $generator->create_module('assign', $assigndata);
$cminfo = get_fast_modinfo($course->id)->get_cm($assign->cmid);
$packager = new activity_packager($cminfo, $USER->id);
// Fetch all backup task settings.
$rc = new \ReflectionClass(activity_packager::class);
$rcmgetall = $rc->getMethod('get_all_task_settings');
$rcmgetall->setAccessible(true);
$tasksettings = $rcmgetall->invoke($packager);
// Fetch the default settings and grab an example value (setting_root_users).
$rootsettings = $tasksettings[\backup_root_task::class];
$testsettingname = 'setting_root_users';
$oldvalue = 99;
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $testsettingname) {
$oldvalue = $setting->get_value();
break;
}
}
// Check we found the setting value (either 0 or 1 are valid).
$this->assertNotEquals(99, $oldvalue);
$this->assertLessThanOrEqual(1, $oldvalue);
// Override the setting_root_users value, then re-fetch the settings to check the change is reflected.
$overridevalue = ($oldvalue == 1) ? 0 : 1;
$rcmoverridesetting = $rc->getMethod('override_task_setting');
$rcmoverridesetting->setAccessible(true);
$rcmoverridesetting->invoke($packager, $tasksettings, $testsettingname, $overridevalue);
$tasksettings = $rcmgetall->invoke($packager);
$rootsettings = $tasksettings[\backup_root_task::class];
$newvalue = 99;
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $testsettingname) {
$newvalue = $setting->get_value();
break;
}
}
$this->assertEquals($overridevalue, $newvalue);
}
/**
* Test overriding a backup task setting.
*
* @covers ::get_package
* @covers ::package
* @return void
*/
public function test_get_package() {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$currenttime = time();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$assigndata = [
'course' => $course->id,
'name' => 'Extremely interesting assignment',
'intro' => 'A great assignment to share',
];
$assign = $generator->create_module('assign', $assigndata);
$cminfo = get_fast_modinfo($course->id)->get_cm($assign->cmid);
$packager = new activity_packager($cminfo, $USER->id);
$package = $packager->get_package();
$this->assertEquals(2, count($package));
// Confirm there are backup file contents returned.
$this->assertTrue(array_key_exists('filecontents', $package));
$this->assertNotEmpty($package['filecontents']);
// Confirm the expected stored_file object is returned.
$this->assertTrue(array_key_exists('storedfile', $package));
$this->assertInstanceOf(\stored_file::class, $package['storedfile']);
// Check some known values in the returned stored_file object to confirm they match the file we have packaged.
$this->assertNotEmpty($package['storedfile']->get_contenthash());
$this->assertEquals($coursecontext->id, $package['storedfile']->get_contextid());
$this->assertEquals('core', $package['storedfile']->get_component());
$this->assertEquals('moodlenet_resource', $package['storedfile']->get_filearea());
$this->assertEquals('assign_backup.mbz', $package['storedfile']->get_filename());
$this->assertGreaterThan(0, $package['storedfile']->get_filesize());
$timecreated = $package['storedfile']->get_timecreated();
$this->assertGreaterThanOrEqual($currenttime, $timecreated);
$this->assertEquals($timecreated, $package['storedfile']->get_timemodified());
}
}