Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Mirko Otto committed Feb 25, 2021
0 parents commit 872bb73
Show file tree
Hide file tree
Showing 13 changed files with 866 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CONTRIBUTIONS.txt
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 \<path to your moodle dir\>/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.
312 changes: 312 additions & 0 deletions classes/converter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
<?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 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);
}
}
Loading

0 comments on commit 872bb73

Please sign in to comment.