. /** * Class for converting files between different file formats using unoconv. * * @package fileconverter_unoconv * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace fileconverter_unoconv; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); use stored_file; use \core_files\conversion; /** * Class for converting files between different formats using unoconv. * * @package fileconverter_unoconv * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class converter implements \core_files\converter_interface { /** No errors */ const UNOCONVPATH_OK = 'ok'; /** Not set */ const UNOCONVPATH_EMPTY = 'empty'; /** Does not exist */ const UNOCONVPATH_DOESNOTEXIST = 'doesnotexist'; /** Is a dir */ const UNOCONVPATH_ISDIR = 'isdir'; /** Not executable */ const UNOCONVPATH_NOTEXECUTABLE = 'notexecutable'; /** Test file missing */ const UNOCONVPATH_NOTESTFILE = 'notestfile'; /** Version not supported */ const UNOCONVPATH_VERSIONNOTSUPPORTED = 'versionnotsupported'; /** Any other error */ const UNOCONVPATH_ERROR = 'error'; /** * @var bool $requirementsmet Whether requirements have been met. */ protected static $requirementsmet = null; /** * @var array $formats The list of formats supported by unoconv. */ protected static $formats; /** * 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(\core_files\conversion $conversion) { global $CFG; if (!self::are_requirements_met()) { $conversion->set('status', conversion::STATUS_FAILED); return $this; } $file = $conversion->get_sourcefile(); // Sanity check that the conversion is supported. $fromformat = pathinfo($file->get_filename(), PATHINFO_EXTENSION); if (!self::is_format_supported($fromformat)) { $conversion->set('status', conversion::STATUS_FAILED); return $this; } $format = $conversion->get('targetformat'); if (!self::is_format_supported($format)) { $conversion->set('status', conversion::STATUS_FAILED); return $this; } // Copy the file to the tmp dir. $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; try { // This function can either return false, or throw an exception so we need to handle both. if ($file->copy_content_to($filename) === false) { throw new \file_exception('storedfileproblem', 'Could not copy file contents to temp file.'); } } catch (\file_exception $fe) { throw $fe; } // The temporary file to copy into. $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format; $newtmpfile = $uniqdir . '/' . clean_param($newtmpfile, PARAM_FILE); $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' . escapeshellarg('-f') . ' ' . escapeshellarg($format) . ' ' . escapeshellarg('-o') . ' ' . escapeshellarg($newtmpfile) . ' ' . escapeshellarg($filename); $output = null; $currentdir = getcwd(); chdir($uniqdir); $result = exec($cmd, $output); chdir($currentdir); touch($newtmpfile); if (filesize($newtmpfile) === 0) { $conversion->set('status', conversion::STATUS_FAILED); return $this; } $conversion ->store_destfile_from_path($newtmpfile) ->set('status', conversion::STATUS_COMPLETE) ->update(); return $this; } /** * Poll an existing conversion for status update. * * @param conversion $conversion The file to be converted * @return $this */ public function poll_conversion_status(conversion $conversion) { // Unoconv does not support asynchronous conversion. return $this; } /** * Generate and serve the test document. * * @return void */ 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_unoconv', 'itemid' => 0, 'filepath' => '/', 'filename' => 'unoconv_test.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/unoconv-source.docx'; $testdocx = $fs->create_file_from_pathname($filerecord, $fixturefile); } $conversions = conversion::get_conversions_for_file($testdocx, $format); foreach ($conversions as $conversion) { $conversion->delete(); } $conversion = new conversion(0, (object) [ 'sourcefileid' => $testdocx->get_id(), 'targetformat' => $format, ]); $conversion->create(); // Convert the doc file to the target format and send it direct to the browser. $this->start_document_conversion($conversion); do { sleep(1); $this->poll_conversion_status($conversion); $status = $conversion->get('status'); } while ($status !== conversion::STATUS_COMPLETE && $status !== conversion::STATUS_FAILED); readfile_accel($conversion->get_destfile(), 'application/pdf', true); } /** * Whether the plugin is configured and requirements are met. * * @return bool */ public static function are_requirements_met() { if (self::$requirementsmet === null) { $requirementsmet = self::test_unoconv_path()->status === self::UNOCONVPATH_OK; $requirementsmet = $requirementsmet && self::is_minimum_version_met(); self::$requirementsmet = $requirementsmet; } return self::$requirementsmet; } /** * Whether the minimum version of unoconv has been met. * * @return bool */ protected static function is_minimum_version_met() { global $CFG; $currentversion = 0; $supportedversion = 0.7; $unoconvbin = \escapeshellarg($CFG->pathtounoconv); $command = "$unoconvbin --version"; exec($command, $output); // If the command execution returned some output, then get the unoconv version. if ($output) { foreach ($output as $response) { if (preg_match('/unoconv (\\d+\\.\\d+)/', $response, $matches)) { $currentversion = (float) $matches[1]; } } if ($currentversion < $supportedversion) { return false; } else { return true; } } return false; } /** * Whether the plugin is fully configured. * * @return \stdClass */ public static function test_unoconv_path() { global $CFG; $unoconvpath = $CFG->pathtounoconv; $ret = new \stdClass(); $ret->status = self::UNOCONVPATH_OK; $ret->message = null; if (empty($unoconvpath)) { $ret->status = self::UNOCONVPATH_EMPTY; return $ret; } if (!file_exists($unoconvpath)) { $ret->status = self::UNOCONVPATH_DOESNOTEXIST; return $ret; } if (is_dir($unoconvpath)) { $ret->status = self::UNOCONVPATH_ISDIR; return $ret; } if (!\file_is_executable($unoconvpath)) { $ret->status = self::UNOCONVPATH_NOTEXECUTABLE; return $ret; } if (!self::is_minimum_version_met()) { $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED; return $ret; } return $ret; } /** * 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 self::is_format_supported($from) && self::is_format_supported($to); } /** * Whether the specified file format is supported. * * @param string $format Whether conversions between this format and another are supported * @return bool */ protected static function is_format_supported($format) { $formats = self::fetch_supported_formats(); $format = trim(\core_text::strtolower($format)); return in_array($format, $formats); } /** * Fetch the list of supported file formats. * * @return array */ protected static function fetch_supported_formats() { global $CFG; if (!isset(self::$formats)) { // Ask unoconv for it's list of supported document formats. $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show'; $pipes = array(); $pipesspec = array(2 => array('pipe', 'w')); $proc = proc_open($cmd, $pipesspec, $pipes); $programoutput = stream_get_contents($pipes[2]); fclose($pipes[2]); proc_close($proc); $matches = array(); preg_match_all('/\[\.(.*)\]/', $programoutput, $matches); $formats = $matches[1]; self::$formats = array_unique($formats); } return self::$formats; } /** * A list of the supported conversions. * * @return string */ public function get_supported_conversions() { return implode(', ', self::fetch_supported_formats()); } }