mirror of
https://github.com/moodle/moodle.git
synced 2025-04-16 14:02:32 +02:00
Merge branch 'MDL-69549-master-7' of git://github.com/andrewnicols/moodle
This commit is contained in:
commit
a83c69c2f5
@ -277,6 +277,11 @@ $string['confirmcheckfull'] = 'Are you absolutely sure you want to confirm {$a}
|
||||
$string['confirmcoursemove'] = 'Are you sure you want to move this course ({$a->course}) into this category ({$a->category})?';
|
||||
$string['considereddigitalminor'] = 'You are too young to create an account on this site.';
|
||||
$string['content'] = 'Content';
|
||||
$string['contentexport_aboutthiscourse'] = 'Course summary';
|
||||
$string['contentexport_coursesummary'] = 'This file is part of the content downloaded from <a href="{$a->courselink}">{$a->coursename}</a>.';
|
||||
$string['contentexport_footersummary'] = 'This file is part of the content downloaded from <a href="{$a->courselink}">{$a->coursename}</a> by {$a->userfullname} on {$a->date}';
|
||||
$string['contentexport_modulesummary'] = 'This page is part of the content downloaded from <a href="{$a->modulelink}">{$a->modulename}</a> on {$a->date}. Note that some content and any files larger than {$a->maxfilesize} are not downloaded.';
|
||||
$string['contentexport_viewfilename'] = 'View the file {$a}';
|
||||
$string['contentbank'] = 'Content bank';
|
||||
$string['continue'] = 'Continue';
|
||||
$string['continuetocourse'] = 'Click here to enter your course';
|
||||
|
150
lib/classes/content.php
Normal file
150
lib/classes/content.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Content API File Area definition.
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core;
|
||||
|
||||
use coding_exception;
|
||||
use context;
|
||||
use core\content\export\exporters\course_exporter;
|
||||
use core\content\export\exporters\component_exporter;
|
||||
use core\content\export\exporters\abstract_mod_exporter;
|
||||
use core\content\export\zipwriter;
|
||||
use core_component;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* The Content API allows all parts of Moodle to determine details about content within a component, or plugintype.
|
||||
*
|
||||
* This includes the description of files.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class content {
|
||||
|
||||
/**
|
||||
* Check whether the specified user can export content for the specified context.
|
||||
*
|
||||
* @param context $currentcontext
|
||||
* @param stdClass $user
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_export_context(context $currentcontext, stdClass $user): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export content for the specified context.
|
||||
*
|
||||
* @param context $requestedcontext The context to be exported
|
||||
* @param stdClass $user The user being exported
|
||||
* @param zipwriter $archive The Zip Archive to export to
|
||||
*/
|
||||
public static function export_context(context $requestedcontext, stdClass $user, zipwriter $archive): void {
|
||||
global $USER;
|
||||
|
||||
if ($requestedcontext->contextlevel != CONTEXT_COURSE) {
|
||||
throw new coding_exception('The Content Export API currently only supports the export of courses');
|
||||
}
|
||||
|
||||
if ($USER->id != $user->id) {
|
||||
throw new coding_exception('The Content Export API currently only supports export of the current user');
|
||||
}
|
||||
|
||||
// Ensure that the zipwriter is aware of the requested context.
|
||||
$archive->set_root_context($requestedcontext);
|
||||
|
||||
// Fetch all child contexts, indexed by path.
|
||||
$contextlist = [
|
||||
$requestedcontext->path => $requestedcontext,
|
||||
];
|
||||
foreach ($requestedcontext->get_child_contexts() as $context) {
|
||||
$contextlist[$context->path] = $context;
|
||||
}
|
||||
|
||||
// Reverse the order by key - this ensures that child contexts are processed before their parent.
|
||||
krsort($contextlist);
|
||||
|
||||
// Get the course modinfo.
|
||||
$modinfo = get_fast_modinfo($requestedcontext->instanceid);
|
||||
|
||||
// Filter out any context which cannot be exported.
|
||||
$contextlist = array_filter($contextlist, function($context) use ($user, $modinfo): bool {
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
return self::can_export_context($context, $user);
|
||||
}
|
||||
|
||||
if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
if (empty($modinfo->cms[$context->instanceid])) {
|
||||
// Unknown coursemodule in the course.
|
||||
return false;
|
||||
}
|
||||
|
||||
$cm = $modinfo->cms[$context->instanceid];
|
||||
|
||||
if (!$cm->uservisible) {
|
||||
// This user cannot view the activity.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Defer to setting checks.
|
||||
return self::can_export_context($context, $user);
|
||||
}
|
||||
|
||||
// Only course and activities are supported at this time.
|
||||
return false;
|
||||
});
|
||||
|
||||
// Export each context.
|
||||
$exportedcontexts = [];
|
||||
$coursecontroller = new course_exporter($requestedcontext->get_course_context(), $user, $archive);
|
||||
foreach ($contextlist as $context) {
|
||||
if ($context->contextlevel === CONTEXT_MODULE) {
|
||||
$cm = $modinfo->cms[$context->instanceid];
|
||||
$component = "mod_{$cm->modname}";
|
||||
|
||||
// Check for a specific implementation for this module.
|
||||
// This will export any content specific to this activity.
|
||||
// For example, in mod_folder it will export the list of folders.
|
||||
$classname = component_exporter::get_classname_for_component($component);
|
||||
$exportables = [];
|
||||
if (class_exists($classname) && is_a($classname, abstract_mod_exporter::class, true)) {
|
||||
$controller = new $classname($context, $component, $user, $archive);
|
||||
$exportables = $controller->get_exportables();
|
||||
}
|
||||
|
||||
// Pass the exportable content to the course controller for export.
|
||||
$coursecontroller->export_mod_content($context, $exportables);
|
||||
|
||||
$exportedcontexts[$context->id] = $context;
|
||||
} else if ($context->contextlevel === CONTEXT_COURSE) {
|
||||
// Export the course content.
|
||||
$coursecontroller->export_course($exportedcontexts);
|
||||
}
|
||||
}
|
||||
|
||||
$archive->finish();
|
||||
}
|
||||
}
|
96
lib/classes/content/export/exportable_item.php
Normal file
96
lib/classes/content/export/exportable_item.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The definition of an item which can be exported.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export;
|
||||
|
||||
use context;
|
||||
use core\content\export\exported_item;
|
||||
use core\content\export\zipwriter;
|
||||
|
||||
/**
|
||||
* An object used to represent content which can be served.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class exportable_item {
|
||||
|
||||
/** @var context The context associated with this exportable item */
|
||||
protected $context = null;
|
||||
|
||||
/** @var string The component being exported */
|
||||
protected $component = null;
|
||||
|
||||
/** @var string The name displayed to the user */
|
||||
protected $uservisiblename = null;
|
||||
|
||||
/**
|
||||
* Create a new exportable_item instance.
|
||||
*
|
||||
* @param context $context The context that this content belongs to
|
||||
* @param string $component The component that this content relates to
|
||||
* @param string $uservisiblename The name displayed in the export
|
||||
*/
|
||||
public function __construct(context $context, string $component, string $uservisiblename) {
|
||||
$this->context = $context;
|
||||
$this->component = $component;
|
||||
$this->uservisiblename = $uservisiblename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context that this exportable item is for.
|
||||
*
|
||||
* @return context
|
||||
*/
|
||||
public function get_context(): context {
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component that this exportable item relates to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_component(): string {
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user visible name for the exportable item.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_user_visible_name(): string {
|
||||
return $this->uservisiblename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the content to the archive.
|
||||
*
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
abstract public function add_to_archive(zipwriter $archive): ?exported_item;
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The definition of a set of files in a filearea to be exported.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use context;
|
||||
use core\content\export\exportable_item;
|
||||
use core\content\export\exported_item;
|
||||
use core\content\export\zipwriter;
|
||||
use moodle_url;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* The definition of a set of files in a filearea to be exported.
|
||||
*
|
||||
* All files mustbe in a single filearea and itemid combination.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exportable_filearea extends exportable_item {
|
||||
|
||||
/** @var string The destination path of the text content */
|
||||
protected $folderpath;
|
||||
|
||||
/** @var string $filearea The file to be exported */
|
||||
protected $filearea;
|
||||
|
||||
/** @var bool|int The itemid in the Files API */
|
||||
protected $itemid;
|
||||
|
||||
/** @var int The itemid to use in the pluginfile URL */
|
||||
protected $pluginfileitemid;
|
||||
|
||||
/**
|
||||
* Create a new exportable_item instance.
|
||||
*
|
||||
* If no filearea or itemid is specified the no attempt will be made to export files.
|
||||
*
|
||||
* @param context $context The context that this content belongs to
|
||||
* @param string $component
|
||||
* @param string $uservisiblename The name displayed to the user when filtering
|
||||
* @param string $filearea The file area in the Files API where these files are located
|
||||
* @param int $itemid The itemid in the Files API where these files are located
|
||||
* @param null|int $pluginfileitemid The itemid as used in the Pluginfile URL
|
||||
* @param string $folderpath Any sub-directory to place files in
|
||||
*/
|
||||
public function __construct(
|
||||
context $context,
|
||||
string $component,
|
||||
string $uservisiblename,
|
||||
string $filearea,
|
||||
int $itemid,
|
||||
?int $pluginfileitemid = null,
|
||||
string $folderpath = ''
|
||||
) {
|
||||
parent::__construct($context, $component, $uservisiblename);
|
||||
|
||||
$this->filearea = $filearea;
|
||||
$this->itemid = $itemid;
|
||||
$this->pluginfileitemid = $pluginfileitemid;
|
||||
$this->folderpath = $folderpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the content to the archive.
|
||||
*
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
public function add_to_archive(zipwriter $archive): ?exported_item {
|
||||
$fs = get_file_storage();
|
||||
|
||||
$files = $fs->get_area_files($this->context->id, $this->component, $this->filearea, $this->itemid);
|
||||
|
||||
$exporteditem = new exported_item();
|
||||
$exporteditem->set_title($this->get_user_visible_name());
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->is_directory()) {
|
||||
// Skip folders. The zipwriter cannot handle them.
|
||||
continue;
|
||||
}
|
||||
// Export the content to [contextpath]/[filepath].
|
||||
$relativefilepath = $this->get_filepath_for_file($file);
|
||||
|
||||
$archive->add_file_from_stored_file(
|
||||
$this->get_context(),
|
||||
$relativefilepath,
|
||||
$file
|
||||
);
|
||||
|
||||
if ($archive->is_file_in_archive($this->context, $relativefilepath)) {
|
||||
// The file was successfully added to the archive.
|
||||
$exporteditem->add_file($relativefilepath, false);
|
||||
} else {
|
||||
// The file was not added. Link to the live version instead.
|
||||
$exporteditem->add_file(
|
||||
$relativefilepath,
|
||||
false,
|
||||
self::get_pluginfile_url_for_stored_file($file, $this->pluginfileitemid)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $exporteditem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filepath for the specified stored_file.
|
||||
*
|
||||
* @param stored_file $file The file to get a filepath for
|
||||
* @return string The generated filepath
|
||||
*/
|
||||
protected function get_filepath_for_file(stored_file $file): string {
|
||||
$folderpath = rtrim($this->folderpath);
|
||||
|
||||
if (!empty($folderpath)) {
|
||||
$folderpath .= '/';
|
||||
}
|
||||
return sprintf(
|
||||
'%s%s%s%s',
|
||||
$folderpath,
|
||||
$file->get_filearea(),
|
||||
$file->get_filepath(),
|
||||
$file->get_filename()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pluginfile URL for a stored file.
|
||||
*
|
||||
* Note: The itemid in the pluginfile may be omitted in some URLs, despite an itemid being present in the database.
|
||||
* Equally, the itemid in the URL may not match the itemid in the files table.
|
||||
*
|
||||
* The pluginfileitemid argument provided to this function is the variant in the URL, and not the one in the files
|
||||
* table.
|
||||
*
|
||||
* @param stored_file $file The file whose link will be generated
|
||||
* @param null|int $pluginfileitemid The itemid of the file in pluginfile URL.
|
||||
*
|
||||
*/
|
||||
protected static function get_pluginfile_url_for_stored_file(stored_file $file, ?int $pluginfileitemid): string {
|
||||
$link = moodle_url::make_pluginfile_url(
|
||||
$file->get_contextid(),
|
||||
$file->get_component(),
|
||||
$file->get_filearea(),
|
||||
$pluginfileitemid,
|
||||
$file->get_filepath(),
|
||||
$file->get_filename(),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return $link->out(false);
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The definition of an item which can be exported.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use context;
|
||||
use core\content\export\exportable_item;
|
||||
use core\content\export\exported_item;
|
||||
use core\content\export\zipwriter;
|
||||
use moodle_url;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* An object used to represent content which can be served.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exportable_stored_file extends exportable_item {
|
||||
|
||||
/** @var string The destination path of the text content */
|
||||
protected $folderpath;
|
||||
|
||||
/** @var stored_file The file to be exported */
|
||||
protected $file;
|
||||
|
||||
/** @var int The itemid to use in the pluginfile URL */
|
||||
protected $pluginfileitemid;
|
||||
|
||||
/**
|
||||
* Create a new exportable_item instance.
|
||||
*
|
||||
* If no filearea or itemid is specified the no attempt will be made to export files.
|
||||
*
|
||||
* @param context $context The context that this content belongs to
|
||||
* @param string $component
|
||||
* @param string $uservisiblename The name displayed to the user when filtering
|
||||
* @param stored_file $file
|
||||
* @param null|int $pluginfileitemid The itemid as used in the pluginfile URL.
|
||||
* If no itemid is used, then a null value can be provided
|
||||
* @param string $folderpath Any sub-directory to place files in
|
||||
*/
|
||||
public function __construct(
|
||||
context $context,
|
||||
string $component,
|
||||
string $uservisiblename,
|
||||
stored_file $file,
|
||||
?int $pluginfileitemid = null,
|
||||
string $folderpath = ''
|
||||
) {
|
||||
parent::__construct($context, $component, $uservisiblename);
|
||||
|
||||
$this->file = $file;
|
||||
$this->folderpath = $folderpath;
|
||||
$this->pluginfileitemid = $pluginfileitemid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a set of exportable_items from a set of area paramaters as passed to get_areas_files().
|
||||
*
|
||||
* If no filearea or itemid is specified the no attempt will be made to export files.
|
||||
*
|
||||
* @param context $context The context that this content belongs to
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param null|int $itemid
|
||||
* @param null|int $pluginfileitemid The itemid as used in the pluginfile URL.
|
||||
* If no itemid is used, then a null value can be provided
|
||||
* @param string $folderpath Any sub-directory to place files in
|
||||
* @return array
|
||||
*/
|
||||
public static function create_from_area_params(
|
||||
context $context,
|
||||
string $component,
|
||||
string $filearea,
|
||||
?int $itemid,
|
||||
?int $pluginfileitemid = null,
|
||||
string $folderpath = ''
|
||||
): array {
|
||||
$fs = get_file_storage();
|
||||
if ($itemid === null) {
|
||||
$itemid = false;
|
||||
}
|
||||
|
||||
$exportables = [];
|
||||
foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
|
||||
if ($file->is_directory()) {
|
||||
// Do not export directories.
|
||||
// If they contain file contents the directory structure will be created in the zip file.
|
||||
continue;
|
||||
}
|
||||
$filepath = $file->get_filepath() . $file->get_filename();
|
||||
$exportables[] = new self($context, $component, $filepath, $file, $pluginfileitemid, $folderpath);
|
||||
}
|
||||
|
||||
return $exportables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the content to the archive.
|
||||
*
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
public function add_to_archive(zipwriter $archive): ?exported_item {
|
||||
// Export the content to [contextpath]/[filepath].
|
||||
$relativefilepath = $this->get_filepath_for_file();
|
||||
|
||||
$archive->add_file_from_stored_file(
|
||||
$this->get_context(),
|
||||
$relativefilepath,
|
||||
$this->file
|
||||
);
|
||||
|
||||
$exporteditem = new exported_item();
|
||||
$exporteditem->set_title($this->get_user_visible_name());
|
||||
|
||||
if ($archive->is_file_in_archive($this->context, $relativefilepath)) {
|
||||
// The file was successfully added to the archive.
|
||||
$exporteditem->add_file($relativefilepath, false);
|
||||
} else {
|
||||
// The file was not added. Link to the live version instead.
|
||||
$exporteditem->add_file(
|
||||
$relativefilepath,
|
||||
false,
|
||||
self::get_pluginfile_url_for_stored_file($this->file, $this->pluginfileitemid)
|
||||
);
|
||||
}
|
||||
|
||||
return $exporteditem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filepath for the specified stored_file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_filepath_for_file(): string {
|
||||
$folderpath = rtrim($this->folderpath);
|
||||
|
||||
if (!empty($folderpath)) {
|
||||
$folderpath .= '/';
|
||||
}
|
||||
return sprintf(
|
||||
'%s%s%s%s',
|
||||
$folderpath,
|
||||
$this->file->get_filearea(),
|
||||
$this->file->get_filepath(),
|
||||
$this->file->get_filename()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pluginfile URL for a stored file.
|
||||
*
|
||||
* Note: The itemid in the pluginfile may be omitted in some URLs, despite an itemid being present in the database.
|
||||
* Equally, the itemid in the URL may not match the itemid in the files table.
|
||||
*
|
||||
* The pluginfileitemid argument provided to this function is the variant in the URL, and not the one in the files
|
||||
* table.
|
||||
*
|
||||
* @param stored_file $file The file whose link will be generated
|
||||
* @param null|int $pluginfileitemid The itemid of the file in pluginfile URL.
|
||||
*
|
||||
*/
|
||||
protected static function get_pluginfile_url_for_stored_file(stored_file $file, ?int $pluginfileitemid): string {
|
||||
$link = moodle_url::make_pluginfile_url(
|
||||
$file->get_contextid(),
|
||||
$file->get_component(),
|
||||
$file->get_filearea(),
|
||||
$pluginfileitemid,
|
||||
$file->get_filepath(),
|
||||
$file->get_filename(),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return $link->out(false);
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The definition of a text area which can be exported.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use context;
|
||||
use core\content\export\exportable_item;
|
||||
use core\content\export\exported_item;
|
||||
use core\content\export\zipwriter;
|
||||
|
||||
/**
|
||||
* The definition of a text area which can be exported.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exportable_textarea extends exportable_item {
|
||||
|
||||
/** @var string The name of the table that ha the textarea within it */
|
||||
protected $tablename;
|
||||
|
||||
/** @var int The id in the table */
|
||||
protected $id;
|
||||
|
||||
/** @var string The name of the text field within the table */
|
||||
protected $textfield;
|
||||
|
||||
/** @var null|string The name of the format field relating to the text field */
|
||||
protected $textformatfield;
|
||||
|
||||
/** @var null|string The name of a file area for this content */
|
||||
protected $filearea;
|
||||
|
||||
/** @var null|int The itemid for files in this text field */
|
||||
protected $itemid;
|
||||
|
||||
/** @var null|int The itemid used for constructing pluginfiles */
|
||||
protected $pluginfileitemid;
|
||||
|
||||
/**
|
||||
* Create a new exportable_item instance.
|
||||
*
|
||||
* If no filearea or itemid is specified the no attempt will be made to export files.
|
||||
*
|
||||
* @param context $context The context that this content belongs to
|
||||
* @param string $component The component that this textarea belongs to
|
||||
* @param string $uservisiblename The name displayed to the user when filtering
|
||||
* @param string $tablename The name of the table that this textarea is in
|
||||
* @param string $textfield The field within the tbale
|
||||
* @param int $id The id in the database
|
||||
* @param null|string $textformatfield The field in the database relating to the format field if one is present
|
||||
* @param null|string $filearea The name of the file area for files associated with this text area
|
||||
* @param null|int $itemid The itemid for files associated with this text area
|
||||
* @param null|int $pluginfileitemid The itemid to use when constructing the pluginfile URL
|
||||
* Some fileareas do not use any itemid in the URL and should therefore provide a `null` value here.
|
||||
*/
|
||||
public function __construct(
|
||||
context $context,
|
||||
string $component,
|
||||
string $uservisiblename,
|
||||
string $tablename,
|
||||
string $textfield,
|
||||
int $id,
|
||||
?string $textformatfield = null,
|
||||
?string $filearea = null,
|
||||
?int $itemid = null,
|
||||
?int $pluginfileitemid = null
|
||||
) {
|
||||
parent::__construct($context, $component, $uservisiblename);
|
||||
|
||||
$this->tablename = $tablename;
|
||||
$this->textfield = $textfield;
|
||||
$this->textformatfield = $textformatfield;
|
||||
$this->id = $id;
|
||||
$this->filearea = $filearea;
|
||||
$this->itemid = $itemid;
|
||||
$this->pluginfileitemid = $pluginfileitemid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the content to the archive.
|
||||
*
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
public function add_to_archive(zipwriter $archive): ?exported_item {
|
||||
global $DB;
|
||||
|
||||
// Fetch the field.
|
||||
$fields = [$this->textfield];
|
||||
if (!empty($this->textformatfield)) {
|
||||
$fields[] = $this->textformatfield;
|
||||
}
|
||||
$record = $DB->get_record($this->tablename, ['id' => $this->id], implode(', ', $fields));
|
||||
|
||||
if (empty($record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Export all of the files for this text area.
|
||||
$text = $record->{$this->textfield};
|
||||
if (empty($text)) {
|
||||
$text = '';
|
||||
}
|
||||
|
||||
if ($this->may_include_files()) {
|
||||
// This content may include inline files.
|
||||
$exporteditem = $archive->add_pluginfiles_for_content(
|
||||
$this->get_context(),
|
||||
"",
|
||||
$text,
|
||||
$this->component,
|
||||
$this->filearea,
|
||||
$this->itemid,
|
||||
$this->pluginfileitemid
|
||||
);
|
||||
} else {
|
||||
$exporteditem = new exported_item();
|
||||
$exporteditem->set_content($text);
|
||||
}
|
||||
|
||||
$exporteditem->set_title($this->get_user_visible_name());
|
||||
return $exporteditem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether files may be included in this textarea.
|
||||
*
|
||||
* Both a filearea, and itemid are required for files to be exportable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function may_include_files(): bool {
|
||||
if ($this->filearea === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->itemid === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
198
lib/classes/content/export/exported_item.php
Normal file
198
lib/classes/content/export/exported_item.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Exported Item.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* This class describes the files which were exported, and any text content that those files were contained in.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exported_item {
|
||||
|
||||
/** @var string A short, descriptive, name for this exported item */
|
||||
protected $title = null;
|
||||
|
||||
/** @var string Any string content for export */
|
||||
protected $content = '';
|
||||
|
||||
/** @var string[] A list of files which were exported and are not present in the content */
|
||||
protected $files = [];
|
||||
|
||||
/** @var string[] A list of files which were exported and are present in the content */
|
||||
protected $contentfiles = [];
|
||||
|
||||
/**
|
||||
* Constructor for the exported_item.
|
||||
*
|
||||
* @param array $files A list of all files which were exported
|
||||
*/
|
||||
public function __construct(array $files = []) {
|
||||
$this->add_files($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a title for this exported item.
|
||||
*
|
||||
* @param string $title
|
||||
*/
|
||||
public function set_title(string $title): void {
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file to the list of exported files.
|
||||
*
|
||||
* @param string $relativefilepath The path to the content relative to the exported context
|
||||
* @param bool $incontent Whether this file is included within the content
|
||||
* @param null|string $url The URL to use of the live file where the file could not be stored in the archive
|
||||
*/
|
||||
public function add_file(string $relativefilepath, bool $incontent = false, ?string $url = null): void {
|
||||
if ($url === null) {
|
||||
$url = $relativefilepath;
|
||||
}
|
||||
|
||||
$file = (object) [
|
||||
'filepath' => $url,
|
||||
'filename' => basename($relativefilepath),
|
||||
];
|
||||
|
||||
$this->files[$relativefilepath] = $file;
|
||||
if ($incontent) {
|
||||
$this->contentfiles[$relativefilepath] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a list of files to the list of exported files.
|
||||
*
|
||||
* @param string[] $files The path to the content relative to the exported context
|
||||
* @param bool $incontent Whether this file is included within the content
|
||||
*/
|
||||
public function add_files(array $files, bool $incontent = false): void {
|
||||
foreach ($files as $relativefilepath) {
|
||||
$this->add_file($relativefilepath, $incontent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rewritten content.
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
public function set_content(string $content): void {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the rewritten content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a short, descriptive name associated with the exported content, if one is avaiable.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function get_title(): ?string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template data for this exported item.
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
public function get_template_data(): stdClass {
|
||||
return (object) [
|
||||
'title' => $this->get_title(),
|
||||
'files' => $this->get_noncontent_files(),
|
||||
'content' => $this->content,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all files in the exported item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_files(): array {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all files present in the content.
|
||||
*
|
||||
* That is those files which were exported, and which are referenced in some fashion.
|
||||
* These files typically do not need to be listed separately.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_content_files(): array {
|
||||
return $this->contentfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files which are not already referenced in the content.
|
||||
*
|
||||
* These files will typically be displayed in a separate list.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_noncontent_files(): array {
|
||||
return array_values(array_diff_key(
|
||||
$this->get_all_files(),
|
||||
$this->get_content_files()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the exported_item includes any data.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_any_data(): bool {
|
||||
if ($this->get_all_files()) {
|
||||
// Some files are present.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trim(html_to_text($this->get_content())) !== '') {
|
||||
// The content is not empty.
|
||||
return true;
|
||||
}
|
||||
|
||||
// No truthy conditions match.
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Activity module exporter for the content API.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core\content\export\exporters;
|
||||
|
||||
use core\content\controllers\export\component_controller;
|
||||
|
||||
/**
|
||||
* Activity module exporter for the content API.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class abstract_mod_exporter extends component_exporter {
|
||||
|
||||
/** @var \cm_info The activity information for this course module */
|
||||
protected $cm;
|
||||
|
||||
/**
|
||||
* Constructor for the general activity exporter.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(...func_get_args());
|
||||
|
||||
$coursecontext = $this->context->get_course_context();
|
||||
$modinfo = get_fast_modinfo($coursecontext->instanceid);
|
||||
$this->cm = $modinfo->get_cm($this->context->instanceid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exportable items for the user in the specified context.
|
||||
*
|
||||
* For activities which allow users to submit their own content which is not visible to all users, for example
|
||||
* graded activities, the caller can request that this be either included, or excluded.
|
||||
*
|
||||
* @param bool $includeuserdata Whether to include user data, in addition to shared content.
|
||||
* @return exportable_item[]
|
||||
*/
|
||||
abstract public function get_exportables(bool $includeuserdata = false): array;
|
||||
|
||||
/**
|
||||
* Get the modname for the activity.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_modname(): string {
|
||||
return $this->cm->modname;
|
||||
}
|
||||
}
|
112
lib/classes/content/export/exporters/component_exporter.php
Normal file
112
lib/classes/content/export/exporters/component_exporter.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Content API Export definition.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core\content\export\exporters;
|
||||
|
||||
use coding_exception;
|
||||
use context;
|
||||
use core\content\export\zipwriter;
|
||||
use core_component;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* A class to help define, describe, and export content in a specific context.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class component_exporter {
|
||||
|
||||
/** @var context The context to be exported */
|
||||
protected $context = null;
|
||||
|
||||
/** @var string The component that this instance belongs to */
|
||||
protected $component = null;
|
||||
|
||||
/** @var stdClass The user being exported */
|
||||
protected $user;
|
||||
|
||||
/** @var zipwriter A reference to the zipwriter */
|
||||
protected $archive;
|
||||
|
||||
/**
|
||||
* Constructor for a new exporter.
|
||||
*
|
||||
* @param context $context The context to export
|
||||
* @param string $component The component that this instance relates to
|
||||
* @param stdClass $user The user to be exported
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
public function __construct(context $context, string $component, stdClass $user, zipwriter $archive) {
|
||||
$this->context = $context;
|
||||
$this->component = $component;
|
||||
$this->user = $user;
|
||||
$this->archive = $archive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context being exported.
|
||||
*
|
||||
* @return context
|
||||
*/
|
||||
public function get_context(): context {
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_component(): string {
|
||||
[$type, $component] = core_component::normalize_component($this->component);
|
||||
if ($type === 'core') {
|
||||
return $component;
|
||||
}
|
||||
|
||||
return core_component::normalize_componentname($this->component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the archive used for export.
|
||||
*
|
||||
* @return zipwriter
|
||||
*/
|
||||
public function get_archive(): zipwriter {
|
||||
if ($this->archive === null) {
|
||||
throw new coding_exception("Archive has not been set up yet");
|
||||
}
|
||||
|
||||
return $this->archive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the exporter for the specified component.
|
||||
*
|
||||
* @param string $component The component to fetch a classname for
|
||||
* @return string The classname for the component
|
||||
*/
|
||||
public static function get_classname_for_component(string $component): string {
|
||||
return "{$component}\\content\\exporter";
|
||||
}
|
||||
}
|
274
lib/classes/content/export/exporters/course_exporter.php
Normal file
274
lib/classes/content/export/exporters/course_exporter.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The course exporter.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core\content\export\exporters;
|
||||
|
||||
use context_course;
|
||||
use context_module;
|
||||
use core\content\export\exported_item;
|
||||
use core\content\export\zipwriter;
|
||||
use section_info;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* The course exporter.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class course_exporter extends component_exporter {
|
||||
|
||||
/** @var stdClass The course being exported */
|
||||
protected $course;
|
||||
|
||||
/** @var \course_modinfo The course_modinfo instnace for this course */
|
||||
protected $modinfo;
|
||||
|
||||
/**
|
||||
* Constructor for the course exporter.
|
||||
*
|
||||
* @param context_course $context The context of the course to export
|
||||
* @param stdClass $user
|
||||
* @param zipwriter $archive
|
||||
*/
|
||||
public function __construct(context_course $context, stdClass $user, zipwriter $archive) {
|
||||
$this->course = get_course($context->instanceid);
|
||||
$this->modinfo = get_fast_modinfo($this->course, $user->id);
|
||||
|
||||
parent::__construct($context, 'core_course', $user, $archive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the course.
|
||||
*
|
||||
* @param context[] $exportedcontexts A list of contexts which were successfully exported
|
||||
*/
|
||||
public function export_course(array $exportedcontexts): void {
|
||||
// A course export is composed of:
|
||||
// - Course summary (including inline files)
|
||||
// - Overview files
|
||||
// - Section:
|
||||
// -- Section name
|
||||
// -- Section summary (including inline files)
|
||||
// -- List of available activities.
|
||||
|
||||
$aboutpagelink = $this->add_course_about_page();
|
||||
$templatedata = (object) [
|
||||
'aboutpagelink' => $aboutpagelink,
|
||||
'sections' => [],
|
||||
];
|
||||
|
||||
// Add all sections.
|
||||
foreach ($this->modinfo->get_section_info_all() as $number => $section) {
|
||||
$templatedata->sections[] = $this->get_course_section($exportedcontexts, $section);
|
||||
}
|
||||
|
||||
$this->get_archive()->add_file_from_template(
|
||||
$this->get_context(),
|
||||
'index.html',
|
||||
'core/content/export/course_index',
|
||||
$templatedata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add course about page.
|
||||
*
|
||||
* @return null|string The URL to the about page if one was generated
|
||||
*/
|
||||
protected function add_course_about_page(): ?string {
|
||||
$hascontent = false;
|
||||
|
||||
$templatedata = (object) [
|
||||
'summary' => '',
|
||||
'overviewfiles' => [],
|
||||
];
|
||||
|
||||
// Fetch the course summary content.
|
||||
if ($this->course->summary) {
|
||||
$summarydata = $this->get_archive()->add_pluginfiles_for_content(
|
||||
$this->get_context(),
|
||||
'_course',
|
||||
$this->course->summary,
|
||||
'course',
|
||||
'summary',
|
||||
0,
|
||||
null
|
||||
);
|
||||
|
||||
if ($summarydata->has_any_data()) {
|
||||
$hascontent = true;
|
||||
$templatedata->summary = $summarydata->get_content();
|
||||
}
|
||||
}
|
||||
|
||||
$files = $this->get_archive()->add_pluginfiles_for_content(
|
||||
$this->get_context(),
|
||||
'',
|
||||
'',
|
||||
'course',
|
||||
'overviewfiles',
|
||||
0,
|
||||
null
|
||||
)->get_noncontent_files();
|
||||
|
||||
if (count($files)) {
|
||||
$templatedata->overviewfiles = $files;
|
||||
$hascontent = true;
|
||||
}
|
||||
|
||||
if ($hascontent) {
|
||||
$this->get_archive()->add_file_from_template(
|
||||
$this->get_context(),
|
||||
'about.html',
|
||||
'core/content/export/course_summary',
|
||||
$templatedata
|
||||
);
|
||||
|
||||
return $this->get_archive()->get_relative_context_path($this->get_context(), $this->get_context(), 'about.html');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data for the specified course section.
|
||||
*
|
||||
* @param context[] $exportedcontexts A list of contexts which were successfully exported
|
||||
* @param section_info $section The section being exported
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function get_course_section(array $exportedcontexts, section_info $section): stdClass {
|
||||
$sectiondata = (object) [
|
||||
'number' => $section->section,
|
||||
'title' => $section->name,
|
||||
'summary' => '',
|
||||
'activities' => [],
|
||||
];
|
||||
|
||||
$sectiondata->summary = $this->get_archive()->add_pluginfiles_for_content(
|
||||
$this->get_context(),
|
||||
"_course",
|
||||
$section->summary,
|
||||
'course',
|
||||
'section',
|
||||
$section->id,
|
||||
$section->id
|
||||
)->get_template_data()->content;
|
||||
|
||||
if (empty($this->modinfo->sections[$section->section])) {
|
||||
return $sectiondata;
|
||||
}
|
||||
|
||||
foreach ($this->modinfo->sections[$section->section] as $cmid) {
|
||||
$cm = $this->modinfo->cms[$cmid];
|
||||
if (!$cm->uservisible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($cm->context->id, $exportedcontexts)) {
|
||||
// This activity was exported.
|
||||
// The link to it from the course index should be a relative link.
|
||||
$url = $this->get_archive()->get_relative_context_path($this->get_context(), $cm->context, 'index.html');
|
||||
} else {
|
||||
// This activity was not included in the export for some reason.
|
||||
// Link to the live activity.
|
||||
$url = $cm->url;
|
||||
}
|
||||
$sectiondata->activities[] = (object) [
|
||||
'title' => $cm->name,
|
||||
'modname' => $cm->modfullname,
|
||||
'link' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
return $sectiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all exportable content for an activity module.
|
||||
*
|
||||
* @param context_module $modcontect
|
||||
* @param exportable_item[] $export_exportables
|
||||
*/
|
||||
public function export_mod_content(context_module $modcontext, array $exportables): void {
|
||||
$cm = $this->modinfo->get_cm($modcontext->instanceid);
|
||||
$modname = $cm->modname;
|
||||
|
||||
$templatedata = (object) [
|
||||
'modulelink' => $cm->url,
|
||||
'modulename' => $cm->get_formatted_name(),
|
||||
'intro' => null,
|
||||
'sections' => [],
|
||||
];
|
||||
|
||||
if (plugin_supports('mod', $modname, FEATURE_MOD_INTRO, true)) {
|
||||
$templatedata->intro = $this->get_mod_intro_data($modcontext);
|
||||
}
|
||||
|
||||
$exporteditems = [];
|
||||
foreach ($exportables as $exportable) {
|
||||
$exporteditem = $exportable->add_to_archive($this->get_archive());
|
||||
$templatedata->sections[] = $exporteditem->get_template_data();
|
||||
}
|
||||
|
||||
// Add the index to the archive.
|
||||
$this->get_archive()->add_file_from_template(
|
||||
$modcontext,
|
||||
'index.html',
|
||||
'core/content/export/module_index',
|
||||
$templatedata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course_module introduction data.
|
||||
*
|
||||
* @param context_module $modcontect
|
||||
* @return null|string The content of the intro area
|
||||
*/
|
||||
protected function get_mod_intro_data(context_module $modcontext): ?string {
|
||||
global $DB;
|
||||
|
||||
$cm = $this->modinfo->get_cm($modcontext->instanceid);
|
||||
$modname = $cm->modname;
|
||||
|
||||
$record = $DB->get_record($modname, ['id' => $cm->instance], 'intro');
|
||||
|
||||
$exporteditem = $this->get_archive()->add_pluginfiles_for_content(
|
||||
$modcontext,
|
||||
'',
|
||||
$record->intro,
|
||||
"mod_{$modname}",
|
||||
'intro',
|
||||
0,
|
||||
null
|
||||
);
|
||||
|
||||
if ($exporteditem->has_any_data()) {
|
||||
return $exporteditem->get_content();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
563
lib/classes/content/export/zipwriter.php
Normal file
563
lib/classes/content/export/zipwriter.php
Normal file
@ -0,0 +1,563 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Zip writer wrapper.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core\content\export;
|
||||
|
||||
use context;
|
||||
use context_system;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* Zip writer wrapper.
|
||||
*
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class zipwriter {
|
||||
|
||||
/** @var int Maximum folder length name for a context */
|
||||
const MAX_CONTEXT_NAME_LENGTH = 32;
|
||||
|
||||
/** @var \ZipStream\ZipStream */
|
||||
protected $archive;
|
||||
|
||||
/** @var int Max file size of an individual file in the archive */
|
||||
protected $maxfilesize = 1 * 1024 * 1024 * 10;
|
||||
|
||||
/** @var resource File resource for the file handle for a file-based zip stream */
|
||||
protected $zipfilehandle = null;
|
||||
|
||||
/** @var string File path for a file-based zip stream */
|
||||
protected $zipfilepath = null;
|
||||
|
||||
/** @var context The context to use as a base for export */
|
||||
protected $rootcontext = null;
|
||||
|
||||
/** @var array The files in the zip */
|
||||
protected $filesinzip = [];
|
||||
|
||||
/** @var bool Whether page requirements needed for HTML pages have been added */
|
||||
protected $pagerequirementsadded = false;
|
||||
|
||||
/** @var stdClass The course relating to the root context */
|
||||
protected $course;
|
||||
|
||||
/** @var context The context of the course for the root contect */
|
||||
protected $coursecontext;
|
||||
|
||||
/**
|
||||
* zipwriter constructor.
|
||||
*
|
||||
* @param \ZipStream\ZipStream $archive
|
||||
* @param stdClass|null $options
|
||||
*/
|
||||
public function __construct(\ZipStream\ZipStream $archive, stdClass $options = null) {
|
||||
$this->archive = $archive;
|
||||
if ($options) {
|
||||
$this->parse_options($options);
|
||||
}
|
||||
|
||||
$this->rootcontext = context_system::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a root context for use during the export.
|
||||
*
|
||||
* This is primarily used for creating paths within the archive relative to the root context.
|
||||
*
|
||||
* @param context $rootcontext
|
||||
*/
|
||||
public function set_root_context(context $rootcontext): void {
|
||||
$this->rootcontext = $rootcontext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course object for the root context.
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function get_course(): stdClass {
|
||||
if ($this->course && ($this->coursecontext !== $this->rootcontext->get_course_context())) {
|
||||
$this->coursecontext = null;
|
||||
$this->course = null;
|
||||
}
|
||||
if (empty($this->course)) {
|
||||
$this->coursecontext = $this->rootcontext->get_course_context();
|
||||
$this->course = get_course($this->coursecontext->instanceid);
|
||||
}
|
||||
|
||||
return $this->course;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse options.
|
||||
*
|
||||
* @param stdClass $options
|
||||
*/
|
||||
protected function parse_options(stdClass $options): void {
|
||||
if (property_exists($options, 'maxfilesize')) {
|
||||
$this->maxfilesize = $options->maxfilesize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish writing the zip footer.
|
||||
*/
|
||||
public function finish(): void {
|
||||
$this->archive->finish();
|
||||
|
||||
if ($this->zipfilehandle) {
|
||||
fclose($this->zipfilehandle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stream writer.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param stdClass|null $exportoptions
|
||||
* @return static
|
||||
*/
|
||||
public static function get_stream_writer(string $filename, stdClass $exportoptions = null) {
|
||||
$options = new \ZipStream\Option\Archive();
|
||||
$options->setSendHttpHeaders(true);
|
||||
$archive = new \ZipStream\ZipStream($filename, $options);
|
||||
|
||||
$zipwriter = new static($archive, $exportoptions);
|
||||
|
||||
\core\session\manager::write_close();
|
||||
return $zipwriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file writer.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param stdClass|null $exportoptions
|
||||
* @return static
|
||||
*/
|
||||
public static function get_file_writer(string $filename, stdClass $exportoptions = null) {
|
||||
$options = new \ZipStream\Option\Archive();
|
||||
|
||||
$dir = make_request_directory();
|
||||
$filepath = $dir . "/$filename";
|
||||
$fh = fopen($filepath, 'w');
|
||||
|
||||
$options->setOutputStream($fh);
|
||||
$options->setSendHttpHeaders(false);
|
||||
$archive = new \ZipStream\ZipStream($filename, $options);
|
||||
|
||||
$zipwriter = new static($archive, $exportoptions);
|
||||
|
||||
$zipwriter->zipfilehandle = $fh;
|
||||
$zipwriter->zipfilepath = $filepath;
|
||||
|
||||
\core\session\manager::write_close();
|
||||
return $zipwriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path for a file-based zip writer.
|
||||
*
|
||||
* If this is not a file-based writer then no value is returned.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function get_file_path(): ?string {
|
||||
return $this->zipfilepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file from the File Storage API.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $filepathinzip
|
||||
* @param stored_file $file The file to add
|
||||
*/
|
||||
public function add_file_from_stored_file(
|
||||
context $context,
|
||||
string $filepathinzip,
|
||||
stored_file $file
|
||||
): void {
|
||||
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
|
||||
|
||||
if ($file->get_filesize() <= $this->maxfilesize) {
|
||||
$filehandle = $file->get_content_file_handle();
|
||||
$this->archive->addFileFromStream($fullfilepathinzip, $filehandle);
|
||||
fclose($filehandle);
|
||||
|
||||
$this->filesinzip[] = $fullfilepathinzip;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file from string content.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $filepathinzip
|
||||
* @param string $content
|
||||
*/
|
||||
public function add_file_from_string(
|
||||
context $context,
|
||||
string $filepathinzip,
|
||||
string $content
|
||||
): void {
|
||||
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
|
||||
|
||||
$this->archive->addFile($fullfilepathinzip, $content);
|
||||
|
||||
$this->filesinzip[] = $fullfilepathinzip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file based on a Mustache Template and associated data.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $filepathinzip
|
||||
* @param string $template
|
||||
* @param stdClass $templatedata
|
||||
*/
|
||||
public function add_file_from_template(
|
||||
context $context,
|
||||
string $filepathinzip,
|
||||
string $template,
|
||||
stdClass $templatedata
|
||||
): void {
|
||||
global $CFG, $PAGE, $SITE, $USER;
|
||||
|
||||
$exportedcourse = $this->get_course();
|
||||
$courselink = (new moodle_url('/course/view.php', ['id' => $exportedcourse->id]))->out(false);
|
||||
|
||||
$this->add_template_requirements();
|
||||
|
||||
$templatedata->global = (object) [
|
||||
'righttoleft' => right_to_left(),
|
||||
'language' => str_replace('_', '-', current_language()),
|
||||
'sitename' => $SITE->fullname,
|
||||
'siteurl' => $CFG->wwwroot,
|
||||
'pathtotop' => $this->get_relative_context_path($context, $this->rootcontext, '/'),
|
||||
'contentexportfooter' => get_string('contentexport_footersummary', 'core', (object) [
|
||||
'courselink' => $courselink,
|
||||
'coursename' => $exportedcourse->fullname,
|
||||
'userfullname' => fullname($USER),
|
||||
'date' => userdate(time()),
|
||||
]),
|
||||
'contentexportsummary' => get_string('contentexport_coursesummary', 'core', (object) [
|
||||
'courselink' => $courselink,
|
||||
'coursename' => $exportedcourse->fullname,
|
||||
'date' => userdate(time()),
|
||||
]),
|
||||
'coursename' => $exportedcourse->fullname,
|
||||
'courseshortname' => $exportedcourse->shortname,
|
||||
'courselink' => $courselink,
|
||||
'exportdate' => userdate(time()),
|
||||
'maxfilesize' => display_size($this->maxfilesize),
|
||||
];
|
||||
|
||||
$renderer = $PAGE->get_renderer('core');
|
||||
$this->add_file_from_string($context, $filepathinzip, $renderer->render_from_template($template, $templatedata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all requirements for a templated page are present.
|
||||
*
|
||||
* This includes CSS, and any other similar content.
|
||||
*/
|
||||
protected function add_template_requirements(): void {
|
||||
if ($this->pagerequirementsadded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CSS required.
|
||||
$this->add_content_from_dirroot('/theme/boost/style/moodle.css', 'shared/moodle.css');
|
||||
|
||||
$this->pagerequirementsadded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content from the dirroot into the specified path in the zip file.
|
||||
*
|
||||
* @param string $dirrootpath
|
||||
* @param string $pathinzip
|
||||
*/
|
||||
protected function add_content_from_dirroot(string $dirrootpath, string $pathinzip): void {
|
||||
global $CFG;
|
||||
|
||||
$this->archive->addFileFromPath(
|
||||
$this->get_context_path($this->rootcontext, $pathinzip),
|
||||
"{$CFG->dirroot}/{$dirrootpath}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the file was actually added to the archive.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $filepathinzip
|
||||
* @return bool
|
||||
*/
|
||||
public function is_file_in_archive(context $context, string $filepathinzip): bool {
|
||||
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
|
||||
|
||||
return in_array($fullfilepathinzip, $this->filesinzip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path to the context within the zip.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $filepathinzip
|
||||
* @return string
|
||||
*/
|
||||
public function get_context_path(context $context, string $filepathinzip): string {
|
||||
if (!$context->is_child_of($this->rootcontext, true)) {
|
||||
throw new \coding_exception("Unexpected path requested");
|
||||
}
|
||||
|
||||
// Fetch the path from the course down.
|
||||
$parentcontexts = array_filter(
|
||||
$context->get_parent_contexts(true),
|
||||
function(context $curcontext): bool {
|
||||
return $curcontext->is_child_of($this->rootcontext, true);
|
||||
}
|
||||
);
|
||||
|
||||
foreach (array_reverse($parentcontexts) as $curcontext) {
|
||||
$path[] = $this->get_context_folder_name($curcontext);
|
||||
}
|
||||
|
||||
$path[] = $filepathinzip;
|
||||
|
||||
$finalpath = implode(DIRECTORY_SEPARATOR, $path);
|
||||
|
||||
// Remove relative paths (./).
|
||||
$finalpath = str_replace('./', '/', $finalpath);
|
||||
|
||||
// De-duplicate slashes.
|
||||
$finalpath = str_replace('//', '/', $finalpath);
|
||||
|
||||
// Remove leading /.
|
||||
ltrim($finalpath, '/');
|
||||
|
||||
return $this->sanitise_filename($finalpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a relative path to the specified context path.
|
||||
*
|
||||
* @param context $rootcontext
|
||||
* @param context $targetcontext
|
||||
* @param string $filepathinzip
|
||||
* @return string
|
||||
*/
|
||||
public function get_relative_context_path(context $rootcontext, context $targetcontext, string $filepathinzip): string {
|
||||
$path = [];
|
||||
if ($targetcontext === $rootcontext) {
|
||||
$lookupcontexts = [];
|
||||
} else if ($targetcontext->is_child_of($rootcontext, true)) {
|
||||
// Fetch the path from the course down.
|
||||
$lookupcontexts = array_filter(
|
||||
$targetcontext->get_parent_contexts(true),
|
||||
function(context $curcontext): bool {
|
||||
return $curcontext->is_child_of($this->rootcontext, false);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($lookupcontexts as $curcontext) {
|
||||
array_unshift($path, $this->get_context_folder_name($curcontext));
|
||||
}
|
||||
} else if ($targetcontext->is_parent_of($rootcontext, true)) {
|
||||
$lookupcontexts = $targetcontext->get_parent_contexts(true);
|
||||
$path[] = '..';
|
||||
}
|
||||
|
||||
$path[] = $filepathinzip;
|
||||
$relativepath = implode(DIRECTORY_SEPARATOR, $path);
|
||||
|
||||
// De-duplicate slashes and remove leading /.
|
||||
$relativepath = ltrim(preg_replace('#/+#', '/', $relativepath), '/');
|
||||
|
||||
if (substr($relativepath, 0, 1) !== '.') {
|
||||
$relativepath = "./{$relativepath}";
|
||||
}
|
||||
|
||||
return $this->sanitise_filename($relativepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitise the file path, removing any unsuitable characters.
|
||||
*
|
||||
* @param string $filepath
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitise_filename(string $filepath): string {
|
||||
// The filename must be sanitised in the same as the parent ZipStream library.
|
||||
return \ZipStream\File::filterFilename($filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the folder for the specified context.
|
||||
*
|
||||
* @param context $context
|
||||
* @return string
|
||||
*/
|
||||
protected function get_context_folder_name(context $context): string {
|
||||
$shortenedname = shorten_text(
|
||||
clean_param($context->get_context_name(), PARAM_FILE),
|
||||
self::MAX_CONTEXT_NAME_LENGTH,
|
||||
true,
|
||||
json_decode('"' . '\u2026' . '"')
|
||||
);
|
||||
|
||||
return "{$shortenedname}_.{$context->id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite any pluginfile URLs in the content.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $content
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param null|int $pluginfileitemid The itemid to use in the pluginfile URL when composing any required URLs
|
||||
* @return string
|
||||
*/
|
||||
protected function rewrite_other_pluginfile_urls(
|
||||
context $context,
|
||||
string $content,
|
||||
string $component,
|
||||
string $filearea,
|
||||
?int $pluginfileitemid
|
||||
): string {
|
||||
// The pluginfile URLs should have been rewritten when the files were exported, but if any file was too large it
|
||||
// may not have been included.
|
||||
// In that situation use a tokenpluginfile URL.
|
||||
|
||||
if (strpos($content, '@@PLUGINFILE@@/') !== false) {
|
||||
// Some files could not be rewritten.
|
||||
// Use a tokenurl pluginfile for those.
|
||||
$content = file_rewrite_pluginfile_urls(
|
||||
$content,
|
||||
'pluginfile.php',
|
||||
$context->id,
|
||||
$component,
|
||||
$filearea,
|
||||
$pluginfileitemid,
|
||||
[
|
||||
'includetoken' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export files releating to this text area.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $subdir The sub directory to export any files to
|
||||
* @param string $content
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param int $fileitemid The itemid as used in the Files API
|
||||
* @param null|int $pluginfileitemid The itemid to use in the pluginfile URL when composing any required URLs
|
||||
* @return exported_item
|
||||
*/
|
||||
public function add_pluginfiles_for_content(
|
||||
context $context,
|
||||
string $subdir,
|
||||
string $content,
|
||||
string $component,
|
||||
string $filearea,
|
||||
int $fileitemid,
|
||||
?int $pluginfileitemid
|
||||
): exported_item {
|
||||
// Export all of the files for this text area.
|
||||
$fs = get_file_storage();
|
||||
$files = $fs->get_area_files($context->id, $component, $filearea, $fileitemid);
|
||||
|
||||
$result = new exported_item();
|
||||
foreach ($files as $file) {
|
||||
if ($file->is_directory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filepathinzip = self::get_filepath_for_file($file, $subdir, false);
|
||||
$this->add_file_from_stored_file(
|
||||
$context,
|
||||
$filepathinzip,
|
||||
$file
|
||||
);
|
||||
|
||||
if ($this->is_file_in_archive($context, $filepathinzip)) {
|
||||
// Attempt to rewrite any @@PLUGINFILE@@ URLs for this file in the content.
|
||||
$searchpath = "@@PLUGINFILE@@" . $file->get_filepath() . rawurlencode($file->get_filename());
|
||||
if (strpos($content, $searchpath) !== false) {
|
||||
$content = str_replace($searchpath, self::get_filepath_for_file($file, $subdir, true), $content);
|
||||
$result->add_file($filepathinzip, true);
|
||||
} else {
|
||||
$result->add_file($filepathinzip, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$content = $this->rewrite_other_pluginfile_urls($context, $content, $component, $filearea, $pluginfileitemid);
|
||||
$result->set_content($content);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filepath for the specified stored_file.
|
||||
*
|
||||
* @param stored_file $file
|
||||
* @param string $parentdir Any parent directory to place this file in
|
||||
* @param bool $escape
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_filepath_for_file(stored_file $file, string $parentdir, bool $escape): string {
|
||||
$path = [];
|
||||
|
||||
$filepath = sprintf(
|
||||
'%s/%s/%s/%s',
|
||||
$parentdir,
|
||||
$file->get_filearea(),
|
||||
$file->get_filepath(),
|
||||
$file->get_filename()
|
||||
);
|
||||
|
||||
if ($escape) {
|
||||
foreach (explode('/', $filepath) as $dirname) {
|
||||
$path[] = rawurlencode($dirname);
|
||||
}
|
||||
$filepath = implode('/', $path);
|
||||
}
|
||||
|
||||
return ltrim(preg_replace('#/+#', '/', $filepath), '/');
|
||||
}
|
||||
|
||||
}
|
106
lib/templates/content/export/course_index.mustache
Normal file
106
lib/templates/content/export/course_index.mustache
Normal file
@ -0,0 +1,106 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/content/export/course_index
|
||||
|
||||
Renders content for a course index.
|
||||
This template is not for use within moodle.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* global
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"global": {
|
||||
"righttoleft": 0,
|
||||
"language": "en",
|
||||
"sitename": "Kevin's Emporium of fine course material",
|
||||
"siteurl": "https://kevin.example.com",
|
||||
"pathtotop": "./",
|
||||
"contentexportfooter": "This file is part of the content downloaded from <a href='https://example.com'>Kevin's Emporium of fine course material</a> by Jennifer Collins on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"contentexportsummary": "This file is part of the content downloaded from <a href='https://example.com/course/view.php?id=4'>Kevin's Emporium of fine course material</a> on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"coursename": "Marketing 101",
|
||||
"courseshortname": "MKT101",
|
||||
"courselink": "https://example.com/course/view.php?id=4",
|
||||
"exportdate": "Tuesday, 24th February 2021, 12:21 am",
|
||||
"maxfilesize": "40MB"
|
||||
},
|
||||
"aboutpagelink": "./about.html",
|
||||
"sections": [
|
||||
{
|
||||
"title": "Welcome",
|
||||
"summary": "<p>Welcome to my <em>awesome</em> course.</p>",
|
||||
"activities": [
|
||||
{
|
||||
"title": "Data Structures - Arrays and Objects",
|
||||
"modname": "Assignment",
|
||||
"link": "./dsao_.1/index.html"
|
||||
},
|
||||
{
|
||||
"title": "Data Structures - Discussion",
|
||||
"modname": "Forum",
|
||||
"link": "./dsao_.2/index.html"
|
||||
},
|
||||
{
|
||||
"title": "Data Structures - Lecture Notes",
|
||||
"modname": "Folder",
|
||||
"link": "./dsao_.3/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
{{<core/content/export/external_page}}
|
||||
{{$pagecontent}}
|
||||
<div class="alert alert-info alert-block">
|
||||
{{{global.contentexportsummary}}}
|
||||
</div>
|
||||
{{#aboutpagelink}}
|
||||
<div>
|
||||
<a href="{{{aboutpagelink}}}">{{#str}}contentexport_aboutthiscourse, core{{/str}}</a>
|
||||
</div>
|
||||
{{/aboutpagelink}}
|
||||
|
||||
{{#sections.0}}
|
||||
<div>
|
||||
{{#sections}}
|
||||
<h3>{{{title}}}</h3>
|
||||
{{#summary}}
|
||||
<div>
|
||||
{{{summary}}}
|
||||
</div>
|
||||
{{/summary}}
|
||||
{{#activities.0}}
|
||||
<ul>
|
||||
{{#activities}}
|
||||
<li><a href="{{{link}}}">{{{title}}} ({{{modname}}})</a></li>
|
||||
{{/activities}}
|
||||
</ul>
|
||||
{{/activities.0}}
|
||||
<hr>
|
||||
{{/sections}}
|
||||
</div>
|
||||
{{/sections.0}}
|
||||
{{/pagecontent}}
|
||||
{{/core/content/export/external_page}}
|
88
lib/templates/content/export/course_summary.mustache
Normal file
88
lib/templates/content/export/course_summary.mustache
Normal file
@ -0,0 +1,88 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/content/export/course_summary
|
||||
|
||||
Renders course summary and overview information.
|
||||
This template is not for use within moodle.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* global
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"global": {
|
||||
"righttoleft": 0,
|
||||
"language": "en",
|
||||
"sitename": "Kevin's Emporium of fine course material",
|
||||
"siteurl": "https://kevin.example.com",
|
||||
"pathtotop": "./",
|
||||
"contentexportfooter": "This file is part of the content downloaded from <a href='https://example.com'>Kevin's Emporium of fine course material</a> by Jennifer Collins on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"contentexportsummary": "This file is part of the content downloaded from <a href='https://example.com/course/view.php?id=4'>Kevin's Emporium of fine course material</a> on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"coursename": "Marketing 101",
|
||||
"courseshortname": "MKT101",
|
||||
"courselink": "https://example.com/course/view.php?id=4",
|
||||
"exportdate": "Tuesday, 24th February 2021, 12:21 am",
|
||||
"maxfilesize": "40MB"
|
||||
},
|
||||
"summary": "<p>This is a summary of the course, and it may contain PLUGINFILE references.</p>",
|
||||
"overviewfiles": [
|
||||
{
|
||||
"filepath": "./_course/overviewfiles/example.pdf",
|
||||
"filename": "Example PDF"
|
||||
},
|
||||
{
|
||||
"filepath": "./_course/overviewfiles/example.jpg",
|
||||
"filename": "Example JPG"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
{{<core/content/export/external_page}}
|
||||
{{$pagecontent}}
|
||||
<div class="alert alert-info alert-block">
|
||||
{{{global.contentexportsummary}}}
|
||||
</div>
|
||||
{{#summary}}
|
||||
<h2>{{#str}}summary, core{{/str}}</h2>
|
||||
<div>
|
||||
{{{summary}}}
|
||||
</div>
|
||||
<hr>
|
||||
{{/summary}}
|
||||
|
||||
{{#overviewfiles.0}}
|
||||
<h2>{{#str}}courseoverviewfiles, core{{/str}}</h2>
|
||||
<div>
|
||||
{{#overviewfiles.0}}
|
||||
<ul>
|
||||
{{#overviewfiles}}
|
||||
<li><a href="{{filepath}}" title="{{#str}}contentexport_viewfilename, core, {{filename}}{{/str}}">{{filename}}</a></li>
|
||||
{{/overviewfiles}}
|
||||
</ul>
|
||||
{{/overviewfiles.0}}
|
||||
</div>
|
||||
<hr>
|
||||
{{/overviewfiles.0}}
|
||||
{{/pagecontent}}
|
||||
{{/core/content/export/external_page}}
|
79
lib/templates/content/export/external_page.mustache
Normal file
79
lib/templates/content/export/external_page.mustache
Normal file
@ -0,0 +1,79 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/content/export/external_page
|
||||
|
||||
Renders content in a basic HTML wrapper designed for viewing offline.
|
||||
This template is not for use within moodle.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* global
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"global": {
|
||||
"righttoleft": 0,
|
||||
"language": "en",
|
||||
"sitename": "Kevin's Emporium of fine course material",
|
||||
"siteurl": "https://kevin.example.com",
|
||||
"pathtotop": "./",
|
||||
"contentexportfooter": "This file is part of the content downloaded from <a href='https://example.com'>Kevin's Emporium of fine course material</a> by Jennifer Collins on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"contentexportsummary": "This file is part of the content downloaded from <a href='https://example.com/course/view.php?id=4'>Kevin's Emporium of fine course material</a> on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"coursename": "Marketing 101",
|
||||
"courseshortname": "MKT101",
|
||||
"courselink": "https://example.com/course/view.php?id=4",
|
||||
"exportdate": "Tuesday, 24th February 2021, 12:21 am",
|
||||
"maxfilesize": "40MB"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{global.language}}"{{#global.righttoleft}} dir="rtl"{{/global.righttoleft}}>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{global.sitename}} - {{global.coursename}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{global.pathtotop}}/shared/moodle.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<nav class="navbar navbar-light bg-white border-bottom">
|
||||
<a class="navbar-brand" href="{{{global.siteurl}}}" title="{{{global.sitename}}}">
|
||||
{{{global.sitename}}}
|
||||
</a>
|
||||
</nav>
|
||||
<main class="container-fluid mt-2 flex-grow-1 flex-shrink-1">
|
||||
<div>
|
||||
<h1><a href="{{global.pathtotop}}index.html">{{global.coursename}}</a></h1>
|
||||
<p>{{global.courseshortname}}</p>
|
||||
{{$pagecontent}}{{/pagecontent}}
|
||||
</div>
|
||||
</main>
|
||||
<footer id="page-footer" class="py-3 bg-dark text-light">
|
||||
<div class="container">
|
||||
<div>{{{global.contentexportfooter}}}</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
112
lib/templates/content/export/module_index.mustache
Normal file
112
lib/templates/content/export/module_index.mustache
Normal file
@ -0,0 +1,112 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/content/export/module_index
|
||||
|
||||
Renders content for a course index.
|
||||
This template is not for use within moodle.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* global
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"global": {
|
||||
"righttoleft": 0,
|
||||
"language": "en",
|
||||
"sitename": "Kevin's Emporium of fine course material",
|
||||
"siteurl": "https://kevin.example.com",
|
||||
"pathtotop": "./",
|
||||
"contentexportfooter": "This file is part of the content downloaded from <a href='https://example.com'>Kevin's Emporium of fine course material</a> by Jennifer Collins on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"contentexportsummary": "This file is part of the content downloaded from <a href='https://example.com/course/view.php?id=4'>Kevin's Emporium of fine course material</a> on Tuesday, 24th February 2021, 12:21 am.",
|
||||
"coursename": "Marketing 101",
|
||||
"courseshortname": "MKT101",
|
||||
"courselink": "https://example.com/course/view.php?id=4",
|
||||
"exportdate": "Tuesday, 24th February 2021, 12:21 am",
|
||||
"maxfilesize": "40MB"
|
||||
},
|
||||
"modulename": "Data Structures - Discussion",
|
||||
"modulelink": "https://kevin.example.com/mod/forum/view.php?id=53",
|
||||
"intro": "<p>This forum provides a place for you to discuss the concepts of data structures.</p>",
|
||||
"sections": [
|
||||
{
|
||||
"title": "The title set in an exportable_item",
|
||||
"content": "<p>Some text area included in an exportable_item.</p>",
|
||||
"files": [
|
||||
{
|
||||
"filename": "Example file which was not included in the text field above",
|
||||
"filepath": "./sections/0/_files"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
{{<core/content/export/external_page}}
|
||||
{{$pagecontent}}
|
||||
<h2>
|
||||
{{{modulename}}}
|
||||
</h2>
|
||||
|
||||
<div class="alert alert-info alert-block">
|
||||
{{#str}}contentexport_modulesummary, core,
|
||||
{
|
||||
"modulelink": "{{modulelink}}",
|
||||
"modulename": {{#quote}}{{{modulename}}}{{/quote}},
|
||||
"date": "{{global.exportdate}}",
|
||||
"maxfilesize": "{{global.maxfilesize}}"
|
||||
}
|
||||
{{/str}}
|
||||
</div>
|
||||
|
||||
{{#intro}}
|
||||
<h3>{{#str}}moduleintro, core{{/str}}</h3>
|
||||
<div>
|
||||
{{{intro}}}
|
||||
</div>
|
||||
<hr>
|
||||
{{/intro}}
|
||||
|
||||
{{#sections}}
|
||||
<div>
|
||||
<h3>{{{title}}}</h3>
|
||||
<div>
|
||||
{{#content}}
|
||||
<div>
|
||||
{{{content}}}
|
||||
</div>
|
||||
{{/content}}
|
||||
|
||||
{{#files.0}}
|
||||
<ul>
|
||||
{{#files}}
|
||||
<li><a href="{{filepath}}" title="{{#str}}contentexport_viewfilename, core, {{filename}}{{/str}}">{{filename}}</a></li>
|
||||
{{/files}}
|
||||
</ul>
|
||||
{{/files.0}}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{{/sections}}
|
||||
{{/pagecontent}}
|
||||
{{/core/content/export/external_page}}
|
@ -0,0 +1,254 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\exportable_items\exportable_filearea.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use advanced_testcase;
|
||||
use context;
|
||||
use context_system;
|
||||
use core\content\export\zipwriter;
|
||||
use core\content\export\exported_item;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Unit tests for the `exportable_filearea` export item class.
|
||||
*
|
||||
* @coversDefaultClass core\content\exportable_items\exportable_filearea
|
||||
*/
|
||||
class exportable_filearea_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that the the exportable_filearea does not fetch files when none exist.
|
||||
*/
|
||||
public function test_no_files(): void {
|
||||
$exportable = new exportable_filearea(
|
||||
context_system::instance(),
|
||||
'fake',
|
||||
'Some fake filearea',
|
||||
'filearea',
|
||||
1
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(exportable_filearea::class, $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the exportable_filearea returns all stored_file items for only the specified itemid, but those which
|
||||
* are not included in the archive receive a pluginfile URL.
|
||||
*/
|
||||
public function test_specified_itemid_excluded_from_zip(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
$otherfiles2 = $this->create_files(context_system::instance(), $component, "other{$filearea}", 2);
|
||||
|
||||
$exportable = new exportable_filearea(
|
||||
$context,
|
||||
$component,
|
||||
'Some filearea description',
|
||||
$filearea,
|
||||
2
|
||||
);
|
||||
|
||||
// There is only one exportable.
|
||||
$this->assertInstanceOf(exportable_filearea::class, $exportable);
|
||||
|
||||
$file2 = reset($files2);
|
||||
$item = $this->assert_exportable_matches_file($component, $user, $context, $filearea, '', $files2, false, $exportable);
|
||||
$this->assertCount(count($files2), $item->get_all_files());
|
||||
$comparisonurl = new moodle_url('/tokenpluginfile.php/');
|
||||
foreach ($item->get_all_files() as $url) {
|
||||
$this->assertStringStartsWith($comparisonurl->out(false), $url->filepath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the exportable_filearea returns all stored_file items for only the specified itemid.
|
||||
*/
|
||||
public function test_specified_itemid(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
$otherfiles2 = $this->create_files(context_system::instance(), $component, "other{$filearea}", 2);
|
||||
|
||||
$exportable = new exportable_filearea(
|
||||
$context,
|
||||
$component,
|
||||
'Some filearea description',
|
||||
$filearea,
|
||||
2
|
||||
);
|
||||
|
||||
// There is only one exportable.
|
||||
$this->assertInstanceOf(exportable_filearea::class, $exportable);
|
||||
|
||||
$file2 = reset($files2);
|
||||
$item = $this->assert_exportable_matches_file($component, $user, $context, $filearea, '', $files2, true, $exportable);
|
||||
$this->assertCount(count($files2), $item->get_all_files());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the exportable_filearea returns all stored_files into the correct file location.
|
||||
*/
|
||||
public function test_in_subdir(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
$subdir = 'a/path/to/my/subdir';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
|
||||
$exportable = new exportable_filearea(
|
||||
$context,
|
||||
$component,
|
||||
'Some filearea description',
|
||||
$filearea,
|
||||
2,
|
||||
2,
|
||||
$subdir
|
||||
);
|
||||
|
||||
// There is only one exportable.
|
||||
$this->assertInstanceOf(exportable_filearea::class, $exportable);
|
||||
|
||||
$item = $this->assert_exportable_matches_file($component, $user, $context, $filearea, $subdir, $files2, true, $exportable);
|
||||
$this->assertCount(count($files2), $item->get_all_files());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create files for use in testing.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param int $itemid
|
||||
* @param int $count
|
||||
* @return filearea[]
|
||||
*/
|
||||
protected function create_files(context $context, string $component, string $filearea, int $itemid, int $count = 1): array {
|
||||
$fs = get_file_storage();
|
||||
|
||||
$files = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
|
||||
$filepath = '/';
|
||||
for ($j = 0; $j < $i; $j++) {
|
||||
$filepath .= "{$j}/";
|
||||
}
|
||||
|
||||
$files[] = $fs->create_file_from_string(
|
||||
(object) [
|
||||
'contextid' => $context->id,
|
||||
'component' => $component,
|
||||
'filearea' => $filearea,
|
||||
'filepath' => $filepath,
|
||||
'filename' => "file.txt",
|
||||
'itemid' => $itemid,
|
||||
],
|
||||
"File content: {$i}"
|
||||
);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the supplied expotable matches the supplied file.
|
||||
*
|
||||
* @param string $component
|
||||
* @param stdClass $user
|
||||
* @param context $context
|
||||
* @param string $filearea
|
||||
* @param string $subdir
|
||||
* @param stored_file[] $expectedfiles
|
||||
* @param bool $addfilestozip Whether to allow files to be added to the archive
|
||||
* @param exportable_filearea $exportable
|
||||
* @return exported_item
|
||||
*/
|
||||
protected function assert_exportable_matches_file(
|
||||
string $component,
|
||||
stdClass $user,
|
||||
context $context,
|
||||
string $filearea,
|
||||
string $subdir,
|
||||
array $expectedfiles,
|
||||
bool $addfilestozip,
|
||||
exportable_filearea $exportable
|
||||
): exported_item {
|
||||
$archive = $this->getMockBuilder(zipwriter::class)
|
||||
->setConstructorArgs([$this->getMockBuilder(\ZipStream\ZipStream::class)->getmock()])
|
||||
->setMethods([
|
||||
'add_file_from_stored_file',
|
||||
'is_file_in_archive',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$archive->expects($this->any())
|
||||
->method('is_file_in_archive')
|
||||
->willReturn($addfilestozip);
|
||||
|
||||
$storedfileargs = [];
|
||||
foreach ($expectedfiles as $file) {
|
||||
$filepathinzip = $subdir . '/' . $file->get_filearea() . '/' . $file->get_filepath() . $file->get_filename();
|
||||
$filepathinzip = ltrim(preg_replace('#/+#', '/', $filepathinzip), '/');
|
||||
$storedfileargs[] = [
|
||||
$this->equalTo($context),
|
||||
$this->equalTo($filepathinzip),
|
||||
$this->equalTo($file),
|
||||
];
|
||||
}
|
||||
|
||||
$archive->expects($this->exactly(count($expectedfiles)))
|
||||
->method('add_file_from_stored_file')
|
||||
->withConsecutive(...$storedfileargs);
|
||||
|
||||
return $exportable->add_to_archive($archive);
|
||||
}
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\exportable_items\exportable_stored_file.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use advanced_testcase;
|
||||
use context;
|
||||
use context_system;
|
||||
use core\content\export\zipwriter;
|
||||
use stdClass;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* Unit tests for the `exportable_stored_file` export item class.
|
||||
*
|
||||
* @coversDefaultClass core\content\exportable_items\exportable_stored_file
|
||||
*/
|
||||
class exportable_stored_file_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that the create_from_area_params function returns an array.
|
||||
*/
|
||||
public function test_create_from_area_params_no_files(): void {
|
||||
$exportables = exportable_stored_file::create_from_area_params(
|
||||
context_system::instance(),
|
||||
'fake',
|
||||
'filearea',
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertIsArray($exportables);
|
||||
$this->assertCount(0, $exportables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the create_from_area_params function returns a set of exportable_stored_file items, for all itemids.
|
||||
*/
|
||||
public function test_create_from_area_params_no_itemid(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
$files = array_values(array_merge($files1, $files2, $files3));
|
||||
|
||||
$exportables = exportable_stored_file::create_from_area_params($context, $component, $filearea, null);
|
||||
|
||||
$this->assertIsArray($exportables);
|
||||
$this->assertCount(3, $exportables);
|
||||
|
||||
// There should be three exportables. These are listed in order of itemid.
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$exportable = $exportables[$i];
|
||||
$file = $files[$i];
|
||||
|
||||
$this->assertInstanceOf(exportable_stored_file::class, $exportable);
|
||||
$this->assert_exportable_matches_file($component, $user, $context, $filearea, '', $file, $exportable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the create_from_area_params function returns a set of exportable_stored_file items, for the requested
|
||||
* itemid
|
||||
*/
|
||||
public function test_create_from_area_params_specified_itemid(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
|
||||
$exportables = exportable_stored_file::create_from_area_params($context, $component, $filearea, 2);
|
||||
|
||||
$this->assertIsArray($exportables);
|
||||
$this->assertCount(1, $exportables);
|
||||
|
||||
// There is only one exportable.
|
||||
$exportable = array_shift($exportables);
|
||||
$this->assertInstanceOf(exportable_stored_file::class, $exportable);
|
||||
|
||||
$file2 = reset($files2);
|
||||
$this->assert_exportable_matches_file($component, $user, $context, $filearea, '', $file2, $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the create_from_area_params function returns a set of exportable_stored_file items, for the requested
|
||||
* itemid
|
||||
*/
|
||||
public function test_create_from_area_params_in_subdir(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Setup for test.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_system::instance();
|
||||
$component = 'fake';
|
||||
$filearea = 'myfirstfilearea';
|
||||
$subdir = 'a/path/to/my/subdir';
|
||||
|
||||
$files1 = $this->create_files(context_system::instance(), $component, $filearea, 1);
|
||||
$files2 = $this->create_files(context_system::instance(), $component, $filearea, 2);
|
||||
$files3 = $this->create_files(context_system::instance(), $component, $filearea, 3);
|
||||
|
||||
$exportables = exportable_stored_file::create_from_area_params($context, $component, $filearea, 2, 2, $subdir);
|
||||
|
||||
$this->assertIsArray($exportables);
|
||||
$this->assertCount(1, $exportables);
|
||||
|
||||
// There is only one exportable.
|
||||
$exportable = array_shift($exportables);
|
||||
$this->assertInstanceOf(exportable_stored_file::class, $exportable);
|
||||
|
||||
$file2 = reset($files2);
|
||||
$this->assert_exportable_matches_file($component, $user, $context, $filearea, $subdir, $file2, $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create files for use in testing.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param int $itemid
|
||||
* @param int $count
|
||||
* @return stored_file[]
|
||||
*/
|
||||
protected function create_files(context $context, string $component, string $filearea, int $itemid, int $count = 1): array {
|
||||
$fs = get_file_storage();
|
||||
|
||||
$files = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
|
||||
$filepath = '/';
|
||||
for ($j = 0; $j < $i; $j++) {
|
||||
$filepath .= "{$j}/";
|
||||
}
|
||||
|
||||
$files[] = $fs->create_file_from_string(
|
||||
(object) [
|
||||
'contextid' => $context->id,
|
||||
'component' => $component,
|
||||
'filearea' => $filearea,
|
||||
'filepath' => $filepath,
|
||||
'filename' => "file.txt",
|
||||
'itemid' => $itemid,
|
||||
],
|
||||
"File content: {$i}"
|
||||
);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the supplied expotable matches the supplied file.
|
||||
*
|
||||
* @param string $component
|
||||
* @param stdClass $user
|
||||
* @param context $context
|
||||
* @param string $filearea
|
||||
* @param string $subdir
|
||||
* @param stored_file $file
|
||||
* @param exportable_stored_file $exportable
|
||||
*/
|
||||
protected function assert_exportable_matches_file(
|
||||
string $component,
|
||||
stdClass $user,
|
||||
context $context,
|
||||
string $filearea,
|
||||
string $subdir,
|
||||
stored_file $file,
|
||||
exportable_stored_file $exportable
|
||||
): void {
|
||||
$archive = $this->getMockBuilder(zipwriter::class)
|
||||
->setConstructorArgs([$this->getMockBuilder(\ZipStream\ZipStream::class)->getmock()])
|
||||
->setMethods([
|
||||
'add_file_from_stored_file',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$this->assertEquals($file->get_filepath() . $file->get_filename(), $exportable->get_user_visible_name());
|
||||
|
||||
$expectedfilepath = implode('/', array_filter([$subdir, $filearea, $file->get_filepath(), $file->get_filename()]));
|
||||
$expectedfilepath = preg_replace('#/+#', '/', $expectedfilepath);
|
||||
|
||||
$archive->expects($this->once())
|
||||
->method('add_file_from_stored_file')
|
||||
->with(
|
||||
$this->equalTo($context),
|
||||
$this->equalTo($expectedfilepath),
|
||||
$this->equalTo($file)
|
||||
);
|
||||
|
||||
$exportable->add_to_archive($archive);
|
||||
}
|
||||
}
|
@ -0,0 +1,376 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\exportable_items\exportable_textarea.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exportable_items;
|
||||
|
||||
use advanced_testcase;
|
||||
use context;
|
||||
use context_module;
|
||||
use context_system;
|
||||
use core\content\export\zipwriter;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Unit tests for the `exportable_textarea` export item class.
|
||||
*
|
||||
* @coversDefaultClass core\content\exportable_items\exportable_textarea
|
||||
*/
|
||||
class exportable_textarea_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that an exportable textarea which does not relate to any content, does not attempt to export any content.
|
||||
*/
|
||||
public function test_valid_table_without_content(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$context = context_system::instance();
|
||||
$component = 'page';
|
||||
$uservisiblename = 'Page content';
|
||||
$tablename = 'page';
|
||||
$fieldname = 'content';
|
||||
$fieldid = -1;
|
||||
$formatfieldname = 'contentformat';
|
||||
$filearea = 'content';
|
||||
|
||||
$exportable = new exportable_textarea(
|
||||
$context,
|
||||
$component,
|
||||
$uservisiblename,
|
||||
$tablename,
|
||||
$fieldname,
|
||||
$fieldid,
|
||||
$formatfieldname
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(exportable_textarea::class, $exportable);
|
||||
|
||||
$this->assert_exportable_empty($component, $user, $context, $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the an exportable textarea exports content from the appropriate locations, but without any files.
|
||||
*/
|
||||
public function test_valid_table_with_content_no_filearea_specified(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$content = '<h1>Hello</h1><p>World!</p>';
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$page = $this->getDataGenerator()->create_module('page', (object) [
|
||||
'course' => $course,
|
||||
'content' => $content,
|
||||
'contentformat' => FORMAT_HTML,
|
||||
]);
|
||||
|
||||
$context = context_module::instance($page->cmid);
|
||||
$expectedfiles = $this->create_files($context, 'mod_page', 'content', (int) $page->id, 5);
|
||||
|
||||
// Unexpected files.
|
||||
$this->create_files($context, 'mod_page', 'content', (int) $page->id + 1, 5);
|
||||
$this->create_files($context, 'mod_page', 'othercontent', (int) $page->id, 5);
|
||||
$this->create_files($context, 'mod_foo', 'content', (int) $page->id, 5);
|
||||
|
||||
$component = 'page';
|
||||
$uservisiblename = 'Page content';
|
||||
$tablename = 'page';
|
||||
$fieldname = 'content';
|
||||
$fieldid = (int) $page->id;
|
||||
$formatfieldname = 'contentformat';
|
||||
|
||||
$exportable = new exportable_textarea(
|
||||
$context,
|
||||
$component,
|
||||
$uservisiblename,
|
||||
$tablename,
|
||||
$fieldname,
|
||||
$fieldid,
|
||||
$formatfieldname
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(exportable_textarea::class, $exportable);
|
||||
|
||||
// Although files exist, the filearea and itemid were not included.
|
||||
$this->assert_exportable_matches_file($component, $user, $context, null, $content, [], '', $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the an exportable textarea exports content from the appropriate locations, but without any files.
|
||||
*/
|
||||
public function test_valid_table_with_content_no_itemid_specified(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$content = '<h1>Hello</h1><p>World!</p>';
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$page = $this->getDataGenerator()->create_module('page', (object) [
|
||||
'course' => $course,
|
||||
'content' => $content,
|
||||
'contentformat' => FORMAT_HTML,
|
||||
]);
|
||||
|
||||
$context = context_module::instance($page->cmid);
|
||||
$expectedfiles = $this->create_files($context, 'mod_page', 'content', (int) $page->id, 5);
|
||||
|
||||
// Unexpected files.
|
||||
$this->create_files($context, 'mod_page', 'content', (int) $page->id + 1, 5);
|
||||
$this->create_files($context, 'mod_page', 'othercontent', (int) $page->id, 5);
|
||||
$this->create_files($context, 'mod_foo', 'content', (int) $page->id, 5);
|
||||
|
||||
$component = 'page';
|
||||
$uservisiblename = 'Page content';
|
||||
$tablename = 'page';
|
||||
$fieldname = 'content';
|
||||
$fieldid = (int) $page->id;
|
||||
$formatfieldname = 'contentformat';
|
||||
$filearea = 'content';
|
||||
|
||||
$exportable = new exportable_textarea(
|
||||
$context,
|
||||
$component,
|
||||
$uservisiblename,
|
||||
$tablename,
|
||||
$fieldname,
|
||||
$fieldid,
|
||||
$formatfieldname,
|
||||
$filearea
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(exportable_textarea::class, $exportable);
|
||||
|
||||
// Although files exist, the filearea and itemid were not included.
|
||||
$this->assert_exportable_matches_file($component, $user, $context, null, $content, [], '', $exportable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the an exportable textarea exports content from the appropriate locations, with files.
|
||||
*/
|
||||
public function test_valid_table_with_content_and_files(): void {
|
||||
$this->resetAfterTest(true);
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$contentin = <<<EOF
|
||||
<h1>Hello</h1><p>World!</p>
|
||||
<img src='@@PLUGINFILE@@/file.txt'>
|
||||
<img src='@@PLUGINFILE@@/other/file.txt'>
|
||||
EOF;
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$page = $this->getDataGenerator()->create_module('page', (object) [
|
||||
'course' => $course,
|
||||
'content' => $contentin,
|
||||
'contentformat' => FORMAT_HTML,
|
||||
]);
|
||||
|
||||
$this->setUser($user);
|
||||
|
||||
$context = context_module::instance($page->cmid);
|
||||
$expectedfiles = $this->create_files(
|
||||
$context,
|
||||
'mod_page',
|
||||
'content',
|
||||
(int) $page->id,
|
||||
5,
|
||||
'contentformat',
|
||||
'content',
|
||||
(int) $page->id,
|
||||
5
|
||||
);
|
||||
|
||||
// Unexpected files.
|
||||
$this->create_files($context, 'mod_page', 'content', (int) $page->id + 1, 5);
|
||||
$this->create_files($context, 'mod_page', 'othercontent', (int) $page->id, 5);
|
||||
$this->create_files($context, 'mod_foo', 'content', (int) $page->id, 5);
|
||||
|
||||
$component = 'mod_page';
|
||||
$uservisiblename = 'Page content';
|
||||
$tablename = 'page';
|
||||
$fieldname = 'content';
|
||||
$fieldid = (int) $page->id;
|
||||
$formatfieldname = 'contentformat';
|
||||
$filearea = 'content';
|
||||
$itemid = (int) $page->id;
|
||||
|
||||
$exportable = new exportable_textarea(
|
||||
$context,
|
||||
$component,
|
||||
$uservisiblename,
|
||||
$tablename,
|
||||
$fieldname,
|
||||
$fieldid,
|
||||
$formatfieldname,
|
||||
$filearea,
|
||||
$itemid,
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(exportable_textarea::class, $exportable);
|
||||
|
||||
$pluginfilebase = moodle_url::make_pluginfile_url(
|
||||
$context->id, $component, $filearea, null, '', '', false, true
|
||||
)->out(false);
|
||||
$expectedcontent = <<<EOF
|
||||
<h1>Hello</h1><p>World!</p>
|
||||
<img src='content/file.txt'>
|
||||
<img src='{$pluginfilebase}/other/file.txt'>
|
||||
EOF;
|
||||
|
||||
// Although files exist, the filearea and itemid were not included.
|
||||
$this->assert_exportable_matches_file(
|
||||
$component, $user, $context, $filearea, $expectedcontent, $expectedfiles, '', $exportable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create files for use in testing.
|
||||
*
|
||||
* @param context $context
|
||||
* @param string $component
|
||||
* @param string $filearea
|
||||
* @param int $itemid
|
||||
* @param int $count
|
||||
* @return stored_file[]
|
||||
*/
|
||||
protected function create_files(context $context, string $component, string $filearea, int $itemid, int $count = 1): array {
|
||||
$fs = get_file_storage();
|
||||
|
||||
$files = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
|
||||
$filepath = '/';
|
||||
for ($j = 0; $j < $i; $j++) {
|
||||
$filepath .= "{$j}/";
|
||||
}
|
||||
|
||||
$files[] = $fs->create_file_from_string(
|
||||
(object) [
|
||||
'contextid' => $context->id,
|
||||
'component' => $component,
|
||||
'filearea' => $filearea,
|
||||
'filepath' => $filepath,
|
||||
'filename' => "file.txt",
|
||||
'itemid' => $itemid,
|
||||
],
|
||||
"File content: {$i}"
|
||||
);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the supplied expotable matches the supplied file.
|
||||
*
|
||||
* @param string $component
|
||||
* @param stdClass $user
|
||||
* @param context $context
|
||||
* @param string $filearea
|
||||
* @param string $content
|
||||
* @param stored_file[] $expectedfiles
|
||||
* @param string $subdir
|
||||
* @param exportable_textarea $exportable
|
||||
*/
|
||||
protected function assert_exportable_matches_file(
|
||||
string $component,
|
||||
stdClass $user,
|
||||
context $context,
|
||||
?string $filearea,
|
||||
string $content,
|
||||
array $expectedfiles,
|
||||
string $subdir,
|
||||
exportable_textarea $exportable
|
||||
): void {
|
||||
$archive = $this->getMockBuilder(zipwriter::class)
|
||||
->setConstructorArgs([$this->getMockBuilder(\ZipStream\ZipStream::class)->getmock()])
|
||||
->setMethods([
|
||||
'is_file_in_archive',
|
||||
'add_file_from_string',
|
||||
'add_file_from_stored_file',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$archive->expects($this->any())
|
||||
->method('is_file_in_archive')
|
||||
->willReturn(true);
|
||||
|
||||
$storedfileargs = [];
|
||||
foreach ($expectedfiles as $file) {
|
||||
$filepathinzip = dirname($subdir) . $file->get_filearea() . '/' . $file->get_filepath() . $file->get_filename();
|
||||
$filepathinzip = ltrim(preg_replace('#/+#', '/', $filepathinzip), '/');
|
||||
$storedfileargs[] = [
|
||||
$this->equalTo($context),
|
||||
$this->equalTo($filepathinzip),
|
||||
$this->equalTo($file),
|
||||
];
|
||||
}
|
||||
|
||||
$archive->expects($this->exactly(count($expectedfiles)))
|
||||
->method('add_file_from_stored_file')
|
||||
->withConsecutive(...$storedfileargs);
|
||||
|
||||
$archive->expects($this->never())
|
||||
->method('add_file_from_string');
|
||||
|
||||
$exportable->add_to_archive($archive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the supplied expotable matches the supplied file.
|
||||
*
|
||||
* @param string $component
|
||||
* @param stdClass $user
|
||||
* @param context $context
|
||||
* @param exportable_textarea $exportable
|
||||
*/
|
||||
protected function assert_exportable_empty(
|
||||
string $component,
|
||||
stdClass $user,
|
||||
context $context,
|
||||
exportable_textarea $exportable
|
||||
): void {
|
||||
$archive = $this->getMockBuilder(zipwriter::class)
|
||||
->setConstructorArgs([$this->getMockBuilder(\ZipStream\ZipStream::class)->getmock()])
|
||||
->setMethods([
|
||||
'add_file_from_stored_file',
|
||||
'add_file_from_string',
|
||||
'add_file_from_template',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$archive->expects($this->never())
|
||||
->method('add_file_from_stored_file');
|
||||
$archive->expects($this->never())
|
||||
->method('add_file_from_string');
|
||||
$archive->expects($this->never())
|
||||
->method('add_file_from_template');
|
||||
|
||||
$exportable->add_to_archive($archive);
|
||||
}
|
||||
}
|
155
lib/tests/content/export/exporters/course_exporter_test.php
Normal file
155
lib/tests/content/export/exporters/course_exporter_test.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\export\exporters\course_exporter.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export\exporters;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_course;
|
||||
use context_module;
|
||||
use ZipArchive;
|
||||
use core\content\export\zipwriter;
|
||||
|
||||
/**
|
||||
* Unit tests for activity exporter.
|
||||
*
|
||||
* @coversDefaultClass \core\content\export\exporters\course_exporter
|
||||
*/
|
||||
class course_exporter_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* The course_exporter should still export a module intro when no exportables are passed.
|
||||
*/
|
||||
public function test_no_exportables_exported(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$course = $generator->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
$intro = 'XX Some introduction should go here XX';
|
||||
$content = 'YY Some content should go here YY';
|
||||
$module = $generator->create_module('page', [
|
||||
'course' => $course->id,
|
||||
'intro' => $intro,
|
||||
'content' => $content,
|
||||
]);
|
||||
$modcontext = context_module::instance($module->cmid);
|
||||
|
||||
$user = $generator->create_user();
|
||||
$generator->enrol_user($user->id, $course->id);
|
||||
|
||||
// Only the module index should be added.
|
||||
$archive = $this->get_mocked_zipwriter(['add_file_from_string']);
|
||||
$archive->expects($this->once())
|
||||
->method('add_file_from_string')
|
||||
->with(
|
||||
$modcontext,
|
||||
'index.html',
|
||||
$this->callback(function($html) use ($intro, $content): bool {
|
||||
if (strpos($html, $intro) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($html, $content) !== false) {
|
||||
// The content as not exported.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
$archive->set_root_context($coursecontext);
|
||||
|
||||
$coursecontroller = new course_exporter($modcontext->get_course_context(), $user, $archive);
|
||||
$coursecontroller->export_mod_content($modcontext, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* The course_exporter should still export exportables as well as module intro.
|
||||
*/
|
||||
public function test_exportables_exported(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$course = $generator->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
$intro = 'XX Some introduction should go here XX';
|
||||
$content = 'YY Some content should go here YY';
|
||||
$module = $generator->create_module('page', [
|
||||
'course' => $course->id,
|
||||
'intro' => $intro,
|
||||
'content' => $content,
|
||||
]);
|
||||
$modcontext = context_module::instance($module->cmid);
|
||||
|
||||
$user = $generator->create_user();
|
||||
$generator->enrol_user($user->id, $course->id);
|
||||
|
||||
// Only the module index should be added.
|
||||
$archive = $this->get_mocked_zipwriter(['add_file_from_string']);
|
||||
$archive->expects($this->once())
|
||||
->method('add_file_from_string')
|
||||
->with(
|
||||
$modcontext,
|
||||
'index.html',
|
||||
$this->callback(function($html) use ($intro, $content): bool {
|
||||
if (strpos($html, $intro) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($html, $content) === false) {
|
||||
// Content was exported.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
$archive->set_root_context($coursecontext);
|
||||
|
||||
$pagecontroller = new \mod_page\content\exporter($modcontext, "mod_page", $user, $archive);
|
||||
|
||||
$coursecontroller = new course_exporter($modcontext->get_course_context(), $user, $archive);
|
||||
$coursecontroller->export_mod_content($modcontext, $pagecontroller->get_exportables());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a mocked zipwriter instance, stubbing the supplieid classes.
|
||||
*
|
||||
* @param string[] $methods
|
||||
* @return zipwriter
|
||||
*/
|
||||
protected function get_mocked_zipwriter(?array $methods = []): zipwriter {
|
||||
return $this->getMockBuilder(zipwriter::class)
|
||||
->setConstructorArgs([$this->getMockBuilder(\ZipStream\ZipStream::class)->getmock()])
|
||||
->setMethods($methods)
|
||||
->getMock();
|
||||
}
|
||||
}
|
119
lib/tests/content/export/zipwriter_test.php
Normal file
119
lib/tests/content/export/zipwriter_test.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\zipwriter.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\content\export;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_module;
|
||||
use context_system;
|
||||
use ZipArchive;
|
||||
|
||||
/**
|
||||
* Unit tests for core\content\zipwriter.
|
||||
*
|
||||
* @coversDefaultClass \core\content\export\zipwriter
|
||||
*/
|
||||
class zipwriter_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test add_file_from_stored_file().
|
||||
*/
|
||||
public function test_add_file_from_stored_file(): void {
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$folder = $this->getDataGenerator()->create_module('folder', ['course' => $course->id]);
|
||||
$context = \context_course::instance($course->id);
|
||||
|
||||
// Add a file to the intro.
|
||||
$fileintroname = "fileintro.txt";
|
||||
$filerecord = [
|
||||
'contextid' => context_module::instance($folder->cmid)->id,
|
||||
'component' => 'mod_folder',
|
||||
'filearea' => 'intro',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => $fileintroname,
|
||||
];
|
||||
$fs = get_file_storage();
|
||||
$storedfile = $fs->create_file_from_string($filerecord, 'image contents');
|
||||
|
||||
$pathinfolder = $storedfile->get_filepath() . $storedfile->get_filename();
|
||||
|
||||
$zipwriter = zipwriter::get_file_writer('test.zip');
|
||||
$zipwriter->add_file_from_stored_file($context, $pathinfolder, $storedfile);
|
||||
$zipwriter->finish();
|
||||
|
||||
$zipfilepath = $zipwriter->get_file_path();
|
||||
$zip = new ZipArchive();
|
||||
$opened = $zip->open($zipfilepath);
|
||||
$this->assertTrue($opened);
|
||||
|
||||
$pathinzip = $zipwriter->get_context_path($context, $pathinfolder);
|
||||
$this->assertEquals($storedfile->get_content(), $zip->getFromName($pathinzip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add_file_from_string().
|
||||
*/
|
||||
public function test_add_file_from_string(): void {
|
||||
$context = context_system::instance();
|
||||
|
||||
$pathinfolder = "/path/to/my/file.txt";
|
||||
$mycontent = "Zippidy do dah";
|
||||
|
||||
$zipwriter = zipwriter::get_file_writer('test.zip');
|
||||
$zipwriter->add_file_from_string($context, $pathinfolder, $mycontent);
|
||||
$zipwriter->finish();
|
||||
|
||||
$zipfilepath = $zipwriter->get_file_path();
|
||||
$zip = new ZipArchive();
|
||||
$opened = $zip->open($zipfilepath);
|
||||
$this->assertTrue($opened);
|
||||
|
||||
$pathinzip = ltrim($zipwriter->get_context_path($context, $pathinfolder), '/');
|
||||
$this->assertEquals($mycontent, $zip->getFromName($pathinzip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_file_writer().
|
||||
*/
|
||||
public function test_get_file_writer(): void {
|
||||
$zipwriter = zipwriter::get_file_writer('test.zip');
|
||||
$this->assertInstanceOf(zipwriter::class, $zipwriter);
|
||||
$this->assertTrue(file_exists($zipwriter->get_file_path()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_stream_writer().
|
||||
*/
|
||||
public function test_get_stream_writer(): void {
|
||||
$zipwriter = zipwriter::get_stream_writer('test.zip');
|
||||
$this->assertInstanceOf(zipwriter::class, $zipwriter);
|
||||
}
|
||||
}
|
61
mod/folder/classes/content/exporter.php
Normal file
61
mod/folder/classes/content/exporter.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Content export definition.
|
||||
*
|
||||
* @package mod_folder
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace mod_folder\content;
|
||||
|
||||
use core\content\export\exportable_items\exportable_filearea;
|
||||
use core\content\export\exporters\abstract_mod_exporter;
|
||||
|
||||
/**
|
||||
* A class which assists a component to export content.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exporter extends abstract_mod_exporter {
|
||||
|
||||
/**
|
||||
* Get the exportable items for mod_folder.
|
||||
*
|
||||
* @param bool $includeuserdata Whether to include user data, in addition to shared content.
|
||||
* @return \core\content\export\exportable_item[]
|
||||
*/
|
||||
public function get_exportables(bool $includeuserdata = false): array {
|
||||
$contentitems = [];
|
||||
|
||||
$contentitems[] = new exportable_filearea(
|
||||
$this->get_context(),
|
||||
$this->get_component(),
|
||||
get_string('foldercontent', 'mod_folder'),
|
||||
|
||||
// The files held in mod_folder are stored in the 'content' filearea, under itemid 0.
|
||||
'content',
|
||||
0,
|
||||
|
||||
// The itemid is used in the URL when accessing.
|
||||
0
|
||||
);
|
||||
|
||||
return $contentitems;
|
||||
}
|
||||
}
|
67
mod/page/classes/content/exporter.php
Normal file
67
mod/page/classes/content/exporter.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Content export definition.
|
||||
*
|
||||
* @package mod_page
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace mod_page\content;
|
||||
|
||||
use core\content\export\exportable_items\exportable_textarea;
|
||||
use core\content\export\exporters\abstract_mod_exporter;
|
||||
|
||||
/**
|
||||
* A class which assists a component to export content.
|
||||
*
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exporter extends abstract_mod_exporter {
|
||||
|
||||
/**
|
||||
* Get the exportable items for mod_page.
|
||||
*
|
||||
* @param bool $includeuserdata Whether to include user data, in addition to shared content.
|
||||
* @return \core\content\export\exportable_item[]
|
||||
*/
|
||||
public function get_exportables(bool $includeuserdata = false): array {
|
||||
$contentitems = [];
|
||||
|
||||
$contentitems[] = new exportable_textarea(
|
||||
$this->get_context(),
|
||||
$this->get_component(),
|
||||
get_string('content', 'mod_page'),
|
||||
|
||||
// Content is in the 'content' field of the 'page' table.
|
||||
$this->get_modname(),
|
||||
'content',
|
||||
|
||||
// The record ID in the database is the CMID.
|
||||
$this->cm->instance,
|
||||
'contentformat',
|
||||
|
||||
// The mod_page content has files in 'content/0', and the itemid (0) is present in the URL.
|
||||
'content',
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
return $contentitems;
|
||||
}
|
||||
}
|
61
mod/resource/classes/content/exporter.php
Normal file
61
mod/resource/classes/content/exporter.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Content export definition.
|
||||
*
|
||||
* @package mod_resource
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace mod_resource\content;
|
||||
|
||||
use core\content\export\exportable_items\exportable_filearea;
|
||||
use core\content\export\exporters\abstract_mod_exporter;
|
||||
|
||||
/**
|
||||
* A class which assists a component to export content.
|
||||
*
|
||||
* @copyright 2020 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exporter extends abstract_mod_exporter {
|
||||
|
||||
/**
|
||||
* Get the exportable items for mod_resource.
|
||||
*
|
||||
* @param bool $includeuserdata Whether to include user data, in addition to shared content.
|
||||
* @return \core\content\export\exportable_item[]
|
||||
*/
|
||||
public function get_exportables(bool $includeuserdata = false): array {
|
||||
$contentitems = [];
|
||||
|
||||
$contentitems[] = new exportable_filearea(
|
||||
$this->get_context(),
|
||||
$this->get_component(),
|
||||
get_string('resourcecontent', 'mod_resource'),
|
||||
|
||||
// The files held in mod_resource are stored in the 'content' filearea, under itemid 0.
|
||||
'content',
|
||||
0,
|
||||
|
||||
// The itemid is used in the URL when accessing.
|
||||
0
|
||||
);
|
||||
|
||||
return $contentitems;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user