Merge branch 'MDL-58272-master-2' of https://github.com/snake/moodle
[moodle.git] / files / classes / converter.php
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/>.
17 /**
18  * Class for converting files between different file formats using unoconv.
19  *
20  * @package    core_files
21  * @copyright  2017 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core_files;
26 defined('MOODLE_INTERNAL') || die();
28 use stored_file;
30 /**
31  * Class for converting files between different formats using unoconv.
32  *
33  * @package    core_files
34  * @copyright  2017 Damyon Wiese
35  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 class converter {
39     /**
40      * Get a list of enabled plugins and classes.
41      *
42      * @return  array List of enabled plugins
43      */
44     protected function get_enabled_plugins() {
45         $plugins = \core\plugininfo\fileconverter::get_enabled_plugins();
47         $pluginclasses = [];
48         foreach ($plugins as $plugin) {
49             $pluginclasses[$plugin] = \core\plugininfo\fileconverter::get_classname($plugin);
50         }
52         return $pluginclasses;
53     }
55     /**
56      * Return the file_storage API.
57      *
58      * This allows for mocking of the file_storage API.
59      *
60      * @return \file_storage
61      */
62     protected function get_file_storage() {
63         return get_file_storage();
64     }
66     /**
67      * Start the conversion for a stored_file into a new format.
68      *
69      * @param   stored_file $file The file to convert
70      * @param   string $format The desired target file format (file extension)
71      * @param   boolean $forcerefresh If true, the file will be converted every time (not cached).
72      * @return  conversion conversion object
73      */
74     public function start_conversion(stored_file $file, $format, $forcerefresh = false) {
75         $conversions = conversion::get_conversions_for_file($file, $format);
77         if ($forcerefresh || count($conversions) > 1) {
78             while ($conversion = array_shift($conversions)) {
79                 if ($conversion->get('id')) {
80                     $conversion->delete();
81                 }
82             }
83         }
85         if (empty($conversions)) {
86             $conversion = new conversion(0, (object) [
87                 'sourcefileid' => $file->get_id(),
88                 'targetformat' => $format,
89             ]);
90             $conversion->create();
91         } else {
92             $conversion = array_shift($conversions);
93         }
95         if ($conversion->get('status') !== conversion::STATUS_COMPLETE) {
96             $this->poll_conversion($conversion);
97         }
99         return $conversion;
100     }
102     /**
103      * Poll for updates to the supplied conversion.
104      *
105      * @param   conversion $conversion The conversion in progress
106      * @return  $this
107      */
108     public function poll_conversion(conversion $conversion) {
109         $format = $conversion->get('targetformat');
110         $file = $conversion->get_sourcefile();
112         if ($conversion->get('status') == conversion::STATUS_IN_PROGRESS) {
113             // The current conversion is in progress.
114             // Check for updates.
115             if ($instance = $conversion->get_converter_instance()) {
116                 $instance->poll_conversion_status($conversion);
117             } else {
118                 // Unable to fetch the converter instance.
119                 // Reset the status back to PENDING so that it may be picked up again.
120                 $conversion->set('status', conversion::STATUS_PENDING);
121             }
122             $conversion->update();
123         }
125         // Refresh the status.
126         $status = $conversion->get('status');
127         if ($status === conversion::STATUS_PENDING || $status === conversion::STATUS_FAILED) {
128             // The current status is either pending or failed.
129             // Attempt to pick up a new converter and convert the document.
130             $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
131             $converters = $this->get_document_converter_classes($from, $format);
132             $currentconverter = $this->get_next_converter($converters, $conversion->get('converter'));
134             if (!$currentconverter) {
135                 // No more converters available.
136                 $conversion->set('status', conversion::STATUS_FAILED);
137                 $conversion->update();
138                 return $this;
139             }
141             do {
142                 $conversion
143                     ->set('converter', $currentconverter)
144                     ->set('status', conversion::STATUS_IN_PROGRESS)
145                     ->update();
147                 $instance = $conversion->get_converter_instance();
148                 $instance->start_document_conversion($conversion);
149                 $failed = $conversion->get('status') === conversion::STATUS_FAILED;
150                 $currentconverter = $this->get_next_converter($converters, $currentconverter);
151             } while ($failed && $currentconverter);
153             $conversion->update();
154         }
156         return $this;
157     }
159     /**
160      * Fetch the next converter to try.
161      *
162      * @param   array $converters The list of converters to try
163      * @param   string|null $currentconverter The converter currently in use
164      * @return  string|false Name of next converter if present
165      */
166     protected function get_next_converter($converters, $currentconverter = null) {
167         if ($currentconverter) {
168             $keys = array_keys($converters, $currentconverter);
169             $key = $keys[0];
170             if (isset($converters[$key + 1])) {
171                 return $converters[$key + 1];
172             } else {
173                 return false;
174             }
175         } else if (!empty($converters)) {
176             return $converters[0];
177         } else {
178             return false;
179         }
180     }
182     /**
183      * Fetch the class for the preferred document converter.
184      *
185      * @param   string $from The source target file (file extension)
186      * @param   string $to The desired target file format (file extension)
187      * @return  string The class for document conversion
188      */
189     protected function get_document_converter_classes($from, $to) {
190         $classes = [];
192         $converters = $this->get_enabled_plugins();
193         foreach ($converters as $plugin => $classname) {
194             if (!class_exists($classname)) {
195                 continue;
196             }
198             if (!$classname::are_requirements_met()) {
199                 continue;
200             }
202             if ($classname::supports($from, $to)) {
203                 $classes[] = $classname;
204             }
205         }
207         return $classes;
208     }
210     /**
211      * Check whether document conversion is supported for this file and target format.
212      *
213      * @param   stored_file $file The file to convert
214      * @param   string $to The desired target file format (file extension)
215      * @return  bool Whether the target type can be converted
216      */
217     public function can_convert_storedfile_to(stored_file $file, $to) {
218         if ($file->is_directory()) {
219             // Directories cannot be converted.
220             return false;
221         }
223         if (!$file->get_filesize()) {
224             // Empty files cannot be converted.
225             return false;
226         }
228         $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
229         if (!$from) {
230             // No file extension could be found. Unable to determine converter.
231             return false;
232         }
234         return $this->can_convert_format_to($from, $to);
235     }
237     /**
238      * Check whether document conversion is supported for this file and target format.
239      *
240      * @param   string $from The source target file (file extension)
241      * @param   string $to The desired target file format (file extension)
242      * @return  bool Whether the target type can be converted
243      */
244     public function can_convert_format_to($from, $to) {
245         return !empty($this->get_document_converter_classes($from, $to));
246     }