1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Class for converting files between different file formats using unoconv. 19 * 20 * @package fileconverter_unoconv 21 * @copyright 2017 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace fileconverter_unoconv; 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->libdir . '/filelib.php'); 29 30 use stored_file; 31 use \core_files\conversion; 32 33 /** 34 * Class for converting files between different formats using unoconv. 35 * 36 * @package fileconverter_unoconv 37 * @copyright 2017 Damyon Wiese 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class converter implements \core_files\converter_interface { 41 42 /** No errors */ 43 const UNOCONVPATH_OK = 'ok'; 44 45 /** Not set */ 46 const UNOCONVPATH_EMPTY = 'empty'; 47 48 /** Does not exist */ 49 const UNOCONVPATH_DOESNOTEXIST = 'doesnotexist'; 50 51 /** Is a dir */ 52 const UNOCONVPATH_ISDIR = 'isdir'; 53 54 /** Not executable */ 55 const UNOCONVPATH_NOTEXECUTABLE = 'notexecutable'; 56 57 /** Test file missing */ 58 const UNOCONVPATH_NOTESTFILE = 'notestfile'; 59 60 /** Version not supported */ 61 const UNOCONVPATH_VERSIONNOTSUPPORTED = 'versionnotsupported'; 62 63 /** Any other error */ 64 const UNOCONVPATH_ERROR = 'error'; 65 66 /** 67 * @var bool $requirementsmet Whether requirements have been met. 68 */ 69 protected static $requirementsmet = null; 70 71 /** 72 * @var array $formats The list of formats supported by unoconv. 73 */ 74 protected static $formats; 75 76 /** 77 * Convert a document to a new format and return a conversion object relating to the conversion in progress. 78 * 79 * @param conversion $conversion The file to be converted 80 * @return $this 81 */ 82 public function start_document_conversion(\core_files\conversion $conversion) { 83 global $CFG; 84 85 if (!self::are_requirements_met()) { 86 $conversion->set('status', conversion::STATUS_FAILED); 87 error_log( 88 "Unoconv conversion failed to verify the configuraton meets the minimum requirements. " . 89 "Please check the unoconv installation configuration." 90 ); 91 return $this; 92 } 93 94 $file = $conversion->get_sourcefile(); 95 $filepath = $file->get_filepath(); 96 97 // Sanity check that the conversion is supported. 98 $fromformat = pathinfo($file->get_filename(), PATHINFO_EXTENSION); 99 if (!self::is_format_supported($fromformat)) { 100 $conversion->set('status', conversion::STATUS_FAILED); 101 error_log( 102 "Unoconv conversion for '" . $filepath . "' found input '" . $fromformat . "' " . 103 "file extension to convert from is not supported." 104 ); 105 return $this; 106 } 107 108 $format = $conversion->get('targetformat'); 109 if (!self::is_format_supported($format)) { 110 $conversion->set('status', conversion::STATUS_FAILED); 111 error_log( 112 "Unoconv conversion for '" . $filepath . "' found output '" . $format . "' " . 113 "file extension to convert to is not supported." 114 ); 115 return $this; 116 } 117 118 // Copy the file to the tmp dir. 119 $uniqdir = make_unique_writable_directory(make_temp_directory('core_file/conversions')); 120 \core_shutdown_manager::register_function('remove_dir', array($uniqdir)); 121 $localfilename = $file->get_id() . '.' . $fromformat; 122 123 $filename = $uniqdir . '/' . $localfilename; 124 try { 125 // This function can either return false, or throw an exception so we need to handle both. 126 if ($file->copy_content_to($filename) === false) { 127 throw new \file_exception('storedfileproblem', 'Could not copy file contents to temp file.'); 128 } 129 } catch (\file_exception $fe) { 130 error_log( 131 "Unoconv conversion for '" . $filepath . "' encountered disk permission error when copying " . 132 "submitted file contents to unique temp file: '" . $filename . "'." 133 ); 134 throw $fe; 135 } 136 137 // The temporary file to copy into. 138 $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format; 139 $newtmpfile = $uniqdir . '/' . clean_param($newtmpfile, PARAM_FILE); 140 141 $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' . 142 escapeshellarg('-f') . ' ' . 143 escapeshellarg($format) . ' ' . 144 escapeshellarg('-o') . ' ' . 145 escapeshellarg($newtmpfile) . ' ' . 146 escapeshellarg($filename); 147 148 $output = null; 149 $currentdir = getcwd(); 150 chdir($uniqdir); 151 $result = exec($cmd, $output, $returncode); 152 chdir($currentdir); 153 touch($newtmpfile); 154 155 if ($returncode != 0) { 156 $conversion->set('status', conversion::STATUS_FAILED); 157 error_log( 158 "Unoconv conversion for '" . $filepath . "' from '" . $fromformat . "' to '" . $format . "' " . 159 "was unsuccessful; returned with exit status code (" . $returncode . "). Please check the unoconv " . 160 "configuration and conversion file content / format." 161 ); 162 return $this; 163 } 164 165 if (!file_exists($newtmpfile)) { 166 $conversion->set('status', conversion::STATUS_FAILED); 167 error_log( 168 "Unoconv conversion for '" . $filepath . "' from '" . $fromformat . "' to '" . $format . "' " . 169 "was unsuccessful; the output file was not found in '" . $newtmpfile . "'. Please check the disk " . 170 "permissions." 171 ); 172 return $this; 173 } 174 175 if (filesize($newtmpfile) === 0) { 176 $conversion->set('status', conversion::STATUS_FAILED); 177 error_log( 178 "Unoconv conversion for '" . $filepath . "' from '" . $fromformat . "' to '" . $format . "' " . 179 "was unsuccessful; the output file size has 0 bytes in '" . $newtmpfile . "'. Please check the " . 180 "conversion file content / format with the command: [ " . $cmd . " ]" 181 ); 182 return $this; 183 } 184 185 $conversion 186 ->store_destfile_from_path($newtmpfile) 187 ->set('status', conversion::STATUS_COMPLETE) 188 ->update(); 189 190 return $this; 191 } 192 193 /** 194 * Poll an existing conversion for status update. 195 * 196 * @param conversion $conversion The file to be converted 197 * @return $this 198 */ 199 public function poll_conversion_status(conversion $conversion) { 200 // Unoconv does not support asynchronous conversion. 201 return $this; 202 } 203 204 /** 205 * Generate and serve the test document. 206 * 207 * @return void 208 */ 209 public function serve_test_document() { 210 global $CFG; 211 require_once($CFG->libdir . '/filelib.php'); 212 213 $format = 'pdf'; 214 215 $filerecord = [ 216 'contextid' => \context_system::instance()->id, 217 'component' => 'test', 218 'filearea' => 'fileconverter_unoconv', 219 'itemid' => 0, 220 'filepath' => '/', 221 'filename' => 'unoconv_test.docx' 222 ]; 223 224 // Get the fixture doc file content and generate and stored_file object. 225 $fs = get_file_storage(); 226 $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'], 227 $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']); 228 229 if (!$testdocx) { 230 $fixturefile = dirname(__DIR__) . '/tests/fixtures/unoconv-source.docx'; 231 $testdocx = $fs->create_file_from_pathname($filerecord, $fixturefile); 232 } 233 234 $conversions = conversion::get_conversions_for_file($testdocx, $format); 235 foreach ($conversions as $conversion) { 236 if ($conversion->get('id')) { 237 $conversion->delete(); 238 } 239 } 240 241 $conversion = new conversion(0, (object) [ 242 'sourcefileid' => $testdocx->get_id(), 243 'targetformat' => $format, 244 ]); 245 $conversion->create(); 246 247 // Convert the doc file to the target format and send it direct to the browser. 248 $this->start_document_conversion($conversion); 249 do { 250 sleep(1); 251 $this->poll_conversion_status($conversion); 252 $status = $conversion->get('status'); 253 } while ($status !== conversion::STATUS_COMPLETE && $status !== conversion::STATUS_FAILED); 254 255 readfile_accel($conversion->get_destfile(), 'application/pdf', true); 256 } 257 258 /** 259 * Whether the plugin is configured and requirements are met. 260 * 261 * @return bool 262 */ 263 public static function are_requirements_met() { 264 if (self::$requirementsmet === null) { 265 $requirementsmet = self::test_unoconv_path()->status === self::UNOCONVPATH_OK; 266 $requirementsmet = $requirementsmet && self::is_minimum_version_met(); 267 self::$requirementsmet = $requirementsmet; 268 } 269 270 return self::$requirementsmet; 271 } 272 273 /** 274 * Whether the minimum version of unoconv has been met. 275 * 276 * @return bool 277 */ 278 protected static function is_minimum_version_met() { 279 global $CFG; 280 281 $currentversion = 0; 282 $supportedversion = 0.7; 283 $unoconvbin = \escapeshellarg($CFG->pathtounoconv); 284 $command = "$unoconvbin --version"; 285 exec($command, $output); 286 287 // If the command execution returned some output, then get the unoconv version. 288 if ($output) { 289 foreach ($output as $response) { 290 if (preg_match('/unoconv (\\d+\\.\\d+)/', $response, $matches)) { 291 $currentversion = (float) $matches[1]; 292 } 293 } 294 if ($currentversion < $supportedversion) { 295 return false; 296 } else { 297 return true; 298 } 299 } 300 301 return false; 302 } 303 304 /** 305 * Whether the plugin is fully configured. 306 * 307 * @return \stdClass 308 */ 309 public static function test_unoconv_path() { 310 global $CFG; 311 312 $unoconvpath = $CFG->pathtounoconv; 313 314 $ret = new \stdClass(); 315 $ret->status = self::UNOCONVPATH_OK; 316 $ret->message = null; 317 318 if (empty($unoconvpath)) { 319 $ret->status = self::UNOCONVPATH_EMPTY; 320 return $ret; 321 } 322 if (!file_exists($unoconvpath)) { 323 $ret->status = self::UNOCONVPATH_DOESNOTEXIST; 324 return $ret; 325 } 326 if (is_dir($unoconvpath)) { 327 $ret->status = self::UNOCONVPATH_ISDIR; 328 return $ret; 329 } 330 if (!\file_is_executable($unoconvpath)) { 331 $ret->status = self::UNOCONVPATH_NOTEXECUTABLE; 332 return $ret; 333 } 334 if (!self::is_minimum_version_met()) { 335 $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED; 336 return $ret; 337 } 338 339 return $ret; 340 341 } 342 343 /** 344 * Whether a file conversion can be completed using this converter. 345 * 346 * @param string $from The source type 347 * @param string $to The destination type 348 * @return bool 349 */ 350 public static function supports($from, $to) { 351 return self::is_format_supported($from) && self::is_format_supported($to); 352 } 353 354 /** 355 * Whether the specified file format is supported. 356 * 357 * @param string $format Whether conversions between this format and another are supported 358 * @return bool 359 */ 360 protected static function is_format_supported($format) { 361 $formats = self::fetch_supported_formats(); 362 363 $format = trim(\core_text::strtolower($format)); 364 return in_array($format, $formats); 365 } 366 367 /** 368 * Fetch the list of supported file formats. 369 * 370 * @return array 371 */ 372 protected static function fetch_supported_formats() { 373 global $CFG; 374 375 if (!isset(self::$formats)) { 376 // Ask unoconv for it's list of supported document formats. 377 $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show'; 378 $pipes = array(); 379 $pipesspec = array(2 => array('pipe', 'w')); 380 $proc = proc_open($cmd, $pipesspec, $pipes); 381 $programoutput = stream_get_contents($pipes[2]); 382 fclose($pipes[2]); 383 proc_close($proc); 384 $matches = array(); 385 preg_match_all('/\[\.(.*)\]/', $programoutput, $matches); 386 387 $formats = $matches[1]; 388 self::$formats = array_unique($formats); 389 } 390 391 return self::$formats; 392 } 393 394 /** 395 * A list of the supported conversions. 396 * 397 * @return string 398 */ 399 public function get_supported_conversions() { 400 return implode(', ', self::fetch_supported_formats()); 401 } 402 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body