MDL-67547 dataformat_pdf: method to convert images to supported format.
[moodle.git] / lib / classes / dataformat / base.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  * Base class for dataformat.
19  *
20  * @package    core
21  * @subpackage dataformat
22  * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 namespace core\dataformat;
28 use coding_exception;
30 /**
31  * Base class for dataformat.
32  *
33  * @package    core
34  * @subpackage dataformat
35  * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 abstract class base {
40     /** @var $mimetype */
41     protected $mimetype = "text/plain";
43     /** @var $extension */
44     protected $extension = ".txt";
46     /** @var $filename */
47     protected $filename = '';
49     /** @var string The location to store the output content */
50     protected $filepath = '';
52     /**
53      * Get the file extension
54      *
55      * @return string file extension
56      */
57     public function get_extension() {
58         return $this->extension;
59     }
61     /**
62      * Set download filename base
63      *
64      * @param string $filename
65      */
66     public function set_filename($filename) {
67         $this->filename = $filename;
68     }
70     /**
71      * Set file path when writing to file
72      *
73      * @param string $filepath
74      * @throws coding_exception
75      */
76     public function set_filepath(string $filepath): void {
77         $filedir = dirname($filepath);
78         if (!is_writable($filedir)) {
79             throw new coding_exception('File path is not writable');
80         }
82         $this->filepath = $filepath;
84         // Some dataformat writers may expect filename to be set too.
85         $this->set_filename(pathinfo($this->filepath, PATHINFO_FILENAME));
86     }
88     /**
89      * Set the title of the worksheet inside a spreadsheet
90      *
91      * For some formats this will be ignored.
92      *
93      * @param string $title
94      */
95     public function set_sheettitle($title) {
96     }
98     /**
99      * Output file headers to initialise the download of the file.
100      */
101     public function send_http_headers() {
102         if (defined('BEHAT_SITE_RUNNING') || PHPUNIT_TEST) {
103             // For text based formats - we cannot test the output with behat if we force a file download.
104             return;
105         }
106         if (is_https()) {
107             // HTTPS sites - watch out for IE! KB812935 and KB316431.
108             header('Cache-Control: max-age=10');
109             header('Pragma: ');
110         } else {
111             // Normal http - prevent caching at all cost.
112             header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
113             header('Pragma: no-cache');
114         }
115         header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
116         header("Content-Type: $this->mimetype\n");
117         $filename = $this->filename . $this->get_extension();
118         header("Content-Disposition: attachment; filename=\"$filename\"");
119     }
121     /**
122      * Set the dataformat to be output to current file. Calling code must call {@see base::close_output_to_file()} when finished
123      */
124     public function start_output_to_file(): void {
125         // Raise memory limit to ensure we can store the entire content. Start collecting output.
126         raise_memory_limit(MEMORY_EXTRA);
128         ob_start();
129         $this->start_output();
130     }
132     /**
133      * Write the start of the file.
134      */
135     public function start_output() {
136         // Override me if needed.
137     }
139     /**
140      * Write the start of the sheet we will be adding data to.
141      *
142      * @param array $columns
143      */
144     public function start_sheet($columns) {
145         // Override me if needed.
146     }
148     /**
149      * Method to define whether the dataformat supports export of HTML
150      *
151      * @return bool
152      */
153     public function supports_html(): bool {
154         return false;
155     }
157     /**
158      * Apply formatting to the cells of a given record
159      *
160      * @param array|\stdClass $record
161      * @return array
162      */
163     protected function format_record($record): array {
164         $record = (array)$record;
166         // If the dataformat supports export of HTML, we need to allow them to manage embedded images.
167         if ($this->supports_html()) {
168             $record = array_map([$this, 'replace_pluginfile_images'], $record);
169         }
171         return $record;
172     }
174     /**
175      * Given a stored_file, return a suitable source attribute for an img element in the export (or null to use the original)
176      *
177      * @param \stored_file $file
178      * @return string|null
179      */
180     protected function export_html_image_source(\stored_file $file): ?string {
181         return null;
182     }
184     /**
185      * We need to locate all img tags within a given cell that match pluginfile URL's. Partly so the exported file will show
186      * the image without requiring the user is logged in; and also to prevent some of the dataformats requesting the file
187      * themselves, which is likely to fail due to them not having an active session
188      *
189      * @param string|null $content
190      * @return string
191      */
192     protected function replace_pluginfile_images(?string $content): string {
193         $content = (string)$content;
195         // Examine content to see if it contains any HTML image tags.
196         return preg_replace_callback('/(?<pre><img[^>]+src=")(?<source>[^"]*)(?<post>".*>)/i', function(array $matches) {
197             $source = $matches['source'];
199             // Now check if the image source looks like a pluginfile URL.
200             if (preg_match('/pluginfile.php\/(?<context>\d+)\/(?<component>[^\/]+)\/(?<filearea>[^\/]+)\/(?:(?<itemid>\d+)\/)?' .
201                     '(?<path>.*)/u', $source, $args)) {
203                 $context = $args['context'];
204                 $component = clean_param($args['component'], PARAM_COMPONENT);
205                 $filearea = clean_param($args['filearea'], PARAM_AREA);
206                 $itemid = $args['itemid'] ?: 0;
207                 $path = clean_param(urldecode($args['path']), PARAM_PATH);
209                 // Try and get the matching file from storage, allow the dataformat to define the replacement source.
210                 $fullpath = "/{$context}/{$component}/{$filearea}/{$itemid}/{$path}";
211                 if ($file = get_file_storage()->get_file_by_hash(sha1($fullpath))) {
212                     $exportsource = $this->export_html_image_source($file);
214                     if ($exportsource) {
215                         $source = $exportsource;
216                     }
217                 }
218             }
220             return $matches['pre'] . $source . $matches['post'];
221         }, $content);
222     }
224     /**
225      * Write a single record
226      *
227      * @param array $record
228      * @param int $rownum
229      */
230     abstract public function write_record($record, $rownum);
232     /**
233      * Write the end of the sheet containing the data.
234      *
235      * @param array $columns
236      */
237     public function close_sheet($columns) {
238         // Override me if needed.
239     }
241     /**
242      * Write the end of the file.
243      */
244     public function close_output() {
245         // Override me if needed.
246     }
248     /**
249      * Write the data to disk. Calling code should have previously called {@see base::start_output_to_file()}
250      *
251      * @return bool Whether the write succeeded
252      */
253     public function close_output_to_file(): bool {
254         $this->close_output();
256         $filecontent = ob_get_contents();
257         ob_end_clean();
259         return file_put_contents($this->filepath, $filecontent) !== false;
260     }