MDL-55528 core_files: Create new fileconverter plugintype

This commit is contained in:
Andrew Nicols
2017-03-10 09:31:14 +08:00
parent 29ce005862
commit 34df779a95
19 changed files with 2411 additions and 20 deletions
+13
View File
@@ -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) {
@@ -20,28 +20,33 @@
{{info}}
</td>
{{/infocolumnname}}
<td>
<td class="text-nowrap">
{{#moveuplink}}
<a href="{{{moveuplink}}}">
{{#moveupicon}}{{>core/pix_icon}}{{/moveupicon}}
{{#pix}}t/up, moodle, {{#str}} up, moodle {{/str}}{{/pix}}
</a>
{{/moveuplink}}
{{^moveuplink}}
{{#spacericon}}{{>core/pix_icon}}{{/spacericon}}
{{#pix}}spacer, moodle{{/pix}}
{{/moveuplink}}
{{#movedownlink}}
<a href="{{{movedownlink}}}">
{{#movedownicon}}{{>core/pix_icon}}{{/movedownicon}}
{{#pix}}t/down, moodle, {{#str}} down, moodle {{/str}}{{/pix}}
</a>
{{/movedownlink}}
{{^movedownlink}}
{{#spacericon}}{{>core/pix_icon}}{{/spacericon}}
{{#pix}}spacer, moodle{{/pix}}
{{/movedownlink}}
</td>
<td>
<a href="{{{togglelink}}}">
{{#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}}
</a>
</td>
<td>
+360
View File
@@ -0,0 +1,360 @@
<?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/>.
/**
* Classes for converting files between different file formats.
*
* @package core_files
* @copyright 2017 Andrew Nicols <[email protected]>
* @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 <[email protected]>
* @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(),
];
}
}
+247
View File
@@ -0,0 +1,247 @@
<?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/>.
/**
* 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));
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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/>.
/**
* Class for converting files between different file formats.
*
* @package core_files
* @copyright 2017 Andrew Nicols <[email protected]>
* @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 <[email protected]>
* @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();
}
@@ -0,0 +1,54 @@
<?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/>.
/**
* A scheduled task to clear up old conversion records.
*
* @package core_files
* @copyright 2017 Andrew Nicols <[email protected]>
* @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 <[email protected]>
* @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();
}
}
+415
View File
@@ -0,0 +1,415 @@
<?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/>.
/**
* PHPUnit tests for conversion API.
*
* @package core_files
* @copyright 2017 Andrew nicols <[email protected]>
* @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 <[email protected]>
* @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));
}
}
+926
View File
@@ -0,0 +1,926 @@
<?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/>.
/**
* PHPUnit tests for fileconverter API.
*
* @package core_files
* @copyright 2017 Andrew nicols <[email protected]>
* @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 <[email protected]>
* @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 <[email protected]>
* @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 <[email protected]>
* @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 <[email protected]>
* @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 <[email protected]>
* @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 <[email protected]>
* @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;
}
}
+3
View File
@@ -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';
+32 -11
View File
@@ -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 <andrew@nicols.co.uk>
* @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.
*
+2
View File
@@ -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();
+17
View File
@@ -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.
*
+3
View File
@@ -1768,6 +1768,9 @@ class core_plugin_manager {
'imagegallery'
),
'fileconverter' => array(
),
'editor' => array(
'atto', 'textarea', 'tinymce'
),
+183
View File
@@ -0,0 +1,183 @@
<?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/>.
/**
* 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";
}
}
+21 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20170207" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20170220" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -2388,6 +2388,26 @@
<INDEX NAME="uq_external_file" UNIQUE="true" FIELDS="referencehash, repositoryid" COMMENT="The combination of repositoryid and reference field is supposed to be a unique identification of an external file. Because the reference is a TEXT field, we can't use to compose the index. So we use the referencehash instead and the file API is responsible to keep it up-to-date"/>
</INDEXES>
</TABLE>
<TABLE NAME="file_conversion" COMMENT="Table to track file conversions.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="sourcefileid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="targetformat" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="status" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="statusmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="converter" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="destfileid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="data" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="sourcefileid" TYPE="foreign" FIELDS="sourcefileid" REFTABLE="files" REFFIELDS="id"/>
<KEY NAME="destfileid" TYPE="foreign" FIELDS="destfileid" REFTABLE="files" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="repository" COMMENT="This table contains one entry for every configured external repository instance.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+9
View File
@@ -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' => '*'
),
);
+32
View File
@@ -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;
}
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -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.