Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}