From 872bb73427fb18718570094d36ed66a90c83601f Mon Sep 17 00:00:00 2001 From: Mirko Otto Date: Thu, 25 Feb 2021 13:17:48 +0100 Subject: [PATCH] initial commit --- CONTRIBUTIONS.txt | 9 + README.md | 39 ++++ classes/converter.php | 312 +++++++++++++++++++++++++ classes/event/document_conversion.php | 64 +++++ classes/privacy/provider.php | 112 +++++++++ classes/task/convert_submissions.php | 83 +++++++ db/tasks.php | 40 ++++ lang/en/fileconverter_flasksoffice.php | 43 ++++ lib.php | 26 +++ settings.php | 36 +++ test.php | 73 ++++++ tests/fixtures/source.docx | Bin 0 -> 11065 bytes version.php | 29 +++ 13 files changed, 866 insertions(+) create mode 100644 CONTRIBUTIONS.txt create mode 100644 README.md create mode 100644 classes/converter.php create mode 100644 classes/event/document_conversion.php create mode 100644 classes/privacy/provider.php create mode 100644 classes/task/convert_submissions.php create mode 100644 db/tasks.php create mode 100644 lang/en/fileconverter_flasksoffice.php create mode 100644 lib.php create mode 100644 settings.php create mode 100644 test.php create mode 100644 tests/fixtures/source.docx create mode 100644 version.php diff --git a/CONTRIBUTIONS.txt b/CONTRIBUTIONS.txt new file mode 100644 index 0000000..edb59e1 --- /dev/null +++ b/CONTRIBUTIONS.txt @@ -0,0 +1,9 @@ +Flask LibreOffice document converter contributions +========== +If you wish to make a contribution to the plugin please follow the steps outlined in this document to ensure that we see it: + +1. Create a ticket describing the change (https://code.ovgu.de/ovgu-urz/moodle-fileconverter_flasksoffice/issues) +2. Create a pull request +3. Link the pull request in the ticket you created + +Please ensure that code follows the Moodle coding standards: https://docs.moodle.org/dev/Coding_style diff --git a/README.md b/README.md new file mode 100644 index 0000000..973682e --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +ABOUT +========== +This is a tool that enables Moodle to use a separat Linux server with +LibreOffice for converting documents. For example, this is useful in +assignment submissions. In combination with a Linux Ubuntu server with +LibreOffice, submitted text documents, spreadsheets, and presentations +are automatically converted to PDF to simplify the grading workflow. + +The plugin uses a Flask rest interface to a LibreOffice on a Linux +server. The installation and configuration of the Flask rest interface +is described in the associated repository. + +It is based on the Google Drive document converter plugin. + +This module may be distributed under the terms of the General Public License +(see http://www.gnu.org/licenses/gpl.txt for details) + +PURPOSE +========== +An alternative document conversion plugin that makes use of Flask and LibreOffice. + +INSTALLATION +========== +The Flask rest server document converter follows the standard installation procedure of file converters. + +1. Create folder \/files/converter/flasksoffice. +2. Extract files from downloaded archive to the created folder. +3. In Moodle: Visit Site administration -> PlugIns -> Additional Plugins -> Flask soffice -> Settings +4. Enter the URL of your Flask Server runnig soffice +5. Optional: To test plugin working properly: Click on "Test this converter is working properly." This will check the plugin. On the next page click on "Test document conversion" this will test a document conversion. If everything works properly a test PDF document is created/opened. + +ENABLING THE CONVERTER +========== +Visit Site administration ► Plugins ► Document converters ► Manage document converters to enable the plugin + +You will need to ensure that it is: + +1. Configured to use a [Flask LibreOffice rest server](https://github.com/miotto/server_fileconverter_flasksoffice). +2. Working by using the 'Test this converter is working properly' link on the settings page. diff --git a/classes/converter.php b/classes/converter.php new file mode 100644 index 0000000..76a9af9 --- /dev/null +++ b/classes/converter.php @@ -0,0 +1,312 @@ +. + +/** + * Class for converting files between different file formats using flask rest server. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace fileconverter_flasksoffice; + +defined('MOODLE_INTERNAL') || die(); + +use stored_file; +use moodle_exception; +use moodle_url; +use coding_exception; +use curl; +use \core_files\conversion; + +/** + * Class for converting files between different formats using flask rest server. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class converter implements \core_files\converter_interface { + + /** @var array $imports List of supported import file formats */ + private static $imports = [ + // Document file formats. + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'rtf' => 'application/rtf', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'html' => 'text/html', + 'txt' => 'text/plain', + // Spreadsheet file formats. + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + // Presentation file formats. + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.ms-powerpoint', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + ]; + + /** @var array $export List of supported export file formats */ + private static $exports = [ + 'pdf' => 'application/pdf' + ]; + + /** + * + * @var object Plugin configuration. + */ + private $config; + + /** + * @var string The base API URL + */ + private $baseurl; + + /** + * Class constructor + */ + public function __construct() { + $this->config = get_config('fileconverter_flasksoffice'); + + if ($this->baseurl == null) { + $this->baseurl = $this->config->flasksofficeurl; + } + } + + /** + * Check if the plugin has the required configuration set. + * + * @param \fileconverter_flasksoffice\converter $converter + * @return boolean $isset Is all configuration options set. + */ + private static function is_config_set(\fileconverter_flasksoffice\converter $converter) { + $iscorrect = true; + + if (empty($converter->config->flasksofficeurl)) { + $iscorrect = false; + } + + return $iscorrect; + } + + /** + * Whether the plugin is configured and requirements are met. + * + * @return bool + */ + public static function are_requirements_met() { + $converter = new \fileconverter_flasksoffice\converter(); + + // First check that we have the basic configuration settings set. + if (!self::is_config_set($converter)) { + debugging('fileconverter_flasksoffice configuration not set'); + return false; + } + + return true; + } + + + /** + * Convert a document to a new format and return a conversion object relating to the conversion in progress. + * + * @param \core_files\conversion $conversion The file to be converted + * @return this + */ + public function start_document_conversion(\core_files\conversion $conversion) { + global $CFG; + + $file = $conversion->get_sourcefile(); + $filepath = $file->get_filepath(); + $fromformat = pathinfo($file->get_filename(), PATHINFO_EXTENSION); + $format = $conversion->get('targetformat'); + + $uniqdir = make_unique_writable_directory(make_temp_directory('core_file/conversions')); + \core_shutdown_manager::register_function('remove_dir', array($uniqdir)); + $localfilename = $file->get_id() . '.' . $fromformat; + $filename = $uniqdir . '/' . $localfilename; + $file->copy_content_to($filename); + + $data = array('file' => curl_file_create($filename)); + + // Test server, if available. + $curl = new curl(); + $location = $this->baseurl; + $options = [ + 'CURLOPT_RETURNTRANSFER' => true + ]; + $curl->post($location, $data, $options); + if ($curl->errno != 0) { + throw new coding_exception($curl->error, $curl->errno); + } + + // Post/upload file to doc-server. + $curl = new curl(); + $location = $this->baseurl . '/upload'; + $options = [ + 'CURLOPT_RETURNTRANSFER' => true, + 'CURLOPT_HTTPHEADER' => array('Content-Type: multipart/form-data'), + 'CURLOPT_HEADER' => false, + ]; + $response = $curl->post($location, $data, $options); + if ($curl->errno != 0) { + throw new coding_exception($curl->error, $curl->errno); + } + + $json = json_decode($response, true); + if (!empty($json->error)) { + throw new coding_exception($json->error->code . ': ' . $json->error->message . '. Response was: '.$response); + } + if (!isset($json['result']['pdf']) OR is_null($json)) { + throw new coding_exception('Response was: '.$response); + } + + $strarray = explode('/', $json['result']['pdf']); + $lastelement = end($strarray); + + // Download file from doc-server. + $client = new curl(); + $sourceurl = new moodle_url($this->baseurl . $json['result']['pdf']); + $source = $sourceurl->out(false); + + $tmp = make_request_directory(); + $downloadto = $tmp . '/' . $lastelement; + + $options = ['filepath' => $downloadto, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; + $success = $client->download_one($source, null, $options); + if ($client->errno != 0) { + throw new coding_exception($client->error, $client->errno); + } + if ($success) { + $conversion->store_destfile_from_path($downloadto); + $conversion->set('status', conversion::STATUS_COMPLETE); + } else { + $conversion->set('status', conversion::STATUS_FAILED); + } + $conversion->update(); + + // Trigger event. + list($context, $course, $cm) = get_context_info_array($file->get_contextid()); + // Only it is related to a course. Config test excluded. + if (!is_null($course)) { + $eventinfo = array( + 'context' => $context, + 'courseid' => $course->id, + 'other' => array( + 'sourcefileid' => $conversion->get('sourcefileid'), + 'targetformat' => $conversion->get('targetformat'), + 'id' => $conversion->get('id'), + 'status' => $this->status + )); + $event = \fileconverter_flasksoffice\event\document_conversion::create($eventinfo); + $event->trigger(); + } + + return $this; + } + + /** + * Workhorse method: Poll an existing conversion for status update. If conversion has succeeded, download the result. + * + * @param conversion $conversion The file to be converted + * @return $this; + */ + public function poll_conversion_status(conversion $conversion) { + + // If conversion is complete or failed return early. + if ($conversion->get('status') == conversion::STATUS_COMPLETE + || $conversion->get('status') == conversion::STATUS_FAILED) { + return $this; + } + return $this->start_document_conversion($conversion); + } + + /** + * Generate and serve the test document. + * + * @return stored_file + */ + public function serve_test_document() { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + $format = 'pdf'; + + $filerecord = [ + 'contextid' => \context_system::instance()->id, + 'component' => 'test', + 'filearea' => 'fileconverter_flasksoffice', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'source.docx' + ]; + + // Get the fixture doc file content and generate and stored_file object. + $fs = get_file_storage(); + $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'], + $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']); + + if (!$testdocx) { + $fixturefile = dirname(__DIR__) . '/tests/fixtures/source.docx'; + $testdocx = $fs->create_file_from_pathname($filerecord, $fixturefile); + } + + $conversion = new \core_files\conversion(0, (object) [ + 'targetformat' => 'pdf', + ]); + + $conversion->set_sourcefile($testdocx); + $conversion->create(); + + // Convert the doc file to pdf and send it direct to the browser. + $this->start_document_conversion($conversion); + + $testfile = $conversion->get_destfile(); + readfile_accel($testfile, 'application/pdf', 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) { + // This is not a one-liner because of php 5.6. + $imports = self::$imports; + $exports = self::$exports; + return isset($imports[$from]) && isset($exports[$to]); + } + + /** + * A list of the supported conversions. + * + * @return string + */ + public function get_supported_conversions() { + $conversions = array( + // Document file formats. + 'doc', 'docx', 'rtf', 'odt', 'html', 'txt', + // Spreadsheet file formats. + 'xls', 'xlsx', 'ods', 'csv', + // Presentation file formats. + 'ppt', 'pptx', 'odp', + ); + return implode(', ', $conversions); + } +} diff --git a/classes/event/document_conversion.php b/classes/event/document_conversion.php new file mode 100644 index 0000000..feca86c --- /dev/null +++ b/classes/event/document_conversion.php @@ -0,0 +1,64 @@ +. + +/** + * The Flask LibreOffice document conversion event. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace fileconverter_flasksoffice\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The Flask LibreOffice document conversion event. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class document_conversion extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event:document_conversion', 'fileconverter_flasksoffice'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The conversion with id '{$this->other['id']}' has been executed and returned the status '{$this->other['status']}'"; + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..e106254 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,112 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace fileconverter_flasksoffice\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_userlist; + +/** + * Privacy class for requesting user data. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\request\plugin\provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_external_location_link('flasksoffice', [ + 'params' => 'privacy:metadata:fileconverter_flasksoffice:params', + 'filecontent' => 'privacy:metadata:fileconverter_flasksoffice:filecontent', + 'filemimetype' => 'privacy:metadata:fileconverter_flasksoffice:filemimetype', + ], 'privacy:metadata:fileconverter_flasksoffice:externalpurpose'); + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new contextlist(); + return $contextlist; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + } + + /** + * Delete all use data which matches the specified deletion_criteria. + * + * @param \context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + } +} diff --git a/classes/task/convert_submissions.php b/classes/task/convert_submissions.php new file mode 100644 index 0000000..7c9aaa0 --- /dev/null +++ b/classes/task/convert_submissions.php @@ -0,0 +1,83 @@ +. + +/** + * A scheduled task. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * based on code by Jan Dageförde, University of Münster and + * Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace fileconverter_flasksoffice\task; + +use core\task\scheduled_task; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Simple task to convert submissions to pdf in the background. + * @copyright 2020 Mirko Otto + * based on code by Jan Dageförde, University of Münster and + * Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class convert_submissions extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('preparesubmissionsforannotation', 'fileconverter_flasksoffice'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $DB; + mtrace('flasksoffice: Processing pending document conversions'); + + $params = array( + 'converter' => '\fileconverter_flasksoffice\converter', + 'status' => '1' + ); + $pendingconversions = $DB->get_recordset('file_conversion', $params, 'sourcefileid DESC', 'sourcefileid, targetformat'); + + $fs = get_file_storage(); + foreach ($pendingconversions as $pendingconversion) { + + $file = $fs->get_file_by_id($pendingconversion->sourcefileid); + if ($file) { + mtrace('flasksoffice: Processing conversions for file id: ' . $pendingconversion->sourcefileid); + $conversions = \core_files\conversion::get_conversions_for_file($file, $pendingconversion->targetformat); + + mtrace('flasksoffice: Found: ' . count($conversions) + . ' conversions for file id: ' . $pendingconversion->sourcefileid); + + foreach ($conversions as $conversion) { + $converter = new \fileconverter_flasksoffice\converter(); + $converter->poll_conversion_status($conversion); + } + } + } + $pendingconversions->close(); + } +} diff --git a/db/tasks.php b/db/tasks.php new file mode 100644 index 0000000..7f39027 --- /dev/null +++ b/db/tasks.php @@ -0,0 +1,40 @@ +. + +/** + * Definition of Flask LibreOffice converter scheduled tasks, adapted from the OnlyOffice one. + * + * @package fileconverter_flasksoffice + * @category task + * @copyright Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/* List of handlers */ + +$tasks = array( + array( + 'classname' => 'fileconverter_flasksoffice\task\convert_submissions', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), +); diff --git a/lang/en/fileconverter_flasksoffice.php b/lang/en/fileconverter_flasksoffice.php new file mode 100644 index 0000000..38582ef --- /dev/null +++ b/lang/en/fileconverter_flasksoffice.php @@ -0,0 +1,43 @@ +. + +/** + * Strings for plugin 'fileconverter_flasksoffice' + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Flask soffice'; + +$string['disabled'] = 'Disabled'; +$string['privacy:metadata:fileconverter_flasksoffice:externalpurpose'] = 'This information is sent to Flask rest server in order the file to be converted to an alternative format. The file is temporarily kept on Flask rest server and gets deleted after the conversion is done.'; +$string['privacy:metadata:fileconverter_flasksoffice:filecontent'] = 'The content of the file.'; +$string['privacy:metadata:fileconverter_flasksoffice:filemimetype'] = 'The MIME type of the file.'; +$string['privacy:metadata:fileconverter_flasksoffice:params'] = 'The query parameters passed to Flask rest server.'; +$string['event:document_conversion'] = 'Document conversion'; +$string['test_converter'] = 'Test this converter is working properly.'; +$string['test_conversion'] = 'Test document conversion'; +$string['test_conversionready'] = 'This document converter is configured properly.'; +$string['test_conversionnotready'] = 'This document converter is not configured properly.'; + +$string['settings:flasksofficeurl'] = 'Document Server URL'; +$string['settings:flasksofficeurl_help'] = 'Specify the URL at which document server can be reached *by Moodle*. The URL is never resolved in the browser, only in CURL requests by Moodle, so it will be resolved only in the local network.'; +$string['preparesubmissionsforannotation'] = 'Prepare submissions for annotation'; + diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..06dcea5 --- /dev/null +++ b/lib.php @@ -0,0 +1,26 @@ +. + +/** + * This plugin is used to convert documents with flask rest server. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..358b2b0 --- /dev/null +++ b/settings.php @@ -0,0 +1,36 @@ +. + +/** + * Link to Flask Webserver. + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $settings->add(new admin_setting_configtext('fileconverter_flasksoffice/flasksofficeurl', + get_string('settings:flasksofficeurl', 'fileconverter_flasksoffice'), + get_string('settings:flasksofficeurl_help', 'fileconverter_flasksoffice'), + '')); + + $url = new moodle_url('/files/converter/flasksoffice/test.php'); + $link = html_writer::link($url, get_string('test_converter', 'fileconverter_flasksoffice')); + $settings->add(new admin_setting_heading('test_converter', '', $link)); +} diff --git a/test.php b/test.php new file mode 100644 index 0000000..acd47f7 --- /dev/null +++ b/test.php @@ -0,0 +1,73 @@ +. + +/** + * Test that flask rest server is configured correctly + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/filelib.php'); + +$sendpdf = optional_param('sendpdf', 0, PARAM_BOOL); + +$PAGE->set_url(new moodle_url('/files/converter/flasksoffice/test.php')); +$PAGE->set_context(context_system::instance()); + +require_login(); +require_capability('moodle/site:config', context_system::instance()); + +$strheading = get_string('test_conversion', 'fileconverter_flasksoffice'); +$PAGE->navbar->add(get_string('administrationsite')); +$PAGE->navbar->add(get_string('plugins', 'admin')); +$PAGE->navbar->add(get_string('pluginname', 'fileconverter_flasksoffice'), + new moodle_url('/admin/settings.php', array('section' => 'fileconverterflasksoffice'))); +$PAGE->navbar->add($strheading); +$PAGE->set_heading($strheading); +$PAGE->set_title($strheading); + +$converter = new \fileconverter_flasksoffice\converter(); + +if ($sendpdf) { + require_sesskey(); + + $converter->serve_test_document(); + die(); +} + +$result = $converter->are_requirements_met(); +if ($result) { + $msg = $OUTPUT->notification(get_string('test_conversionready', 'fileconverter_flasksoffice'), 'success'); + $pdflink = new moodle_url($PAGE->url, array('sendpdf' => 1, 'sesskey' => sesskey())); + $msg .= html_writer::link($pdflink, get_string('test_conversion', 'fileconverter_flasksoffice')); + $msg .= html_writer::empty_tag('br'); +} else { + + // Diagnostics time. + $msg = ''; + + if (empty($msg)) { + $msg = $OUTPUT->notification(get_string('test_conversionnotready', 'fileconverter_flasksoffice'), 'warning'); + } +} +$returl = new moodle_url('/admin/settings.php', array('section' => 'fileconverterflasksoffice')); +$msg .= $OUTPUT->continue_button($returl); + +echo $OUTPUT->header(); +echo $OUTPUT->box($msg, 'generalbox'); +echo $OUTPUT->footer(); diff --git a/tests/fixtures/source.docx b/tests/fixtures/source.docx new file mode 100644 index 0000000000000000000000000000000000000000..286c58a89265855790206b771ad41b8960e96e62 GIT binary patch literal 11065 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ˇf5dz;xHOL_*nqznGZ#8{USV{{H=7i|u)qO40zs z-BkZEWl}-X%3TX?BpczkK0aeIs|~ap(e>MX0ZlyK1~>Y$o>*(61yLol2}hK9pJjy8 z4yZV$uN{e|XfBLhUKIGx!?`=g;8QX6Eop`WlAuN9K-O~Y0ZAT#33Zb-jEvvl3_17Y3`d9^#)d&&XWQ?|XX@b0bgXaK14- z4-O&djV6028eb}RePe7+`jN~RyW9kD^TV(f15QWm=3(9YnCqO(c`6?<{zC2_r&BOQ$?hnMh&p>?t%J)% zf5)Egs}Ou$bl}z#|%#bt0F%z_u3H$(z~X zRVC#u(YTv$!!MY4R}`!Fqogp|+7)Ti`Z;LXrf$XZDezW{R0KBhlL9%~6^HCVKb()f zdr&2GwrTM;tYfjR%Rb&JwRI%;-P3D6F6w zwm=K&^4W|Oa8r}MnaS{O(xDeQ?20Zh+=rnrd{!Irws^wyuqB_)W@wj^X?(M=*lp{* zJ}A1|!dz{;T{A|#`Bah3Hq#O*wxd;Wwb-IhG3)G4-+*eZ!m@Y8l6|D_^=7PVtdF%A zE`NxVCL}klxs>Bsv{|otq%gP=AJ^iod0rEwKg03o9EhZx{e8iqP z*@3md`H1;>s<_^iwg8UZZ`>{fx5H6Ng@-z&9*O?lav@?}ePS^|pAylIu5eMS&{ewe zm?{??=pjO7oB6kL3!l$=ZJ-CI%qkWdGeX!{0Q8}?A;z{?rkOWo89ZW1YML z(5>1lsSHv%^q0x&s7}mcj|bEJ`hGz7eEFs7UKarKr(J$PyNqFtMa0JxUR#Cxd5)3r z&j>&hOjCrxr$F5bkC{SBy;|_*&Z9ISB88|b;4LEk9JKyHlxLo=lm^J0$Z^pIjB)N* zqxlp7G~`^#rf7U}we^_-Px*=I2^Z2izGVs(|4G%iT1OMYKX%H+ z6pds$+Xq?@2pb_#_=*fWiC=0TVmzmu7X5Kez5$oX7iJ+no&Vbbq0V*m_&9SEx|Ypp z56KH*`90=D-7`#;8Mun{Y9cJ0t`<*(8w97Wt`owg2Fe{hlyf^U?(^I*)iyRT-xftJL zn3W0mH*6s!?L9GNjyz{hA>ZYeTSq9|X|M3ZTcSAT!g<(Q)xM8j9%ded61ZTo-Z*k{ zw{rJy#~ATZ7y_Hu_Xe&i@BM*f{4G;mSQy^ocM^hKA;v(iV8l!|mn{LbAc$%nDLw?Y z7NhZxaIIOh!uhk7kT^J=byCcv{hlr5nM<6=h{9JZR8r0YX9PrvzJ1zl1oVjvyELWu zuu1}##Y2)#!6=z>X6+1Yx9406R`_{&8dYgx-_5f}yPur)9`qrx%+SjBiLbVG=w+Fv zPu5pTOf0cpNj8?g=NGaNDg#T|&SAM%f>Pvv5^T;Gf~^hWQ9AvY=zsDA*%qMXk#16i zqIHmmFBAfZO({axKUslR$BzySuFJ=8#urxSCY2Cb=!+$B5vuhJZ^c6jE==X*$Q_(X zjc_qiiekw(8Axs1pX=?PQV9H_p+pJ&1-f0I@M@f4x|2N~Oy7Xa>A|Xf6xOwobcnB0 zO3kE_W=K`T-t&@6GAMJ;hPegzq@DE17HIn(F;P1eVVlb~%s$|&vy~Qb>@It3 zaQ;USE%~o<2n?Jav={&99>ZTH?r*n$=r~l6{=0y`cR~Gz1!RIMu-|&3{sjKLHRN|- z59mDb|J5k+r=&j{1^$xt0xI?Xqix_%_@76Rf5Ep9{}=w}f#jbe{v?0@5>bZ!?;`Uz z%HvP?pOn^La4Zn5@@oKpBe?#=|4Fm_h404sU;H1W%bybd&RzV42LLK?0f0Z5j6czT z-#z}0wj%fq{qMcxpYT76^1lQqgDAjX!~d&9SCEDRxyR3rI|M)%h-opR{`vGj$C+`Q literal 0 HcmV?d00001 diff --git a/version.php b/version.php new file mode 100644 index 0000000..427cc3f --- /dev/null +++ b/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details + * + * @package fileconverter_flasksoffice + * @copyright 2020 Mirko Otto + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2021021900; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2019052000; // Requires this Moodle version. +$plugin->component = 'fileconverter_flasksoffice'; // Full name of the plugin (used for diagnostics).