Passed
Push — hypernext ( affafd...433812 )
by Nico
13:20
created

MakePdf::appendPdfs()   A

Complexity

Conditions 5
Paths 15

Size

Total Lines 38
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 15
c 0
b 0
f 0
nc 15
nop 1
dl 0
loc 38
rs 9.4555
ccs 0
cts 14
cp 0
crap 30
1
<?php declare(strict_types=1);
2
/**
3
 * @author Nicolas CARPi <[email protected]>
4
 * @copyright 2012 Nicolas CARPi
5
 * @see https://www.elabftw.net Official website
6
 * @license AGPL-3.0
7
 * @package elabftw
8
 */
9
10
namespace Elabftw\Services;
11
12
use DateTime;
13
use function dirname;
14
use Elabftw\Elabftw\ContentParams;
15
use Elabftw\Elabftw\Tools;
16
use Elabftw\Exceptions\FilesystemErrorException;
17
use Elabftw\Interfaces\FileMakerInterface;
18
use Elabftw\Interfaces\MpdfProviderInterface;
19
use Elabftw\Models\AbstractEntity;
20
use Elabftw\Models\Config;
21
use Elabftw\Models\Experiments;
22
use Elabftw\Models\Users;
23
use Elabftw\Traits\PdfTrait;
24
use Elabftw\Traits\TwigTrait;
25
use function is_dir;
26
use function mkdir;
27
use Mpdf\Mpdf;
28
use function preg_replace;
29
use setasign\Fpdi\FpdiException;
30
use function str_replace;
31
use function strtolower;
32
use Symfony\Component\HttpFoundation\Request;
33
34
/**
35 1
 * Create a pdf from an Entity
36
 */
37 1
class MakePdf extends AbstractMake implements FileMakerInterface
38
{
39 1
    use TwigTrait;
40
41 1
    use PdfTrait;
42
43
    public string $longName;
44 1
45 1
    /**
46 1
     * Constructor
47
     *
48
     * @param AbstractEntity $entity Experiments or Database
49
     * @param bool $temporary do we need to save it in cache folder or uploads folder
50
     */
51
    public function __construct(MpdfProviderInterface $mpdfProvider, AbstractEntity $entity, $temporary = false)
52
    {
53
        parent::__construct($entity);
54 1
55 1
        $this->longName = $this->getLongName() . '.pdf';
56
57
        $this->mpdf = $mpdfProvider->getInstance();
58
        $this->mpdf->SetTitle($this->Entity->entityData['title']);
59
        $this->mpdf->SetKeywords(str_replace('|', ' ', $this->Entity->entityData['tags'] ?? ''));
60
61
        if ($temporary) {
62
            $this->filePath = $this->getTmpPath() . $this->getUniqueString();
63
        } else {
64
            $this->filePath = $this->getUploadsPath() . $this->longName;
65
            $dir = dirname($this->filePath);
66
            if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
67
                throw new FilesystemErrorException('Cannot create folder! Check permissions of uploads folder.');
68
            }
69
        }
70
71
        // suppress the "A non-numeric value encountered" error from mpdf
72
        // see https://github.com/baselbers/mpdf/commit
73
        // 5cbaff4303604247f698afc6b13a51987a58f5bc#commitcomment-23217652
74
        error_reporting(E_ERROR);
75
    }
76
77
    /**
78
     * Generate pdf and output it to a file
79
     */
80
    public function outputToFile(): void
81
    {
82
        $this->generate()->Output($this->filePath, 'F');
83
    }
84
85
    /**
86
     * Generate pdf and return it as string
87
     */
88
    public function getFileContent(): string
89
    {
90
        return $this->generate()->Output('', 'S');
91
    }
92
93
    /**
94
     * Replace weird characters by underscores
95
     */
96
    public function getFileName(): string
97
    {
98
        $title = Filter::forFilesystem($this->Entity->entityData['title']);
99
        return $this->Entity->entityData['date'] . ' - ' . $title . '.pdf';
100
    }
101
102
    /**
103
     * Get the final html content with tex expressions converted in svg by tex2svg
104
     */
105
    public function getContent(): string
106
    {
107
        $Tex2Svg = new Tex2Svg($this->mpdf, $this->getHtml());
108
        return $Tex2Svg->getContent();
109
    }
110
111
    /**
112
     * Append PDFs attached to an entity
113
     */
114
    private function appendPdfs(array $pdfs): void
115
    {
116
        foreach ($pdfs as $pdf) {
117
            // There will be cases where the merging will fail
118
            // due to incompatibilities of Mpdf (actually fpdi) with the pdfs
119
            // See https://manuals.setasign.com/fpdi-manual/v2/limitations/
120
            // These cases will be caught and ignored
121
            try {
122
                $numberOfPages = $this->mpdf->setSourceFile($pdf[0]);
123
124
                for ($i = 1; $i <= $numberOfPages; $i++) {
125
                    // Import the ith page of the source PDF file
126
                    $page = $this->mpdf->importPage($i);
127
128
                    // getTemplateSize() is not documented in the MPDF manual
129
                    // @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
130
                    $pageDim = $this->mpdf->getTemplateSize($page);
131
132
                    if (is_array($pageDim)) { // satisfy phpstan
133
                        // add a new (blank) page with the dimensions of the imported page
134
                        $this->mpdf->AddPageByArray(array(
135
                            'orientation' => $pageDim['orientation'],
136
                            'sheet-size' => array($pageDim['width'], $pageDim['height']),
137
                        ));
138
                    }
139
140
                    // empty the header and footer
141
                    // cannot be an empty string
142
                    $this->mpdf->SetHTMLHeader(' ', '', true);
143
                    $this->mpdf->SetHTMLFooter(' ', '');
144
145
                    // add the content of the imported page
146
                    $this->mpdf->useTemplate($page);
147
                }
148
                // not all pdf will be able to be integrated, so for the one that will trigger an exception
149
            // we simply ignore it
150
            } catch (FpdiException) {
151
                continue;
152
            }
153
        }
154
    }
