aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/exportdocument/ExportPDF.php
blob: f676c4516c84977174519791f7718cc7454b0027 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
<?php
# Lifter010: TODO
/**
 * ExportPDF.php - create and export or save a pdf with simple HTML-Data
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Rasmus Fuhse & Peter Thienel
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */

define('K_PATH_IMAGES', $GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/images/');

/**
 * Class to create an PDF by putting in Stud.IP-formatted code.
 * Usage:
 *
 *  $doc = new ExportPDF();
 *  $doc->addPage();
 *  $doc->addContent('Hallo, %%wir%% benutzen :studip:-Formatierung.');
 *  $doc->dispatch("test_pdf");
 *  //lines following dispatch won't be accessed anymor, because dispatch
 *  //cancels all other output.
 *
 */
class ExportPDF extends TCPDF implements ExportDocument
{
    private $media_proxy = NULL;
    private $config;
    private $defaults = false;
    private $page_added = false;
    private $h_title = '';
    private $h_string = '';
    private $domains;
    static protected $countEndnote = 0;

    /**
     * Create a basic document (without any content so far).
     * @param string $orientation page orientation. Possible values are (case insensitive):<ul><li>P or Portrait (default)</li><li>L or Landscape</li><li>'' (empty string) for automatic orientation</li></ul>
     * @param string $unit User measure unit. Possible values are:<ul><li>pt: point</li><li>mm: millimeter (default)</li><li>cm: centimeter</li><li>in: inch</li></ul><br />A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit.
     * @param mixed $format The format used for pages. It can be either: one of the string values specified at getPageSizeFromFormat() or an array of parameters specified at setPageFormat().
     * @param boolean $unicode TRUE means that the input text is unicode (default = true)
     * @param String $encoding charset encoding; default is UTF-8
     */
    public function __construct($orientation = 'P', $unit = 'mm', $format = 'A4', $unicode = true, $encoding = 'UTF-8')
    {
        $this->config = Config::GetInstance();
        if ($this->config->getValue('LOAD_EXTERNAL_MEDIA') == 'proxy') {
            $this->media_proxy = new MediaProxy();
        }
        parent::__construct($orientation, $unit, $format, $unicode, $encoding, false);
        $this->getDomains();
        $this->setDefaults();
    }

    /**
     * Adding a new page to the document. This page can contain even more content
     * than for just one page. The pagebreak will be managed by tcpdf. But this function
     * will create a new pagebreak. Needs to be called at least once to addContent.
     * @param string $orientation page orientation. Possible values are (case insensitive):<ul><li>P or Portrait (default)</li><li>L or Landscape</li><li>'' (empty string) for automatic orientation</li></ul>
     * @param mixed $format The format used for pages. It can be either: one of the string values specified at getPageSizeFromFormat() or an array of parameters specified at setPageFormat().
     * @param boolean $keepmargins if true overwrites the default page margins with the current margins
     * @param boolean $tocpage if true set the tocpage state to true (the added page will be used to display Table Of Content).
     */
    public function addPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false)
    {
        $this->page_added = true;
        parent::AddPage($orientation, $format, $keepmargins, $tocpage);
    }

    /**
     * Adding Stud.IP formatted code to the current page of the pdf.
     * Remember to call addPage first.
     * @param string $content Stud.IP formatted code
     */
    public function addContent($content)
    {
        $endnote = "";
        preg_match_all("#\[comment(=.*)?\](.*)\[/comment\]#msU", $content, $matches);
        if (count($matches[0])) {
            $endnote .= "<br><br>"._("Kommentare")."<hr>";
            for ($i=0; $i < count($matches[0]); $i++) {
                $endnote .= ($i+1).") ".htmlReady(mb_substr($matches[1][$i], 1)).": ".htmlReady($matches[2][$i])."<br>";
            }
        }
        $content = preg_replace_callback("#\[comment(=.*)?\](.*)\[/comment\]#msU", function ($m) {return $this->addEndnote($m[1], $m[2]);}, $content);
        $content = formatReady($content, true, true, true, null);
        $content = str_replace("<table", "<table border=\"1\"", $content);

        // Since TCPDF cannot handle missing images at all, the content needs
        // to be cleaned from those (see tickets #2957, #3329 and #3688)
        $content = preg_replace_callback('/<img[^>]+src="(.*?)"[^>]*>/', function ($match) {
            $url = $match[1];

            // Detect possible html entities in url and remove them
            if (mb_strpos($url, '&amp;') !== false) {
                $url = html_entity_decode($url);
            }

            // Handle optional media proxy
            if (Config::GetInstance()->LOAD_EXTERNAL_MEDIA) {
                $parsed = parse_url($url);
                // Detect media proxy
                if (mb_strpos($parsed['path'], 'media_proxy') !== false && mb_strpos($parsed['query'], 'url=') !== false) {
                    // Remove media proxy
                    parse_str($parsed['query'], $parameters);
                    $url = $parameters['url'];
                }
            }

            // Fetch headers from url, handle possible redirects
            do {
                $headers = get_headers($url, true, get_default_http_stream_context($url));
                if (!$headers) {
                    break;
                }
                list(, $status) = explode(' ', $headers[0]);

                $url = $headers['Location'] ?? $headers['location'] ?? $url;
            } while (in_array($status, [300, 301, 302, 303, 305, 307]));

            $status = $status ?? 404;

            // Replace image with link on error (and not internal), otherwise return sainitized
            // url
            return ((!is_internal_url($url) || $status == 404) && $status >= 400)
                 ? sprintf('[<a href="%s">%s</a>]', $url, basename($url))
                 : str_replace($match[1], $url, $match[0]);
        }, $content);

        $this->writeHTML($content.$endnote);
    }

    /**
     *
     * @param <type> $commented_by
     * @param <type> $text
     * @return <type>
     */
    public function addEndnote($commented_by, $text)
    {
        self::$countEndnote++;
        return ">>"._("Kommentar")." ".self::$countEndnote.">>";
    }

    /**
     * Dispatches the PDF to the user and cancels all other output of Stud.IP.
     * @param string $filename name of the future file without the extension.
     */
    public function dispatch($filename)
    {
        $this->Output($filename.".pdf", 'I');
    }

    /**
     * Saves the content as a file in the filesystem and returns a FileRef object.
     *
     * @param string $filename name of the future file without the extension.
     * @param mixed $folder_id md5-id of a given folder in database or null for nothing
     * @return FileRef of the exported file or false if creation of the FileRef or its associated File object failed.
     */
    public function save($filename, $folder_id = null)
    {
        global $user;

        //get folder:
        $folder = Folder::find($folder_id);
        if(!$folder) {
            return false;
        }
        $folder = $folder->getTypedFolder();

        //Create a File:
        $file = new File();
        $file->user_id = $user->id;
        $file->mime_type = 'application/pdf';
        $file->name = FileManager::cleanFileName($filename);
        $file->storage = 'disk';
        if(!$file->store()) {
            return false;
        }

        //...and a FileRef:
        $file_ref = new FileRef();
        $file_ref->file_id = $file->id;
        $file_ref->folder_id = $folder->getId();
        $file_ref->user_id = $user->id;
        $file_ref->name = $file->name;
        if(!$file_ref->store()) {
            return false;
        }

        //Now we can create the PDF file and store it in the file's path:
        $path = $file->getPath();
        $this->Output($path, 'F');
        $file->size = filesize($path);
        if($file->store()) {
            return $file_ref;
        }

        return false;
    }

    /**
     * Sets some default-values for the document, that tcpdf needs.
     */
    private function setDefaults ()
    {
        $this->defaults = true;

        // setting defaults
        $this->SetCreator('Stud.IP - ' . $this->config->getValue('UNI_NAME_CLEAN'));
        // set header and footer fonts
        $this->setHeaderFont([PDF_FONT_NAME_MAIN, '', 8]);
        $this->setFooterFont([PDF_FONT_NAME_DATA, '', 8]);
        // set default monospaced font
        $this->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
        //set margins
        $this->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
        $this->SetHeaderMargin(PDF_MARGIN_HEADER);
        $this->SetFooterMargin(PDF_MARGIN_FOOTER);
        //set auto page breaks
        $this->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
        //set image scale factor
        $this->setImageScale(PDF_IMAGE_SCALE_RATIO);
        // set default font subsetting mode
        $this->setFontSubsetting(true);
        // Set font
        //$this->SetFont('helvetica', '', 10, '', true);

        // set default page header
        $this->setHeaderData();

    }

    /**
     * Sets the title of the header of each page.
     * @param string $title title of the head
     */
    public function setHeaderTitle ($title)
    {
        $this->h_title = $title;
        $this->setHeaderData();
    }

    /**
     * Sets the subtitle of the header of each page.
     * @param string $subtitle subtitle of the head
     */
    public function setHeaderSubtitle ($subtitle)
    {
        $this->h_string = $subtitle;
        $this->setHeaderData();
    }

    /**
     * Creates a header for each page with a custom logo defined
     * @param string $ln header image logo
     * @param int $lw header image logo width in mm
     * @param string $ht string to print as title on document header
     * @param string $hs string to print on document header
     */
    public function setHeaderData($ln = '', $lw = 0, $ht = '', $hs = '', $tc = [], $lc = []) {
        if (!$ln) {
            $ln = Config::get()->PDF_LOGO ?: 'logos/logoklein.png';
        }
        $lw = 30;
        $ht = ($ht == '' ? $this->h_title : $ht);
        $hs = ($hs == '' ? $this->h_string : $hs);

        parent::resetHeaderTemplate();

        parent::setHeaderData($ln, $lw, $ht, $hs);
    }

    /**
     * Overrides writeHTML-method of tcpdf to convert image-urls, so that they
     * aren't accessed via proxy but directly.
     * @param string $html text to display
     * @param boolean $ln if true add a new line after text (default = true)
     * @param boolean $fill Indicates if the background must be painted (true) or transparent (false).
     * @param boolean $reseth if true reset the last cell height (default false).
     * @param boolean $cell if true add the current left (or right for RTL) padding to each Write (default false).
     * @param string $align Allows to center or align the text. Possible values are:<ul><li>L : left align</li><li>C : center</li><li>R : right align</li><li>'' : empty string : left for LTR or right for RTL</li></ul>
     */
    public function writeHTML ($html, $ln = true, $fill = false, $reseth = false, $cell = false, $align = '')
    {
        $html = preg_replace_callback('/src="([^@].*)"/U', function ($m) {return $this->convertURL($m[1]);}, $html);
        parent::writeHTML($html, $ln, $fill, $reseth, $cell, $align);
    }

    /**
     * Converts URLs in images so that the webserver can access them without proxy.
     * @param string $url of an image
     * @return string " src=\"".$converted_url."\""
     */
    protected function convertURL($url)
    {
        $convurl = $url;
        $url_elements = @parse_url($url);
        $url = $url_elements['path'];
        if (isset($url_elements['query'])) {
            $url .= "?{$url_elements['query']}";
        }
        if (mb_strpos(implode('#', $this->domains), $url_elements['host']) !== false) {
            if (mb_strpos($url, 'dispatch.php/media_proxy?url=') !== false) {
                $targeturl = urldecode(mb_substr($url, 4));
                try {
                    // is file in cache?
                    if (!$metadata = $this->media_proxy->getMetaData($targeturl)) {
                        $convurl = $targeturl;
                    } else {
                        $convurl = $this->config->getValue('MEDIA_CACHE_PATH') . '/' . md5($targeturl);
                    }
                } catch (Exception $e) {
                    $convurl = '';
                }
            } else if (mb_stripos($url, 'dispatch.php/document/download') !== false) {
                if (preg_match('#([a-f0-9]{32})#', $url, $matches)) {
                    $file_ref = FileRef::find($matches[1]);
                    $folder = $file_ref->folder->getTypedFolder();
                    if($folder->isFileDownloadable($file_ref->id, $GLOBALS['user']->id)) {
                        $convurl = $file_ref->file->getPath();
                    }
                }
            } else if (mb_stripos($url, 'download') !== false
                    || mb_stripos($url, 'sendfile.php') !== false) {
                //// get file id
                if (preg_match('#([a-f0-9]{32})#', $url, $matches)) {
                    $file_ref = FileRef::find($matches[1]);
                    $folder = $file_ref->folder->getTypedFolder();
                    if($folder->isFileDownloadable($file_ref->id, $GLOBALS['user']->id)) {
                        $convurl = $file_ref->file->getPath();
                    } else {
                        $convurl = Assets::image_path('messagebox/exception.png');
                    }
                }
            }
        }

        $src = 'src=""';
        $file_content = @file_get_contents($convurl, false, get_default_http_stream_context($convurl));
        if ($file_content) {
            $img_size = @getimagesizefromstring($file_content);
            if (is_array($img_size) && $img_size[0] > 0) {
                $src = 'src="@' . base64_encode($file_content) . '"';
            }
        }
        return $src;
    }

    /**
     * finds an array with all domains of this Stud.IP and stores it in $this->domains
     */
    protected function getDomains()
    {
        $this->domains = [];
        $host_url_parsed = @parse_url($GLOBALS['ABSOLUTE_URI_STUDIP']);
        if (isset($GLOBALS['STUDIP_DOMAINS'])) {
            $this->domains = $GLOBALS['STUDIP_DOMAINS'];
        }
        $this->domains[] = $host_url_parsed['host'];
    }

}