diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php
index bd759a706cd..1dfdd6a8df1 100644
--- a/admin/settings/plugins.php
+++ b/admin/settings/plugins.php
@@ -230,6 +230,19 @@ if ($hassiteconfig) {
300, PARAM_INT, 10));
$ADMIN->add('mediaplayers', $temp);
+ // Convert plugins.
+ $ADMIN->add('modules', new admin_category('fileconverterplugins', new lang_string('type_fileconverter_plural', 'plugin')));
+ $temp = new admin_settingpage('managefileconverterplugins', new lang_string('type_fileconverter', 'plugin'));
+ $temp->add(new admin_setting_manage_fileconverter_plugins());
+ $ADMIN->add('fileconverterplugins', $temp);
+
+ $plugins = core_plugin_manager::instance()->get_plugins_of_type('fileconverter');
+ core_collator::asort_objects_by_property($plugins, 'displayname');
+ foreach ($plugins as $plugin) {
+ /** @var \core\plugininfo\media $plugin */
+ $plugin->load_settings($ADMIN, 'fileconverterplugins', $hassiteconfig);
+ }
+
$plugins = core_plugin_manager::instance()->get_plugins_of_type('media');
core_collator::asort_objects_by_property($plugins, 'displayname');
foreach ($plugins as $plugin) {
diff --git a/admin/templates/setting_manage_plugins.mustache b/admin/templates/setting_manage_plugins.mustache
index 65d941d86a9..5c38e51513f 100644
--- a/admin/templates/setting_manage_plugins.mustache
+++ b/admin/templates/setting_manage_plugins.mustache
@@ -20,28 +20,33 @@
{{info}}
{{/infocolumnname}}
-
+ |
{{#moveuplink}}
- {{#moveupicon}}{{>core/pix_icon}}{{/moveupicon}}
+ {{#pix}}t/up, moodle, {{#str}} up, moodle {{/str}}{{/pix}}
{{/moveuplink}}
{{^moveuplink}}
- {{#spacericon}}{{>core/pix_icon}}{{/spacericon}}
+ {{#pix}}spacer, moodle{{/pix}}
{{/moveuplink}}
{{#movedownlink}}
- {{#movedownicon}}{{>core/pix_icon}}{{/movedownicon}}
+ {{#pix}}t/down, moodle, {{#str}} down, moodle {{/str}}{{/pix}}
{{/movedownlink}}
{{^movedownlink}}
- {{#spacericon}}{{>core/pix_icon}}{{/spacericon}}
+ {{#pix}}spacer, moodle{{/pix}}
{{/movedownlink}}
|
- {{#toggleicon}}{{>core/pix_icon}}{{/toggleicon}}
+ {{#toggletarget}}
+ {{#pix}}i/show, moodle, {{#str}} enable, moodle {{/str}}{{/pix}}
+ {{/toggletarget}}
+ {{^toggletarget}}
+ {{#pix}}i/hide, moodle, {{#str}} disable, moodle {{/str}}{{/pix}}
+ {{/toggletarget}}
|
diff --git a/files/classes/conversion.php b/files/classes/conversion.php
new file mode 100644
index 00000000000..07872ce40a2
--- /dev/null
+++ b/files/classes/conversion.php
@@ -0,0 +1,360 @@
+.
+
+/**
+ * Classes for converting files between different file formats.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_files;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stored_file;
+
+/**
+ * Class representing a conversion currently in progress.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class conversion extends \core\persistent {
+
+ /**
+ * Status value representing a conversion waiting to start.
+ */
+ const STATUS_PENDING = 0;
+
+ /**
+ * Status value representing a conversion in progress.
+ */
+ const STATUS_IN_PROGRESS = 1;
+
+ /**
+ * Status value representing a successful conversion.
+ */
+ const STATUS_COMPLETE = 2;
+
+ /**
+ * Status value representing a failed conversion.
+ */
+ const STATUS_FAILED = -1;
+
+ /**
+ * Table name for this persistent.
+ */
+ const TABLE = 'file_conversion';
+
+ protected static function define_properties() {
+ return array(
+ 'sourcefileid' => [
+ 'type' => PARAM_INT,
+ ],
+ 'targetformat' => [
+ 'type' => PARAM_ALPHANUMEXT,
+ ],
+ 'status' => [
+ 'type' => PARAM_INT,
+ 'choices' => [
+ self::STATUS_PENDING,
+ self::STATUS_IN_PROGRESS,
+ self::STATUS_COMPLETE,
+ self::STATUS_FAILED,
+ ],
+ 'default' => self::STATUS_PENDING,
+ ],
+ 'statusmessage' => [
+ 'type' => PARAM_RAW,
+ 'null' => NULL_ALLOWED,
+ 'default' => null,
+ ],
+ 'converter' => [
+ 'type' => PARAM_RAW,
+ 'null' => NULL_ALLOWED,
+ 'default' => null,
+ ],
+ 'destfileid' => [
+ 'type' => PARAM_INT,
+ 'null' => NULL_ALLOWED,
+ 'default' => null,
+ ],
+ 'data' => [
+ 'type' => PARAM_RAW,
+ 'null' => NULL_ALLOWED,
+ 'default' => null,
+ ],
+ );
+ }
+
+ /**
+ * Fetch all conversions relating to the specified file.
+ *
+ * Only conversions which have a valid file are returned.
+ *
+ * @param stored_file $file The source file being converted
+ * @param string $format The targetforamt to filter to
+ * @return conversion[]
+ */
+ public static function get_conversions_for_file(stored_file $file, $format) {
+ global $DB;
+ $instances = [];
+
+ // Conversion records are intended for tracking a conversion in progress or recently completed.
+ // The record is removed periodically, but the destination file is not.
+ // We need to fetch all conversion records which match the source file and target, and also all source and
+ // destination files which do not have a conversion record.
+ $sqlfields = self::get_sql_fields('c', 'conversion');
+
+ // Fetch actual conversions which relate to the specified source file, and have a matching conversion record,
+ // and either have a valid destination file which still exists, or do not have a destination file at all.
+ $sql = "SELECT {$sqlfields}
+ FROM {" . self::TABLE . "} c
+ INNER JOIN {files} conversionsourcefile ON conversionsourcefile.id = c.sourcefileid
+ LEFT JOIN {files} conversiondestfile ON conversiondestfile.id = c.destfileid
+ WHERE
+ conversionsourcefile.contenthash = :ccontenthash
+ AND c.targetformat = :cformat
+ AND (
+ c.destfileid IS NULL OR conversiondestfile.id IS NOT NULL
+ )";
+
+ // Fetch a empty conversion record for each source/destination combination that we find to match where the
+ // destination file is in the correct filearea/filepath/filename combination to meet the requirements.
+ // This ensures that existing conversions are used where possible, even if there is no 'conversion' record for
+ // them.
+ $sql .= "
+ UNION ALL
+ SELECT
+ NULL AS conversionid,
+ orphanedsourcefile.id AS conversionsourcefileid,
+ :oformat AS conversiontargetformat,
+ 2 AS conversionstatus,
+ NULL AS conversionstatusmessage,
+ NULL AS conversionconverter,
+ orphaneddestfile.id AS conversiondestfileid,
+ NULL AS conversiondata,
+ 0 AS conversiontimecreated,
+ 0 AS conversiontimemodified,
+ 0 AS conversionusermodified
+ FROM {files} orphanedsourcefile
+ INNER JOIN {files} orphaneddestfile ON (
+ orphaneddestfile.filename = orphanedsourcefile.contenthash
+ AND orphaneddestfile.component = 'core'
+ AND orphaneddestfile.filearea = 'documentconversion'
+ AND orphaneddestfile.filepath = :ofilepath
+ )
+ LEFT JOIN {" . self::TABLE . "} orphanedconversion ON orphanedconversion.destfileid = orphaneddestfile.id
+ WHERE
+ orphanedconversion.id IS NULL
+ AND
+ orphanedsourcefile.id = :osourcefileid
+ ";
+ $records = $DB->get_records_sql($sql, [
+ 'ccontenthash' => $file->get_contenthash(),
+ 'osourcefileid' => $file->get_id(),
+ 'cfilepath' => "/{$format}/",
+ 'ofilepath' => "/{$format}/",
+ 'cformat' => $format,
+ 'oformat' => $format,
+ ]);
+
+ foreach ($records as $record) {
+ $data = self::extract_record($record, 'conversion');
+ $newrecord = new static(0, $data);
+ $instances[] = $newrecord;
+ }
+
+ return $instances;
+ }
+
+ /**
+ * Remove all old conversion records.
+ */
+ public static function remove_old_conversion_records() {
+ global $DB;
+
+ $DB->delete_records_select(self::TABLE, 'timemodified <= :weekagosecs', [
+ 'weekagosecs' => time() - WEEKSECS,
+ ]);
+ }
+
+ /**
+ * Set the source file id for the conversion.
+ *
+ * @param stored_file $file The file to convert
+ * @return $this
+ */
+ public function set_sourcefile(stored_file $file) {
+ $this->raw_set('sourcefileid', $file->get_id());
+
+ return $this;
+ }
+
+ /**
+ * Fetch the source file.
+ *
+ * @return stored_file|false
+ */
+ public function get_sourcefile() {
+ $fs = get_file_storage();
+
+ return $fs->get_file_by_id($this->get('sourcefileid'));
+ }
+
+ /**
+ * Set the destination file for this conversion.
+ *
+ * @param string $filepath The path to the converted file
+ * @return $this
+ */
+ public function store_destfile_from_path($filepath) {
+ if ($record = $this->get_file_record()) {
+ $fs = get_file_storage();
+ $existing = $fs->get_file(
+ $record['contextid'],
+ $record['component'],
+ $record['filearea'],
+ $record['itemid'],
+ $record['filepath'],
+ $record['filename']
+ );
+ if ($existing) {
+ $existing->delete();
+ }
+ $file = $fs->create_file_from_pathname($record, $filepath);
+
+ $this->raw_set('destfileid', $file->get_id());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the destination file for this conversion.
+ *
+ * @param string $content The content of the converted file
+ * @return $this
+ */
+ public function store_destfile_from_string($content) {
+ if ($record = $this->get_file_record()) {
+ $fs = get_file_storage();
+ $existing = $fs->get_file(
+ $record['contextid'],
+ $record['component'],
+ $record['filearea'],
+ $record['itemid'],
+ $record['filepath'],
+ $record['filename']
+ );
+ if ($existing) {
+ $existing->delete();
+ }
+ $file = $fs->create_file_from_string($record, $content);
+
+ $this->raw_set('destfileid', $file->get_id());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the destination file.
+ *
+ * @return stored_file|this
+ */
+ public function get_destfile() {
+ $fs = get_file_storage();
+
+ return $fs->get_file_by_id($this->get('destfileid'));
+ }
+
+ /**
+ * Helper to ensure that the returned status is always an int.
+ *
+ * @return int
+ */
+ protected function get_status() {
+ return (int) $this->raw_get('status');
+ }
+
+ /**
+ * Get an instance of the current converter.
+ *
+ * @return converter_interface|false
+ */
+ public function get_converter_instance() {
+ $currentconverter = $this->get('converter');
+
+ if ($currentconverter && class_exists($currentconverter)) {
+ return new $currentconverter();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Transform data into a storable format.
+ *
+ * @param stdClass $data The data to be stored
+ * @return $this
+ */
+ protected function set_data($data) {
+ $this->raw_set('data', json_encode($data));
+
+ return $this;
+ }
+
+ /**
+ * Transform data into a storable format.
+ *
+ * @return stdClass The stored data
+ */
+ protected function get_data() {
+ $data = $this->raw_get('data');
+
+ if (!empty($data)) {
+ return json_decode($data);
+ }
+
+ return (object) [];
+ }
+
+ /**
+ * Return the file record base for use in the files table.
+ *
+ * @return array
+ */
+ protected function get_file_record() {
+ $file = $this->get_sourcefile();
+
+ if (!$file) {
+ // If the source file was removed before we completed, we must return early.
+ return false;
+ }
+
+ return [
+ 'contextid' => \context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'itemid' => 0,
+ 'filepath' => "/" . $this->get('targetformat') . "/",
+ 'filename' => $file->get_contenthash(),
+ ];
+ }
+}
diff --git a/files/classes/converter.php b/files/classes/converter.php
new file mode 100644
index 00000000000..d39d84f0733
--- /dev/null
+++ b/files/classes/converter.php
@@ -0,0 +1,247 @@
+.
+
+/**
+ * Class for converting files between different file formats using unoconv.
+ *
+ * @package core_files
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_files;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stored_file;
+
+/**
+ * Class for converting files between different formats using unoconv.
+ *
+ * @package core_files
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class converter {
+
+ /**
+ * Get a list of enabled plugins and classes.
+ *
+ * @return array
+ */
+ protected function get_enabled_plugins() {
+ $plugins = \core\plugininfo\fileconverter::get_enabled_plugins();
+
+ $pluginclasses = [];
+ foreach ($plugins as $plugin) {
+ $pluginclasses[$plugin] = \core\plugininfo\fileconverter::get_classname($plugin);
+ }
+
+ return $pluginclasses;
+ }
+
+ /**
+ * Return the file_storage API.
+ *
+ * This allows for mocking of the file_storage API.
+ *
+ * @return file_storage
+ */
+ protected function get_file_storage() {
+ return get_file_storage();
+ }
+
+ /**
+ * Start the conversion for a stored_file into a new format.
+ *
+ * @param stored_file $file The file to convert
+ * @param string $format The desired target file format (file extension)
+ * @param boolean $forcerefresh If true, the file will be converted every time (not cached).
+ * @return conversion
+ */
+ public function start_conversion(stored_file $file, $format, $forcerefresh = false) {
+ $conversions = conversion::get_conversions_for_file($file, $format);
+
+ if ($forcerefresh || count($conversions) > 1) {
+ while ($conversion = array_shift($conversions)) {
+ if ($conversion->get('id')) {
+ $conversion->delete();
+ }
+ }
+ }
+
+ if (empty($conversions)) {
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $file->get_id(),
+ 'targetformat' => $format,
+ ]);
+ $conversion->create();
+ } else {
+ $conversion = array_shift($conversions);
+ }
+
+ if ($conversion->get('status') !== conversion::STATUS_COMPLETE) {
+ $this->poll_conversion($conversion);
+ }
+
+ return $conversion;
+ }
+
+ /**
+ * Poll for updates to the supplied conversion.
+ *
+ * @param conversion $conversion The conversion in progress
+ * @return $this
+ */
+ public function poll_conversion(conversion $conversion) {
+ $format = $conversion->get('targetformat');
+ $file = $conversion->get_sourcefile();
+
+ if ($conversion->get('status') == conversion::STATUS_IN_PROGRESS) {
+ // The current conversion is in progress.
+ // Check for updates.
+ if ($instance = $conversion->get_converter_instance()) {
+ $instance->poll_conversion_status($conversion);
+ } else {
+ // Unable to fetch the converter instance.
+ // Reset the status back to PENDING so that it may be picked up again.
+ $conversion->set('status', conversion::STATUS_PENDING);
+ $conversion->update();
+ }
+ }
+
+ // Refresh the status.
+ $status = $conversion->get('status');
+ if ($status === conversion::STATUS_PENDING || $status === conversion::STATUS_FAILED) {
+ // The current status is either pending or failed.
+ // Attempt to pick up a new converter and convert the document.
+ $from = \core_filetypes::get_file_extension($file->get_mimetype());
+ $converters = $this->get_document_converter_classes($from, $format);
+ $currentconverter = $this->get_next_converter($converters, $conversion->get('converter'));
+
+ if (!$currentconverter) {
+ // No more converters available.
+ $conversion->set('status', conversion::STATUS_FAILED);
+ return $this;
+ }
+
+ do {
+ $conversion
+ ->set('converter', $currentconverter)
+ ->set('status', conversion::STATUS_IN_PROGRESS)
+ ->update();
+
+ $instance = $conversion->get_converter_instance();
+ $instance->start_document_conversion($conversion);
+ $failed = $conversion->get('status') === conversion::STATUS_FAILED;
+ $currentconverter = $this->get_next_converter($converters, $currentconverter);
+ } while ($failed && $currentconverter);
+
+ $conversion->update();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Fetch the next converter to try.
+ *
+ * @param array $converters The list of converters to try
+ * @param string|null $currentconverter The converter currently in use
+ * @return string|false
+ */
+ protected function get_next_converter($converters, $currentconverter = null) {
+ if ($currentconverter) {
+ $keys = array_keys($converters, $currentconverter);
+ $key = $keys[0];
+ if (isset($converters[$key + 1])) {
+ return $converters[$key + 1];
+ } else {
+ return false;
+ }
+ } else if (!empty($converters)) {
+ return $converters[0];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Fetch the class for the preferred document converter.
+ *
+ * @param string $from The source target file (file extension)
+ * @param string $to The desired target file format (file extension)
+ * @return string The class for document conversion
+ */
+ protected function get_document_converter_classes($from, $to) {
+ $classes = [];
+
+ $converters = $this->get_enabled_plugins();
+ foreach ($converters as $plugin => $classname) {
+ if (!class_exists($classname)) {
+ continue;
+ }
+
+ if (!$classname::are_requirements_met()) {
+ continue;
+ }
+
+ if ($classname::supports($from, $to)) {
+ $classes[] = $classname;
+ }
+ }
+
+ return $classes;
+ }
+
+ /**
+ * Check whether document conversion is supported for this file and target format.
+ *
+ * @param stored_file $file The file to convert
+ * @param string $to The desired target file format (file extension)
+ * @return bool Whether the target type can be converted
+ */
+ public function can_convert_storedfile_to(stored_file $file, $to) {
+ if ($file->is_directory()) {
+ // Directories cannot be converted.
+ return false;
+ }
+
+ if (!$file->get_filesize()) {
+ // Empty files cannot be converted.
+ return false;
+ }
+
+ $from = \core_filetypes::get_file_extension($file->get_mimetype());
+ if (!$from) {
+ // No mimetype could be found. Unable to determine converter.
+ return false;
+ }
+
+ return $this->can_convert_format_to($from, $to);
+ }
+
+ /**
+ * Check whether document conversion is supported for this file and target format.
+ *
+ * @param string $from The source target file (file extension)
+ * @param string $to The desired target file format (file extension)
+ * @return bool Whether the target type can be converted
+ */
+ public function can_convert_format_to($from, $to) {
+ return !empty($this->get_document_converter_classes($from, $to));
+ }
+
+}
diff --git a/files/classes/converter_interface.php b/files/classes/converter_interface.php
new file mode 100644
index 00000000000..52c1a946f3b
--- /dev/null
+++ b/files/classes/converter_interface.php
@@ -0,0 +1,81 @@
+.
+
+/**
+ * Class for converting files between different file formats.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_files;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for converting files between different file formats.
+ *
+ * @package docconvert_unoconv
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface converter_interface {
+
+ /**
+ * Whether the plugin is configured and requirements are met.
+ *
+ * Note: This function may be called frequently and caching is advisable.
+ *
+ * @return bool
+ */
+ public static function are_requirements_met();
+
+ /**
+ * Convert a document to a new format and return a conversion object relating to the conversion in progress.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return $this
+ */
+ public function start_document_conversion(conversion $conversion);
+
+ /**
+ * Poll an existing conversion for status update.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return $this
+ */
+ public function poll_conversion_status(conversion $conversion);
+
+ /**
+ * Determine whether a conversion between the two supplied formats is achievable.
+ *
+ * Note: This function may be called frequently and caching is advisable.
+ *
+ * @param string $from The source type
+ * @param string $to The destination type
+ * @return bool
+ */
+ public static function supports($from, $to);
+
+ /**
+ * A list of the supported conversions.
+ *
+ * Note: This information is only displayed to administrators.
+ *
+ * @return string
+ */
+ public function get_supported_conversions();
+}
diff --git a/files/classes/task/conversion_cleanup_task.php b/files/classes/task/conversion_cleanup_task.php
new file mode 100644
index 00000000000..1353b5b0967
--- /dev/null
+++ b/files/classes/task/conversion_cleanup_task.php
@@ -0,0 +1,54 @@
+.
+
+/**
+ * A scheduled task to clear up old conversion records.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_files\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to clear up old conversion records.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class conversion_cleanup_task extends \core\task\scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('fileconversioncleanuptask', 'admin');
+ }
+
+ /**
+ * Run task.
+ */
+ public function execute() {
+ \core_files\conversion::remove_old_conversion_records();
+ }
+
+}
diff --git a/files/tests/conversion_test.php b/files/tests/conversion_test.php
new file mode 100644
index 00000000000..662ccc3003f
--- /dev/null
+++ b/files/tests/conversion_test.php
@@ -0,0 +1,415 @@
+.
+
+
+/**
+ * PHPUnit tests for conversion API.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+use core_files\conversion;
+
+/**
+ * PHPUnit tests for conversion persistent.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_files_conversion_testcase extends advanced_testcase {
+
+ /**
+ * Helper to create a stored file object with the given supplied content.
+ *
+ * @param string $filecontent The content of the mocked file
+ * @param string $filename The file name to use in the stored_file
+ * @param string $filerecord Any overrides to the filerecord
+ * @return stored_file
+ */
+ protected function create_stored_file($filecontent = 'content', $filename = 'testfile.txt', $filerecord = []) {
+ $filerecord = array_merge([
+ 'contextid' => context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'unittest',
+ 'itemid' => 0,
+ 'filepath' => '/',
+ 'filename' => $filename,
+ ], $filerecord);
+
+ $fs = get_file_storage();
+ $file = $fs->create_file_from_string($filerecord, $filecontent);
+
+ return $file;
+ }
+
+ /**
+ * Ensure that get_conversions_for_file returns an existing conversion
+ * record with matching sourcefileid and targetformat.
+ */
+ public function test_get_conversions_for_file_existing_conversion_incomplete() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+
+ $existing = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $existing->create();
+
+ $conversions = conversion::get_conversions_for_file($sourcefile, 'pdf');
+
+ $this->assertCount(1, $conversions);
+
+ $conversion = array_shift($conversions);
+ $conversionfile = $conversion->get_sourcefile();
+
+ $this->assertEquals($sourcefile->get_id(), $conversionfile->get_id());
+ $this->assertFalse($conversion->get_destfile());
+ }
+
+ /**
+ * Ensure that get_conversions_for_file returns an existing conversion
+ * record with matching sourcefileid and targetformat when a second
+ * conversion to a different format exists.
+ */
+ public function test_get_conversions_for_file_existing_conversion_multiple_formats_incomplete() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+
+ $existing = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $existing->create();
+
+ $second = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'doc',
+ ]);
+ $second->create();
+
+ $conversions = conversion::get_conversions_for_file($sourcefile, 'pdf');
+
+ $this->assertCount(1, $conversions);
+
+ $conversion = array_shift($conversions);
+ $conversionfile = $conversion->get_sourcefile();
+
+ $this->assertEquals($sourcefile->get_id(), $conversionfile->get_id());
+ $this->assertFalse($conversion->get_destfile());
+ }
+
+ /**
+ * Ensure that get_conversions_for_file returns an existing conversion
+ * record with matching sourcefileid and targetformat.
+ */
+ public function test_get_conversions_for_file_existing_conversion_complete() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $destfile = $this->create_stored_file(
+ 'example content',
+ $sourcefile->get_contenthash(),
+ [
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'filepath' => '/pdf/',
+ ]);
+
+ $existing = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ 'destfileid' => $destfile->get_id(),
+ ]);
+ $existing->create();
+
+ $conversions = conversion::get_conversions_for_file($sourcefile, 'pdf');
+
+ // Only one file should be returned.
+ $this->assertCount(1, $conversions);
+
+ $conversion = array_shift($conversions);
+
+ $this->assertEquals($sourcefile->get_id(), $conversion->get_sourcefile()->get_id());
+ $this->assertEquals($destfile->get_id(), $conversion->get_destfile()->get_id());
+ }
+
+ /**
+ * Ensure that get_conversions_for_file returns an existing conversion
+ * record with matching sourcefileid and targetformat.
+ */
+ public function test_get_conversions_for_file_existing_conversion_multiple_formats_complete() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $destfile = $this->create_stored_file(
+ 'example content',
+ $sourcefile->get_contenthash(),
+ [
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'filepath' => '/pdf/',
+ ]);
+
+ $existing = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ 'destfileid' => $destfile->get_id(),
+ ]);
+ $existing->create();
+
+ $second = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'doc',
+ ]);
+ $second->create();
+
+ $conversions = conversion::get_conversions_for_file($sourcefile, 'pdf');
+
+ // Only one file should be returned.
+ $this->assertCount(1, $conversions);
+
+ $conversion = array_shift($conversions);
+
+ $this->assertEquals($sourcefile->get_id(), $conversion->get_sourcefile()->get_id());
+ $this->assertEquals($destfile->get_id(), $conversion->get_destfile()->get_id());
+ }
+
+ /**
+ * Ensure that get_conversions_for_file returns an existing conversion
+ * record does not exist, but the file has previously been converted.
+ */
+ public function test_get_conversions_for_file_existing_target() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $destfile = $this->create_stored_file(
+ 'example content',
+ $sourcefile->get_contenthash(),
+ [
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'filepath' => '/pdf/',
+ ]);
+
+ $conversions = conversion::get_conversions_for_file($sourcefile, 'pdf');
+
+ $this->assertCount(1, $conversions);
+
+ $conversion = array_shift($conversions);
+ $conversionsource = $conversion->get_sourcefile();
+ $this->assertEquals($sourcefile->get_id(), $conversionsource->get_id());
+ $conversiondest = $conversion->get_destfile();
+ $this->assertEquals($destfile->get_id(), $conversiondest->get_id());
+ }
+
+ /**
+ * Ensure that set_sourcefile sets the correct fileid.
+ */
+ public function test_set_sourcefile() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) []);
+
+ $conversion->set_sourcefile($sourcefile);
+
+ $this->assertEquals($sourcefile->get_id(), $conversion->get('sourcefileid'));
+ $this->assertNull($conversion->get('destfileid'));
+ }
+
+ /**
+ * Ensure that store_destfile_from_path stores the file as expected.
+ */
+ public function test_store_destfile_from_path() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+
+ $fixture = __FILE__;
+ $conversion->store_destfile_from_path($fixture);
+
+ $destfile = $conversion->get_destfile();
+ $this->assertEquals(file_get_contents($fixture), $destfile->get_content());
+ }
+
+ /**
+ * Ensure that store_destfile_from_path stores the file as expected.
+ */
+ public function test_store_destfile_from_path_delete_existing() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+
+ $record = [
+ 'contextid' => \context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'itemid' => 0,
+ 'filepath' => '/pdf/',
+ ];
+ $existingfile = $this->create_stored_file('foo', $sourcefile->get_contenthash(), $record);
+
+ $fixture = __FILE__;
+ $conversion->store_destfile_from_path($fixture);
+
+ $destfile = $conversion->get_destfile();
+ $this->assertEquals(file_get_contents($fixture), $destfile->get_content());
+ }
+
+ /**
+ * Ensure that store_destfile_from_path stores the file as expected.
+ */
+ public function test_store_destfile_from_string() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+
+ $fixture = 'Example content';
+ $conversion->store_destfile_from_string($fixture);
+
+ $destfile = $conversion->get_destfile();
+ $this->assertEquals($fixture, $destfile->get_content());
+ }
+
+ /**
+ * Ensure that store_destfile_from_string stores the file as expected when
+ * an existing destfile is found.
+ */
+ public function test_store_destfile_from_string_delete_existing() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+
+ $record = [
+ 'contextid' => \context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'documentconversion',
+ 'itemid' => 0,
+ 'filepath' => '/pdf/',
+ ];
+ $existingfile = $this->create_stored_file('foo', $sourcefile->get_contenthash(), $record);
+
+ $fixture = 'Example content';
+ $conversion->store_destfile_from_string($fixture);
+
+ $destfile = $conversion->get_destfile();
+ $this->assertEquals($fixture, $destfile->get_content());
+ }
+
+ /**
+ * Ensure that the get_status functions cast the status to integer correctly.
+ */
+ public function test_get_status() {
+ $conversion = new conversion(0, (object) [
+ 'status' => (string) 1,
+ ]);
+
+ $this->assertInternalType('integer', $conversion->get('status'));
+ }
+
+ /**
+ * Ensure that get_converter_instance returns false when no converter is set.
+ */
+ public function test_get_converter_instance_none_set() {
+ $conversion = new conversion(0, (object) []);
+ $this->assertFalse($conversion->get_converter_instance());
+ }
+
+ /**
+ * Ensure that get_converter_instance returns false when no valid converter is set.
+ */
+ public function test_get_converter_instance_invalid_set() {
+ $conversion = new conversion(0, (object) [
+ 'converter' => '\\fileconverter_not_a_valid_converter\\converter',
+ ]);
+ $this->assertFalse($conversion->get_converter_instance());
+ }
+
+ /**
+ * Ensure that get_converter_instance returns an instance when a valid converter is set.
+ */
+ public function test_get_converter_instance_valid_set() {
+ $conversion = new conversion(0, (object) [
+ 'converter' => \fileconverter_unoconv\converter::class,
+ ]);
+ $this->assertInstanceOf(\fileconverter_unoconv\converter::class, $conversion->get_converter_instance());
+ }
+
+ /**
+ * Test that all old conversion records are removed periodically.
+ */
+ public function test_remove_old_conversion_records_old() {
+ $this->resetAfterTest();
+ global $DB;
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $conversion->create();
+ $DB->set_field(conversion::TABLE, 'timemodified', time() - YEARSECS);
+
+ conversion::remove_old_conversion_records();
+
+ $this->assertEquals(0, $DB->count_records(conversion::TABLE));
+ }
+
+ /**
+ * Test that all old conversion records are removed periodically.
+ */
+ public function test_remove_old_conversion_records_young() {
+ $this->resetAfterTest();
+ global $DB;
+
+ $sourcefile = $this->create_stored_file();
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $conversion->create();
+ $DB->set_field(conversion::TABLE, 'timemodified', time() - DAYSECS);
+
+ conversion::remove_old_conversion_records();
+
+ $this->assertEquals(1, $DB->count_records(conversion::TABLE));
+ }
+}
diff --git a/files/tests/converter_test.php b/files/tests/converter_test.php
new file mode 100644
index 00000000000..6f182c6777d
--- /dev/null
+++ b/files/tests/converter_test.php
@@ -0,0 +1,926 @@
+.
+
+
+/**
+ * PHPUnit tests for fileconverter API.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+use core_files\conversion;
+use core_files\converter;
+
+/**
+ * PHPUnit tests for fileconverter API.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_files_converter_testcase extends advanced_testcase {
+
+ /**
+ * Get a testable mock of the abstract files_converter class.
+ *
+ * @param array $mockedmethods A list of methods you intend to override
+ * If no methods are specified, only abstract functions are mocked.
+ * @return \core_files\converter
+ */
+ protected function get_testable_mock($mockedmethods = []) {
+ $converter = $this->getMockBuilder(\core_files\converter::class)
+ ->setMethods($mockedmethods)
+ ->getMockForAbstractClass();
+
+ return $converter;
+ }
+
+ /**
+ * Get a testable mock of the conversion.
+ *
+ * @param array $mockedmethods A list of methods you intend to override
+ * @return \core_files\conversion
+ */
+ protected function get_testable_conversion($mockedmethods = []) {
+ $conversion = $this->getMockBuilder(\core_files\conversion::class)
+ ->setMethods($mockedmethods)
+ ->setConstructorArgs([0, (object) []])
+ ->getMock();
+
+ return $conversion;
+ }
+
+ /**
+ * Get a testable mock of the abstract files_converter class.
+ *
+ * @param array $mockedmethods A list of methods you intend to override
+ * If no methods are specified, only abstract functions are mocked.
+ * @return \core_files\converter_interface
+ */
+ protected function get_mocked_converter($mockedmethods = []) {
+ $converter = $this->getMockBuilder(\core_files\converter_interface::class)
+ ->setMethods($mockedmethods)
+ ->getMockForAbstractClass();
+
+ return $converter;
+ }
+
+ /**
+ * Helper to create a stored file objectw with the given supplied content.
+ *
+ * @param string $filecontent The content of the mocked file
+ * @param string $filename The file name to use in the stored_file
+ * @param array $mockedmethods A list of methods you intend to override
+ * If no methods are specified, only abstract functions are mocked.
+ * @return stored_file
+ */
+ protected function get_stored_file($filecontent = 'content', $filename = null, $filerecord = [], $mockedmethods = null) {
+ global $CFG;
+
+ $contenthash = sha1($filecontent);
+ if (empty($filename)) {
+ $filename = $contenthash;
+ }
+
+ $filerecord['contenthash'] = $contenthash;
+ $filerecord['filesize'] = strlen($filecontent);
+ $filerecord['filename'] = $filename;
+ $filerecord['id'] = 42;
+
+ $file = $this->getMockBuilder(stored_file::class)
+ ->setMethods($mockedmethods)
+ ->setConstructorArgs([get_file_storage(), (object) $filerecord])
+ ->getMock();
+
+ return $file;
+ }
+
+ /**
+ * Helper to create a stored file object with the given supplied content.
+ *
+ * @param string $filecontent The content of the mocked file
+ * @param string $filename The file name to use in the stored_file
+ * @param string $filerecord Any overrides to the filerecord
+ * @return stored_file
+ */
+ protected function create_stored_file($filecontent = 'content', $filename = 'testfile.txt', $filerecord = []) {
+ $filerecord = array_merge([
+ 'contextid' => context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'unittest',
+ 'itemid' => 0,
+ 'filepath' => '/',
+ 'filename' => $filename,
+ ], $filerecord);
+
+ $fs = get_file_storage();
+ $file = $fs->create_file_from_string($filerecord, $filecontent);
+
+ return $file;
+ }
+
+ /**
+ * Get a mock of the file_storage API.
+ *
+ * @param array $mockedmethods A list of methods you intend to override
+ * @return file_storage
+ */
+ protected function get_file_storage_mock($mockedmethods = []) {
+ $fs = $this->getMockBuilder(\file_storage::class)
+ ->setMethods($mockedmethods)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $fs;
+ }
+
+ /**
+ * Test the start_conversion function.
+ */
+ public function test_start_conversion_existing_single() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+
+ $first = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $first->create();
+
+ $converter = $this->get_testable_mock(['poll_conversion']);
+ $conversion = $converter->start_conversion($sourcefile, 'pdf', false);
+
+ // The old conversions should still be present and match the one returned.
+ $this->assertEquals($first->get('id'), $conversion->get('id'));
+ }
+
+ /**
+ * Test the start_conversion function.
+ */
+ public function test_start_conversion_existing_multiple() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+
+ $first = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $first->create();
+
+ $second = new conversion(0, (object) [
+ 'sourcefileid' => $sourcefile->get_id(),
+ 'targetformat' => 'pdf',
+ ]);
+ $second->create();
+
+ $converter = $this->get_testable_mock(['poll_conversion']);
+ $conversion = $converter->start_conversion($sourcefile, 'pdf', false);
+
+ // The old conversions should have been removed.
+ $this->assertFalse(conversion::get_record(['id' => $first->get('id')]));
+ $this->assertFalse(conversion::get_record(['id' => $second->get('id')]));
+ }
+
+ /**
+ * Test the start_conversion function.
+ */
+ public function test_start_conversion_no_existing() {
+ $this->resetAfterTest();
+
+ $sourcefile = $this->create_stored_file();
+
+ $converter = $this->get_testable_mock(['poll_conversion']);
+ $conversion = $converter->start_conversion($sourcefile, 'pdf', false);
+
+ $this->assertInstanceOf(\core_files\conversion::class, $conversion);
+ }
+
+ /**
+ * Test the get_document_converter_classes function with no enabled plugins.
+ */
+ public function test_get_document_converter_classes_no_plugins() {
+ $converter = $this->get_testable_mock(['get_enabled_plugins']);
+ $converter->method('get_enabled_plugins')->willReturn([]);
+
+ $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes');
+ $method->setAccessible(true);
+ $result = $method->invokeArgs($converter, ['docx', 'pdf']);
+ $this->assertEmpty($result);
+ }
+
+ /**
+ * Test the get_document_converter_classes function when no class was found.
+ */
+ public function test_get_document_converter_classes_plugin_class_not_found() {
+ $converter = $this->get_testable_mock(['get_enabled_plugins']);
+ $converter->method('get_enabled_plugins')->willReturn([
+ 'noplugin' => '\not\a\real\plugin',
+ ]);
+
+ $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes');
+ $method->setAccessible(true);
+ $result = $method->invokeArgs($converter, ['docx', 'pdf']);
+ $this->assertEmpty($result);
+ }
+
+ /**
+ * Test the get_document_converter_classes function when the returned classes do not meet requirements.
+ */
+ public function test_get_document_converter_classes_plugin_class_requirements_not_met() {
+ $plugin = $this->getMockBuilder(\core_file_converter_requirements_not_met_test::class)
+ ->setMethods()
+ ->getMock();
+
+ $converter = $this->get_testable_mock(['get_enabled_plugins']);
+ $converter->method('get_enabled_plugins')->willReturn([
+ 'test_plugin' => get_class($plugin),
+ ]);
+
+ $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes');
+ $method->setAccessible(true);
+ $result = $method->invokeArgs($converter, ['docx', 'pdf']);
+ $this->assertEmpty($result);
+ }
+
+ /**
+ * Test the get_document_converter_classes function when the returned classes do not meet requirements.
+ */
+ public function test_get_document_converter_classes_plugin_class_met_not_supported() {
+ $plugin = $this->getMockBuilder(\core_file_converter_type_not_supported_test::class)
+ ->setMethods()
+ ->getMock();
+
+ $converter = $this->get_testable_mock(['get_enabled_plugins']);
+ $converter->method('get_enabled_plugins')->willReturn([
+ 'test_plugin' => get_class($plugin),
+ ]);
+
+ $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes');
+ $method->setAccessible(true);
+ $result = $method->invokeArgs($converter, ['docx', 'pdf']);
+ $this->assertEmpty($result);
+ }
+
+ /**
+ * Test the get_document_converter_classes function when the returned classes do not meet requirements.
+ */
+ public function test_get_document_converter_classes_plugin_class_met_and_supported() {
+ $plugin = $this->getMockBuilder(\core_file_converter_type_supported_test::class)
+ ->setMethods()
+ ->getMock();
+ $classname = get_class($plugin);
+
+ $converter = $this->get_testable_mock(['get_enabled_plugins']);
+ $converter->method('get_enabled_plugins')->willReturn([
+ 'test_plugin' => $classname,
+ ]);
+
+ $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes');
+ $method->setAccessible(true);
+ $result = $method->invokeArgs($converter, ['docx', 'pdf']);
+ $this->assertCount(1, $result);
+ $this->assertNotFalse(array_search($classname, $result));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with a directory.
+ */
+ public function test_can_convert_storedfile_to_directory() {
+ $converter = $this->get_testable_mock();
+
+ // A file with filename '.' is a directory.
+ $file = $this->get_stored_file('', '.');
+
+ $this->assertFalse($converter->can_convert_storedfile_to($file, 'target'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with an empty file.
+ */
+ public function test_can_convert_storedfile_to_emptyfile() {
+ $converter = $this->get_testable_mock();
+
+ // A file with filename '.' is a directory.
+ $file = $this->get_stored_file('');
+
+ $this->assertFalse($converter->can_convert_storedfile_to($file, 'target'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with a file with indistinguished mimetype.
+ */
+ public function test_can_convert_storedfile_to_no_mimetype() {
+ $converter = $this->get_testable_mock();
+
+ // A file with filename '.' is a directory.
+ $file = $this->get_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $this->assertFalse($converter->can_convert_storedfile_to($file, 'target'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with a file with indistinguished mimetype.
+ */
+ public function test_can_convert_storedfile_to_docx() {
+ $returnvalue = (object) [];
+
+ $converter = $this->get_testable_mock([
+ 'can_convert_format_to'
+ ]);
+
+ $types = \core_filetypes::get_types();
+
+ // A file with filename '.' is a directory.
+ $file = $this->get_stored_file('example content', 'example', [
+ 'mimetype' => $types['docx']['type'],
+ ]);
+
+ $converter->expects($this->once())
+ ->method('can_convert_format_to')
+ ->willReturn($returnvalue);
+
+ $result = $converter->can_convert_storedfile_to($file, 'target');
+ $this->assertEquals($returnvalue, $result);
+ }
+
+
+ /**
+ * Test the can_convert_format_to function.
+ */
+ public function test_can_convert_format_to_found() {
+ $converter = $this->get_testable_mock(['get_document_converter_classes']);
+
+ $mock = $this->get_mocked_converter();
+
+ $converter->method('get_document_converter_classes')
+ ->willReturn([$mock]);
+
+ $result = $converter->can_convert_format_to('from', 'to');
+ $this->assertTrue($result);
+ }
+
+ /**
+ * Test the can_convert_format_to function.
+ */
+ public function test_can_convert_format_to_not_found() {
+ $converter = $this->get_testable_mock(['get_document_converter_classes']);
+
+ $converter->method('get_document_converter_classes')
+ ->willReturn([]);
+
+ $result = $converter->can_convert_format_to('from', 'to');
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with an empty file.
+ */
+ public function test_poll_conversion_in_progress() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ 'get_next_converter',
+ ]);
+
+ $converter->method('get_document_converter_classes')->willReturn([]);
+ $converter->method('get_next_converter')->willReturn(false);
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $this->get_testable_conversion([
+ 'get_converter_instance',
+ ]);
+ $conversion->set_sourcefile($file);
+ $conversion->set('targetformat', 'target');
+ $conversion->set('status', conversion::STATUS_IN_PROGRESS);
+
+ $converterinstance = $this->get_mocked_converter([
+ 'poll_conversion_status',
+ ]);
+ $converterinstance->expects($this->once())
+ ->method('poll_conversion_status');
+ $conversion->method('get_converter_instance')->willReturn($converterinstance);
+
+ $converter->poll_conversion($conversion);
+
+ $this->assertEquals(conversion::STATUS_IN_PROGRESS, $conversion->get('status'));
+ }
+
+ /**
+ * Test poll_conversion with an in-progress conversion where we are
+ * unable to instantiate the converter instance.
+ */
+ public function test_poll_conversion_in_progress_fail() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ 'get_next_converter',
+ ]);
+
+ $converter->method('get_document_converter_classes')->willReturn([]);
+ $converter->method('get_next_converter')->willReturn(false);
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $this->get_testable_conversion([
+ 'get_converter_instance',
+ ]);
+ $conversion->set_sourcefile($file);
+ $conversion->set('targetformat', 'target');
+ $conversion->set('status', conversion::STATUS_IN_PROGRESS);
+ $conversion->create();
+
+ $conversion->method('get_converter_instance')->willReturn(false);
+
+ $converter->poll_conversion($conversion);
+
+ $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with an empty file.
+ */
+ public function test_poll_conversion_none_supported() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ 'get_next_converter',
+ ]);
+
+ $converter->method('get_document_converter_classes')->willReturn([]);
+ $converter->method('get_next_converter')->willReturn(false);
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = new conversion(0, (object) [
+ 'sourcefileid' => $file->get_id(),
+ 'targetformat' => 'target',
+ ]);
+
+ $converter->poll_conversion($conversion);
+
+ $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with an empty file.
+ */
+ public function test_poll_conversion_pick_first() {
+ $this->resetAfterTest();
+
+ $converterinstance = $this->get_mocked_converter([
+ 'start_document_conversion',
+ 'poll_conversion_status',
+ ]);
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ 'get_next_converter',
+ ]);
+
+ $converter->method('get_document_converter_classes')->willReturn([]);
+ $converter->method('get_next_converter')->willReturn(get_class($converterinstance));
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $this->get_testable_conversion([
+ 'get_converter_instance',
+ ]);
+ $conversion->set_sourcefile($file);
+ $conversion->set('targetformat', 'target');
+ $conversion->set('status', conversion::STATUS_PENDING);
+ $conversion->create();
+
+ $conversion->method('get_converter_instance')->willReturn($converterinstance);
+
+ $converterinstance->expects($this->once())
+ ->method('start_document_conversion');
+ $converterinstance->expects($this->never())
+ ->method('poll_conversion_status');
+
+ $converter->poll_conversion($conversion);
+
+ $this->assertEquals(conversion::STATUS_IN_PROGRESS, $conversion->get('status'));
+ }
+
+ /**
+ * Test the can_convert_storedfile_to function with an empty file.
+ */
+ public function test_poll_conversion_pick_subsequent() {
+ $this->resetAfterTest();
+
+ $converterinstance = $this->get_mocked_converter([
+ 'start_document_conversion',
+ 'poll_conversion_status',
+ ]);
+ $converterinstance2 = $this->get_mocked_converter([
+ 'start_document_conversion',
+ 'poll_conversion_status',
+ ]);
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ 'get_next_converter',
+ ]);
+
+ $converter->method('get_document_converter_classes')->willReturn([]);
+ $converter->method('get_next_converter')
+ ->will($this->onConsecutiveCalls(
+ get_class($converterinstance),
+ get_class($converterinstance2)
+ ));
+
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $this->get_testable_conversion([
+ 'get_converter_instance',
+ 'get_status',
+ ]);
+ $conversion->set_sourcefile($file);
+ $conversion->set('targetformat', 'target');
+ $conversion->set('status', conversion::STATUS_PENDING);
+ $conversion->create();
+
+ $conversion->method('get_status')
+ ->will($this->onConsecutiveCalls(
+ // Initial status check.
+ conversion::STATUS_PENDING,
+ // Second check to make sure it's still pending after polling.
+ conversion::STATUS_PENDING,
+ // First one fails.
+ conversion::STATUS_FAILED,
+ // Second one succeeds.
+ conversion::STATUS_COMPLETE,
+ // And the final result checked in this unit test.
+ conversion::STATUS_COMPLETE
+ ));
+
+ $conversion->method('get_converter_instance')
+ ->will($this->onConsecutiveCalls(
+ $converterinstance,
+ $converterinstance2
+ ));
+
+ $converterinstance->expects($this->once())
+ ->method('start_document_conversion');
+ $converterinstance->expects($this->never())
+ ->method('poll_conversion_status');
+ $converterinstance2->expects($this->once())
+ ->method('start_document_conversion');
+ $converterinstance2->expects($this->never())
+ ->method('poll_conversion_status');
+
+ $converter->poll_conversion($conversion);
+
+ $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status'));
+ }
+
+ /**
+ * Test the start_conversion with a single converter which succeeds.
+ */
+ public function test_start_conversion_one_supported_success() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ ]);
+
+ $converter->method('get_document_converter_classes')
+ ->willReturn([\core_file_converter_type_successful::class]);
+
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $converter->start_conversion($file, 'target');
+
+ $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status'));
+ }
+
+ /**
+ * Test the start_conversion with a single converter which failes.
+ */
+ public function test_start_conversion_one_supported_failure() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ ]);
+
+ $mock = $this->get_mocked_converter(['start_document_conversion']);
+ $converter->method('get_document_converter_classes')
+ ->willReturn([\core_file_converter_type_failed::class]);
+
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $converter->start_conversion($file, 'target');
+
+ $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status'));
+ }
+
+ /**
+ * Test the start_conversion with two converters - fail, then succeed.
+ */
+ public function test_start_conversion_two_supported() {
+ $this->resetAfterTest();
+
+ $converter = $this->get_testable_mock([
+ 'get_document_converter_classes',
+ ]);
+
+ $mock = $this->get_mocked_converter(['start_document_conversion']);
+ $converter->method('get_document_converter_classes')
+ ->willReturn([
+ \core_file_converter_type_failed::class,
+ \core_file_converter_type_successful::class,
+ ]);
+
+ $file = $this->create_stored_file('example content', 'example', [
+ 'mimetype' => null,
+ ]);
+
+ $conversion = $converter->start_conversion($file, 'target');
+
+ $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status'));
+ }
+
+ /**
+ * Ensure that get_next_converter returns false when no converters are available.
+ */
+ public function test_get_next_converter_no_converters() {
+ $rcm = new \ReflectionMethod(converter::class, 'get_next_converter');
+ $rcm->setAccessible(true);
+
+ $converter = new \core_files\converter();
+ $result = $rcm->invoke($converter, [], null);
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Ensure that get_next_converter returns false when already on the
+ * only converter.
+ */
+ public function test_get_next_converter_only_converters() {
+ $rcm = new \ReflectionMethod(converter::class, 'get_next_converter');
+ $rcm->setAccessible(true);
+
+ $converter = new converter();
+ $result = $rcm->invoke($converter, ['example'], 'example');
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Ensure that get_next_converter returns false when already on the
+ * last converter.
+ */
+ public function test_get_next_converter_last_converters() {
+ $rcm = new \ReflectionMethod(converter::class, 'get_next_converter');
+ $rcm->setAccessible(true);
+
+ $converter = new converter();
+ $result = $rcm->invoke($converter, ['foo', 'example'], 'example');
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Ensure that get_next_converter returns the next vlaue when in a
+ * current converter.
+ */
+ public function test_get_next_converter_middle_converters() {
+ $rcm = new \ReflectionMethod(converter::class, 'get_next_converter');
+ $rcm->setAccessible(true);
+
+ $converter = new converter();
+ $result = $rcm->invoke($converter, ['foo', 'bar', 'baz', 'example'], 'bar');
+ $this->assertEquals('baz', $result);
+ }
+ /**
+ *
+ * Ensure that get_next_converter returns the next vlaue when in a
+ * current converter.
+ */
+ public function test_get_next_converter_first() {
+ $rcm = new \ReflectionMethod(converter::class, 'get_next_converter');
+ $rcm->setAccessible(true);
+
+ $converter = new converter();
+ $result = $rcm->invoke($converter, ['foo', 'bar', 'baz', 'example']);
+ $this->assertEquals('foo', $result);
+ }
+}
+
+class core_file_converter_requirements_test_base implements \core_files\converter_interface {
+
+ /**
+ * Whether the plugin is configured and requirements are met.
+ *
+ * @return bool
+ */
+ public static function are_requirements_met() {
+ return false;
+ }
+
+ /**
+ * Convert a document to a new format and return a conversion object relating to the conversion in progress.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return conversion
+ */
+ public function start_document_conversion(conversion $conversion) {
+ }
+
+ /**
+ * Poll an existing conversion for status update.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return conversion
+ */
+ public function poll_conversion_status(conversion $conversion) {
+ }
+
+ /**
+ * Whether a file conversion can be completed using this converter.
+ *
+ * @param string $from The source type
+ * @param string $to The destination type
+ * @return bool
+ */
+ public static function supports($from, $to) {
+ return false;
+ }
+
+ /**
+ * A list of the supported conversions.
+ *
+ * @return string
+ */
+ public function get_supported_conversions() {
+ return [];
+ }
+
+}
+
+/**
+ * Test class for converter support with requirements are not met.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_file_converter_requirements_not_met_test extends core_file_converter_requirements_test_base {
+}
+
+/**
+ * Test class for converter support with requirements met and conversion not supported.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_file_converter_type_not_supported_test extends core_file_converter_requirements_test_base {
+
+ /**
+ * Whether the plugin is configured and requirements are met.
+ *
+ * @return bool
+ */
+ public static function are_requirements_met() {
+ return true;
+ }
+}
+
+/**
+ * Test class for converter support with requirements met and conversion supported.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_file_converter_type_supported_test extends core_file_converter_requirements_test_base {
+
+ /**
+ * Whether the plugin is configured and requirements are met.
+ *
+ * @return bool
+ */
+ public static function are_requirements_met() {
+ return true;
+ }
+
+ /**
+ * Whether a file conversion can be completed using this converter.
+ *
+ * @param string $from The source type
+ * @param string $to The destination type
+ * @return bool
+ */
+ public static function supports($from, $to) {
+ return true;
+ }
+}
+
+/**
+ * Test class for converter support with requirements met and successful conversion.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_file_converter_type_successful extends core_file_converter_requirements_test_base {
+
+ /**
+ * Convert a document to a new format and return a conversion object relating to the conversion in progress.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return conversion
+ */
+ public function start_document_conversion(conversion $conversion) {
+ $conversion->set('status', conversion::STATUS_COMPLETE);
+
+ return $conversion;
+ }
+
+ /**
+ * Whether a file conversion can be completed using this converter.
+ *
+ * @param string $from The source type
+ * @param string $to The destination type
+ * @return bool
+ */
+ public static function supports($from, $to) {
+ return true;
+ }
+}
+
+/**
+ * Test class for converter support with requirements met and failed conversion.
+ *
+ * @package core_files
+ * @copyright 2017 Andrew nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_file_converter_type_failed extends core_file_converter_requirements_test_base {
+
+ /**
+ * Whether the plugin is configured and requirements are met.
+ *
+ * @return bool
+ */
+ public static function are_requirements_met() {
+ return true;
+ }
+
+ /**
+ * Convert a document to a new format and return a conversion object relating to the conversion in progress.
+ *
+ * @param conversion $conversion The file to be converted
+ * @return conversion
+ */
+ public function start_document_conversion(conversion $conversion) {
+ $conversion->set('status', conversion::STATUS_FAILED);
+
+ return $conversion;
+ }
+
+ /**
+ * Whether a file conversion can be completed using this converter.
+ *
+ * @param string $from The source type
+ * @param string $to The destination type
+ * @return bool
+ */
+ public static function supports($from, $to) {
+ return true;
+ }
+}
diff --git a/lang/en/plugin.php b/lang/en/plugin.php
index 9ed63798bf0..b865dff531e 100644
--- a/lang/en/plugin.php
+++ b/lang/en/plugin.php
@@ -105,6 +105,7 @@ $string['status_nodb'] = 'No database';
$string['status_upgrade'] = 'To be upgraded';
$string['status_uptodate'] = 'Installed';
$string['supportedmoodleversions'] = 'Supported Moodle versions';
+$string['supportedconversions'] = 'Supported document conversions';
$string['systemname'] = 'Identifier';
$string['type_antivirus'] = 'Antivirus plugin';
$string['type_antivirus_plural'] = 'Antivirus plugins';
@@ -128,6 +129,8 @@ $string['type_editor'] = 'Editor';
$string['type_editor_plural'] = 'Editors';
$string['type_enrol'] = 'Enrolment method';
$string['type_enrol_plural'] = 'Enrolment methods';
+$string['type_fileconverter'] = 'Document converter';
+$string['type_fileconverter_plural'] = 'Document converters';
$string['type_filter'] = 'Text filter';
$string['type_filter_plural'] = 'Text filters';
$string['type_format'] = 'Course format';
diff --git a/lib/adminlib.php b/lib/adminlib.php
index e1ce048cf68..28aafd6ce96 100644
--- a/lib/adminlib.php
+++ b/lib/adminlib.php
@@ -7399,6 +7399,7 @@ abstract class admin_setting_manage_plugins extends admin_setting {
/**
* Get the admin settings section name (just a unique string)
+ *
* @return string
*/
public function get_section_name() {
@@ -7407,18 +7408,21 @@ abstract class admin_setting_manage_plugins extends admin_setting {
/**
* Get the admin settings section title (use get_string).
+ *
* @return string
*/
abstract public function get_section_title();
/**
* Get the type of plugin to manage.
+ *
* @return string
*/
abstract public function get_plugin_type();
/**
* Get the name of the second column.
+ *
* @return string
*/
public function get_info_column_name() {
@@ -7514,19 +7518,12 @@ abstract class admin_setting_manage_plugins extends admin_setting {
public function output_html($data, $query = '') {
global $CFG, $OUTPUT, $DB, $PAGE;
- $spacer = new pix_icon('spacer', '', 'moodle');
- $moveup = new pix_icon('t/up', get_string('up'), 'moodle');
- $movedown = new pix_icon('t/down', get_string('down'), 'moodle');
-
$context = (object) [
'manageurl' => new moodle_url($this->get_manage_url(), [
'type' => $this->get_plugin_type(),
'sesskey' => sesskey(),
]),
'infocolumnname' => $this->get_info_column_name(),
- 'spacericon' => $spacer->export_for_template($OUTPUT),
- 'moveupicon' => $moveup->export_for_template($OUTPUT),
- 'movedownicon' => $movedown->export_for_template($OUTPUT),
'plugins' => [],
];
@@ -7541,7 +7538,6 @@ abstract class admin_setting_manage_plugins extends admin_setting {
'plugin' => $plugin->displayname,
'enabled' => $plugin->is_enabled(),
'togglelink' => '',
- 'toggleicon' => '',
'moveuplink' => '',
'movedownlink' => '',
'settingslink' => $plugin->get_settings_url(),
@@ -7552,7 +7548,7 @@ abstract class admin_setting_manage_plugins extends admin_setting {
// Enable/Disable link.
$togglelink = new moodle_url($pluginlink);
if ($plugin->is_enabled()) {
- $toggleicon = new pix_icon('i/hide', get_string('disable', 'moodle'), 'moodle');
+ $toggletarget = false;
$togglelink->param('action', 'disable');
if (count($context->plugins)) {
@@ -7567,11 +7563,11 @@ abstract class admin_setting_manage_plugins extends admin_setting {
$pluginkey->info = $this->get_info_column($plugin);
} else {
- $toggleicon = new pix_icon('i/show', get_string('enable', 'moodle'), 'moodle');
+ $toggletarget = true;
$togglelink->param('action', 'enable');
}
- $pluginkey->toggleicon = $toggleicon->export_for_template($OUTPUT);
+ $pluginkey->toggletarget = $toggletarget;
$pluginkey->togglelink = $togglelink;
$frankenstyle = $plugin->type . '_' . $plugin->name;
@@ -7593,6 +7589,31 @@ abstract class admin_setting_manage_plugins extends admin_setting {
}
}
+/**
+ * Generic class for managing plugins in a table that allows re-ordering and enable/disable of each plugin.
+ * Requires a get_rank method on the plugininfo class for sorting.
+ *
+ * @copyright 2017 Andrew Nicols
+* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_manage_fileconverter_plugins extends admin_setting_manage_plugins {
+ public function get_section_title() {
+ return get_string('type_fileconverter_plural', 'plugin');
+ }
+
+ public function get_plugin_type() {
+ return 'fileconverter';
+ }
+
+ public function get_info_column_name() {
+ return get_string('supportedconversions', 'plugin');
+ }
+
+ public function get_info_column($plugininfo) {
+ return $plugininfo->get_supported_conversions();
+ }
+}
+
/**
* Special class for media player plugins management.
*
diff --git a/lib/classes/component.php b/lib/classes/component.php
index 735f3863c58..009e8a34b08 100644
--- a/lib/classes/component.php
+++ b/lib/classes/component.php
@@ -439,6 +439,7 @@ $cache = '.var_export($cache, true).';
'enrol' => $CFG->dirroot.'/enrol',
'error' => null,
'filepicker' => null,
+ 'fileconverter' => $CFG->dirroot.'/files/converter',
'files' => $CFG->dirroot.'/files',
'filters' => null,
//'fonts' => null, // Bogus.
@@ -525,6 +526,7 @@ $cache = '.var_export($cache, true).';
'tool' => $CFG->dirroot.'/'.$CFG->admin.'/tool',
'cachestore' => $CFG->dirroot.'/cache/stores',
'cachelock' => $CFG->dirroot.'/cache/locks',
+ 'fileconverter' => $CFG->dirroot.'/files/converter',
);
$parents = array();
$subplugins = array();
diff --git a/lib/classes/filetypes.php b/lib/classes/filetypes.php
index f1d9cf8d4a8..4a160b0c80e 100644
--- a/lib/classes/filetypes.php
+++ b/lib/classes/filetypes.php
@@ -291,6 +291,23 @@ abstract class core_filetypes {
);
}
+ /**
+ * Given a mimetype - return a valid file extension for it.
+ *
+ * @param $mimetype string
+ * @return string|bool False if the mimetype was not known, a string indicating a valid file extension otherwise. It may not
+ * be the only valid file extension - just the first one found.
+ */
+ public static function get_file_extension($mimetype) {
+ $types = self::get_types();
+ foreach ($types as $extension => $info) {
+ if ($info['type'] == $mimetype) {
+ return $extension;
+ }
+ }
+ return false;
+ }
+
/**
* Gets all the current types.
*
diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index 8d7e37670a9..6f9917d6d4c 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -1768,6 +1768,9 @@ class core_plugin_manager {
'imagegallery'
),
+ 'fileconverter' => array(
+ ),
+
'editor' => array(
'atto', 'textarea', 'tinymce'
),
diff --git a/lib/classes/plugininfo/fileconverter.php b/lib/classes/plugininfo/fileconverter.php
new file mode 100644
index 00000000000..e7a5baeb30a
--- /dev/null
+++ b/lib/classes/plugininfo/fileconverter.php
@@ -0,0 +1,183 @@
+.
+
+/**
+ * Defines classes used for plugin info.
+ *
+ * @package core
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\plugininfo;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for document converter plugins
+ *
+ * @package core
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fileconverter extends base {
+
+ /**
+ * Should there be a way to uninstall the plugin via the administration UI.
+ *
+ * Uninstallation is allowed for fileconverter plugins.
+ *
+ * @return bool
+ */
+ public function is_uninstall_allowed() {
+ return true;
+ }
+
+ /**
+ * Get the name for the settings section.
+ *
+ * @return string
+ */
+ public function get_settings_section_name() {
+ return 'fileconverter' . $this->name;
+ }
+
+ /**
+ * Load the global settings for a particular availability plugin (if there are any)
+ *
+ * @param \part_of_admin_tree $adminroot
+ * @param string $parentnodename
+ * @param bool $hassiteconfig
+ */
+ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+ global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+ $ADMIN = $adminroot; // May be used in settings.php.
+ $plugininfo = $this; // Also can be used inside settings.php.
+
+ if (!$this->is_installed_and_upgraded()) {
+ return;
+ }
+
+ if (!$hassiteconfig) {
+ return;
+ }
+
+ $section = $this->get_settings_section_name();
+
+ $settings = null;
+ if (file_exists($this->full_path('settings.php'))) {
+ $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
+ include($this->full_path('settings.php')); // This may also set $settings to null.
+ }
+ if ($settings) {
+ $ADMIN->add($parentnodename, $settings);
+ }
+ }
+
+ /**
+ * Return URL used for management of plugins of this type.
+ * @return \moodle_url
+ */
+ public static function get_manage_url() {
+ return new \moodle_url('/admin/settings.php', array('section' => 'managefileconverterplugins'));
+ }
+
+ /**
+ * Finds all enabled plugins, the result may include missing plugins.
+ *
+ * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
+ */
+ public static function get_enabled_plugins() {
+ global $CFG;
+
+ $order = (!empty($CFG->converter_plugins_sortorder)) ? explode(',', $CFG->converter_plugins_sortorder) : [];
+ if ($order) {
+ $plugins = \core_plugin_manager::instance()->get_installed_plugins('fileconverter');
+ $order = array_intersect($order, array_keys($plugins));
+ }
+
+ return array_combine($order, $order);
+ }
+
+ /**
+ * Sets the current plugin as enabled or disabled
+ * When enabling tries to guess the sortorder based on default rank returned by the plugin.
+ * @param bool $newstate
+ */
+ public function set_enabled($newstate = true) {
+ $enabled = self::get_enabled_plugins();
+ if (array_key_exists($this->name, $enabled) == $newstate) {
+ // Nothing to do.
+ return;
+ }
+ if ($newstate) {
+ // Enable converter plugin.
+ $plugins = \core_plugin_manager::instance()->get_plugins_of_type('fileconverter');
+ if (!array_key_exists($this->name, $plugins)) {
+ // Can not be enabled.
+ return;
+ }
+ $enabled[$this->name] = $this->name;
+ self::set_enabled_plugins($enabled);
+ } else {
+ // Disable converter plugin.
+ unset($enabled[$this->name]);
+ self::set_enabled_plugins($enabled);
+ }
+ }
+
+ /**
+ * Set the list of enabled converter players in the specified sort order
+ * To be used when changing settings or in unit tests
+ * @param string|array $list list of plugin names without frankenstyle prefix - comma-separated string or an array
+ */
+ public static function set_enabled_plugins($list) {
+ if (empty($list)) {
+ $list = [];
+ } else if (!is_array($list)) {
+ $list = explode(',', $list);
+ }
+ if ($list) {
+ $plugins = \core_plugin_manager::instance()->get_installed_plugins('fileconverter');
+ $list = array_intersect($list, array_keys($plugins));
+ }
+ set_config('converter_plugins_sortorder', join(',', $list));
+ \core_plugin_manager::reset_caches();
+ }
+
+ /**
+ * Returns a string describing the formats this engine can converter from / to.
+ *
+ * @return string
+ */
+ public function get_supported_conversions() {
+ $classname = self::get_classname($this->name);
+ if (class_exists($classname)) {
+ $object = new $classname();
+ return $object->get_supported_conversions();
+ }
+ return '';
+ }
+
+ /**
+ * Return the class name for the plugin.
+ *
+ * @param string $plugin
+ * @return string
+ */
+ public static function get_classname($plugin) {
+ return "\\fileconverter_{$plugin}\\converter";
+ }
+}
diff --git a/lib/db/install.xml b/lib/db/install.xml
index 1c86d11c497..905a4660048 100644
--- a/lib/db/install.xml
+++ b/lib/db/install.xml
@@ -1,5 +1,5 @@
-
@@ -2388,6 +2388,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/db/tasks.php b/lib/db/tasks.php
index f7296c544fe..c820348d6c2 100644
--- a/lib/db/tasks.php
+++ b/lib/db/tasks.php
@@ -338,4 +338,13 @@ $tasks = array(
'dayofweek' => '*',
'month' => '*'
),
+ array(
+ 'classname' => 'core_files\task\conversion_cleanup_task',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => '2',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
);
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php
index 0dc1482fcf2..8e935d0fe8a 100644
--- a/lib/db/upgrade.php
+++ b/lib/db/upgrade.php
@@ -2555,5 +2555,37 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017021400.00);
}
+ if ($oldversion < 2017030700.00) {
+
+ // Define table file_conversion to be created.
+ $table = new xmldb_table('file_conversion');
+
+ // Adding fields to table file_conversion.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('sourcefileid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('targetformat', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('status', XMLDB_TYPE_INTEGER, '10', null, null, null, '0');
+ $table->add_field('statusmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+ $table->add_field('converter', XMLDB_TYPE_CHAR, '255', null, null, null, null);
+ $table->add_field('destfileid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+ $table->add_field('data', XMLDB_TYPE_TEXT, null, null, null, null, null);
+
+ // Adding keys to table file_conversion.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('sourcefileid', XMLDB_KEY_FOREIGN, array('sourcefileid'), 'files', array('id'));
+ $table->add_key('destfileid', XMLDB_KEY_FOREIGN, array('destfileid'), 'files', array('id'));
+
+ // Conditionally launch create table for file_conversion.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2017030700.00);
+ }
+
return true;
}
diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php
index 51b53df52b0..5ac54351d4c 100644
--- a/lib/tests/component_test.php
+++ b/lib/tests/component_test.php
@@ -34,7 +34,7 @@ class core_component_testcase extends advanced_testcase {
// To be changed if number of subsystems increases/decreases,
// this is defined here to annoy devs that try to add more without any thinking,
// always verify that it does not collide with any existing add-on modules and subplugins!!!
- const SUBSYSTEMCOUNT = 65;
+ const SUBSYSTEMCOUNT = 66;
public function setUp() {
$psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
diff --git a/version.php b/version.php
index 1d8ae473617..90ddbe0c5c0 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2017030300.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017030700.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
|