MDL-59629 mod_block: Set the default region in add_region
[moodle.git] / mod / assign / feedback / editpdf / classes / combined_document.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  * This file contains the combined document class for the assignfeedback_editpdf plugin.
19  *
20  * @package   assignfeedback_editpdf
21  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace assignfeedback_editpdf;
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * The combined_document class for the assignfeedback_editpdf plugin.
31  *
32  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
33  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 class combined_document {
37     /**
38      * Status value representing a conversion waiting to start.
39      */
40     const STATUS_PENDING_INPUT = 0;
42     /**
43      * Status value representing all documents ready to be combined.
44      */
45     const STATUS_READY = 1;
47     /**
48      * Status value representing a successful conversion.
49      */
50     const STATUS_COMPLETE = 2;
52     /**
53      * Status value representing a permanent error.
54      */
55     const STATUS_FAILED = -1;
57     /**
58      * The list of files which make this document.
59      */
60     protected $sourcefiles = [];
62     /**
63      * The resultant combined file.
64      */
65     protected $combinedfile;
67     /**
68      * The combination status.
69      */
70     protected $combinationstatus = null;
72     /**
73      * The number of pages in the combined PDF.
74      */
75     protected $pagecount = 0;
77     /**
78      * Check the current status of the document combination.
79      *
80      * @return  int
81      */
82     public function get_status() {
83         if ($this->combinedfile) {
84             // The combined file exists. Report success.
85             return self::STATUS_COMPLETE;
86         }
88         if (empty($this->sourcefiles)) {
89             // There are no source files to combine.
90             return self::STATUS_FAILED;
91         }
93         if (!empty($this->combinationstatus)) {
94             // The combination is in progress and has set a status.
95             // Return it instead.
96             return $this->combinationstatus;
97         }
99         $pending = false;
100         foreach ($this->sourcefiles as $file) {
101             // The combined file has not yet been generated.
102             // Check the status of each source file.
103             if (is_a($file, \core_files\conversion::class)) {
104                 $status = $file->get('status');
105                 switch ($status) {
106                     case \core_files\conversion::STATUS_IN_PROGRESS:
107                     case \core_files\conversion::STATUS_PENDING:
108                         $pending = true;
109                         break;
111                     case \core_files\conversion::STATUS_FAILED:
112                         return self::STATUS_FAILED;
113                 }
114             }
115         }
116         if ($pending) {
117             return self::STATUS_PENDING_INPUT;
118         } else {
119             return self::STATUS_READY;
120         }
121     }
122     /**
123      * Set the completed combined file.
124      *
125      * @param   stored_file $file The completed document for all files to be combined.
126      * @return  $this
127      */
128     public function set_combined_file($file) {
129         $this->combinedfile = $file;
131         return $this;
132     }
134     /**
135      * Retrieve the completed combined file.
136      *
137      * @return  stored_file
138      */
139     public function get_combined_file() {
140         return $this->combinedfile;
141     }
143     /**
144      * Set all source files which are to be combined.
145      *
146      * @param   stored_file|conversion[] $files The complete list of all source files to be combined.
147      * @return  $this
148      */
149     public function set_source_files($files) {
150         $this->sourcefiles = $files;
152         return $this;
153     }
155     /**
156      * Add an additional source file to the end of the existing list.
157      *
158      * @param   stored_file|conversion $file The file to add to the end of the list.
159      * @return  $this
160      */
161     public function add_source_file($file) {
162         $this->sourcefiles[] = $file;
164         return $this;
165     }
167     /**
168      * Retrieve the complete list of source files.
169      *
170      * @return  stored_file|conversion[]
171      */
172     public function get_source_files() {
173         return $this->sourcefiles;
174     }
176     /**
177      * Refresh the files.
178      *
179      * This includes polling any pending conversions to see if they are complete.
180      *
181      * @return  $this
182      */
183     public function refresh_files() {
184         $converter = new \core_files\converter();
185         foreach ($this->sourcefiles as $file) {
186             if (is_a($file, \core_files\conversion::class)) {
187                 $status = $file->get('status');
188                 switch ($status) {
189                     case \core_files\conversion::STATUS_COMPLETE:
190                         continue 2;
191                         break;
192                     default:
193                         $converter->poll_conversion($conversion);
194                 }
195             }
196         }
198         return $this;
199     }
201     /**
202      * Combine all source files into a single PDF and store it in the
203      * file_storage API using the supplied contextid and itemid.
204      *
205      * @param   int $contextid The contextid for the file to be stored under
206      * @param   int $itemid The itemid for the file to be stored under
207      * @return  $this
208      */
209     public function combine_files($contextid, $itemid) {
210         global $CFG;
212         $currentstatus = $this->get_status();
213         if ($currentstatus === self::STATUS_FAILED) {
214             $this->store_empty_document($contextid, $itemid);
216             return $this;
217         } else if ($currentstatus !== self::STATUS_READY) {
218             // The document is either:
219             // * already combined; or
220             // * pending input being fully converted; or
221             // * unable to continue due to an issue with the input documents.
222             //
223             // Exit early as we cannot continue.
224             return $this;
225         }
227         require_once($CFG->libdir . '/pdflib.php');
229         $pdf = new pdf();
230         $files = $this->get_source_files();
231         $compatiblepdfs = [];
233         foreach ($files as $file) {
234             // Check that each file is compatible and add it to the list.
235             // Note: We drop non-compatible files.
236             $compatiblepdf = false;
237             if (is_a($file, \core_files\conversion::class)) {
238                 $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
239             } else {
240                 $compatiblepdf = pdf::ensure_pdf_compatible($file);
241             }
243             if ($compatiblepdf) {
244                 $compatiblepdfs[] = $compatiblepdf;
245             }
246         }
248         $tmpdir = make_request_directory();
249         $tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME;
251         try {
252             $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
253             $pdf->Close();
254         } catch (\Exception $e) {
255             // Unable to combine the PDF.
256             debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
258             $pdf->Close();
259             return $this->mark_combination_failed();
260         }
262         // Verify the PDF.
263         $verifypdf = new pdf();
264         $verifypagecount = $verifypdf->load_pdf($tmpfile);
265         $verifypdf->Close();
267         if ($verifypagecount <= 0) {
268             // No pages were found in the combined PDF.
269             return $this->mark_combination_failed();
270         }
272         // Store the newly created file as a stored_file.
273         $this->store_combined_file($tmpfile, $contextid, $itemid);
275         // Note the verified page count.
276         $this->pagecount = $verifypagecount;
278         return $this;
279     }
281     /**
282      * Mark the combination attempt as having encountered a permanent failure.
283      *
284      * @return  $this
285      */
286     protected function mark_combination_failed() {
287         $this->combinationstatus = self::STATUS_FAILED;
289         return $this;
290     }
292     /**
293      * Store the combined file in the file_storage API.
294      *
295      * @param   string $tmpfile The path to the file on disk to be stored.
296      * @param   int $contextid The contextid for the file to be stored under
297      * @param   int $itemid The itemid for the file to be stored under
298      * @return  $this
299      */
300     protected function store_combined_file($tmpfile, $contextid, $itemid) {
301         // Store the file.
302         $record = $this->get_stored_file_record($contextid, $itemid);
303         $fs = get_file_storage();
305         // Delete existing files first.
306         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
308         // This was a combined pdf.
309         $file = $fs->create_file_from_pathname($record, $tmpfile);
311         $this->set_combined_file($file);
313         return $this;
314     }
316     /**
317      * Store the empty document file in the file_storage API.
318      *
319      * @param   int $contextid The contextid for the file to be stored under
320      * @param   int $itemid The itemid for the file to be stored under
321      * @return  $this
322      */
323     protected function store_empty_document($contextid, $itemid) {
324         // Store the file.
325         $record = $this->get_stored_file_record($contextid, $itemid);
326         $fs = get_file_storage();
328         // Delete existing files first.
329         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
331         $file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64));
332         $this->pagecount = 1;
334         $this->set_combined_file($file);
336         return $this;
337     }
339     /**
340      * Get the total number of pages in the combined document.
341      *
342      * If there are no pages, or it is not yet possible to count them a
343      * value of 0 is returned.
344      *
345      * @return  int
346      */
347     public function get_page_count() {
348         if ($this->pagecount) {
349             return $this->pagecount;
350         }
352         if ($this->get_status() === self::STATUS_FAILED) {
353             // The empty document will be returned.
354             return 1;
355         }
357         if ($this->get_status() !== self::STATUS_COMPLETE) {
358             // No pages yet.
359             return 0;
360         }
362         // Load the PDF to determine the page count.
363         $temparea = make_request_directory();
364         $tempsrc = $temparea . "/source.pdf";
365         $this->get_combined_file()->copy_content_to($tempsrc);
367         $pdf = new pdf();
368         $pagecount = $pdf->load_pdf($tempsrc);
369         $pdf->Close();
371         if ($pagecount <= 0) {
372             // Something went wrong. Return an empty page count again.
373             return 0;
374         }
376         $this->pagecount = $pagecount;
377         return $this->pagecount;
378     }
380     /**
381      * Get the total number of documents to be combined.
382      *
383      * @return  int
384      */
385     public function get_document_count() {
386         return count($this->sourcefiles);
387     }
389     /**
390      * Helper to fetch the stored_file record.
391      *
392      * @param   int $contextid The contextid for the file to be stored under
393      * @param   int $itemid The itemid for the file to be stored under
394      * @return  stdClass
395      */
396     protected function get_stored_file_record($contextid, $itemid) {
397         return (object) [
398             'contextid' => $contextid,
399             'component' => 'assignfeedback_editpdf',
400             'filearea' => document_services::COMBINED_PDF_FILEAREA,
401             'itemid' => $itemid,
402             'filepath' => '/',
403             'filename' => document_services::COMBINED_PDF_FILENAME,
404         ];
405     }