155
156
    /**
157
     * Build HTML content that will be fed to mpdf->WriteHTML()
158
     */
159
    private function getHtml(): string
160
    {
161
        $Request = Request::createFromGlobals();
162
163
        if ($this->Entity->entityData['tags']) {
164
            $tags = '<strong>Tags:</strong> <em>' .
165
                str_replace('|', ' ', $this->Entity->entityData['tags']) . '</em> <br />';
166
        }
167
168
        $date = new DateTime($this->Entity->entityData['date'] ?? Filter::kdate());
169
170
        $locked = $this->Entity->entityData['locked'];
171
        $lockDate = '';
172
        $lockerName = '';
173
174
        if ($locked) {
175
            // get info about the locker
176
            $Locker = new Users((int) $this->Entity->entityData['lockedby']);
177
            $lockerName = $Locker->userData['fullname'];
178
179
            // separate the date and time
180
            $ldate = explode(' ', $this->Entity->entityData['lockedwhen']);
181
            $lockDate = $ldate[0] . ' at ' . $ldate[1];
182
        }
183
184
        $renderArr = array(
185
            'body' => $this->getBody(),
186
            'commentsArr' => $this->Entity->Comments->read(new ContentParams()),
187
            'css' => $this->getCss(),
188
            'date' => $date->format('Y-m-d'),
189
            'elabid' => $this->Entity->entityData['elabid'],
190
            'fullname' => $this->Entity->entityData['fullname'],
191
            'includeFiles' => $this->Entity->Users->userData['inc_files_pdf'],
192
            'linksArr' => $this->Entity->Links->read(new ContentParams()),
193
            'locked' => $locked,
194
            'lockDate' => $lockDate,
195
            'lockerName' => $lockerName,
196
            'metadata' => $this->Entity->entityData['metadata'],
197
            'pdfSig' => $Request->cookies->get('pdf_sig'),
198
            'stepsArr' => $this->Entity->Steps->read(new ContentParams()),
199
            'tags' => $this->Entity->entityData['tags'],
200
            'title' => $this->Entity->entityData['title'],
201
            'uploadsArr' => $this->Entity->Uploads->readAll(),
202
            'uploadsFolder' => dirname(__DIR__, 2) . '/uploads/',
203
            'url' => $this->getUrl(),
204
            'linkBaseUrl' => Tools::getUrl($Request) . '/database.php',
205
            'useCjk' => $this->Entity->Users->userData['cjk_fonts'],
206
        );
207
208
        $html = $this->getTwig(Config::getConfig())->render('pdf.html', $renderArr);
209
210
        // now remove any img src pointing to outside world
211
        // prevent blind ssrf (thwarted by CSP on webpage, but not in pdf)
212
        return preg_replace('/img src=("|\')(ht|f|)tp/i', 'nope', $html);
213
    }
214
215
    /**
216
     * Get a list of all PDFs that are attached to an entity
217
     *
218
     * @return array Empty or array of arrays with information for PDFs array('path/to/file', 'real.name')
219
     */
220
    private function getAttachedPdfs(): array
221
    {
222
        $uploadsArr = $this->Entity->Uploads->readAll();
223
        $listOfPdfs = array();
224
225
        if (empty($uploadsArr)) {
226
            return $listOfPdfs;
227
        }
228
229
        foreach ($uploadsArr as $upload) {
230
            $filePath = dirname(__DIR__, 2) . '/uploads/' . $upload['long_name'];
231
            if (file_exists($filePath) && strtolower(Tools::getExt($upload['real_name'])) === 'pdf') {
232
                $listOfPdfs[] = array($filePath, $upload['real_name']);
233
            }
234
        }
235
236
        return $listOfPdfs;
237
    }
238
239
    /**
240
     * Build the pdf
241
     */
242
    private function generate(): Mpdf
243
    {
244
        // write content
245
        $this->mpdf->WriteHTML($this->getContent());
246
247
        if ($this->Entity->Users->userData['append_pdfs']) {
248
            $this->appendPdfs($this->getAttachedPdfs());
249
        }
250
251
        return $this->mpdf;
252
    }
253
254
    private function getBody(): string
255
    {
256
        $body = Tools::md2html($this->Entity->entityData['body']);
257
        // we need to fix the file path in the body so it shows properly into the pdf for timestamping (issue #131)
258
        return str_replace('src="app/download.php?f=', 'src="' . dirname(__DIR__, 2) . '/uploads/', $body);
259
    }
260
}
261