Download.php 26.2 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
2
3
4
5
6
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 4/17/17
 * Time: 11:32 AM
Carsten  Rose's avatar
Carsten Rose committed
7
8
 *
 * Check: CODING.md > Download
Carsten  Rose's avatar
Carsten Rose committed
9
10
 */

Marc Egger's avatar
Marc Egger committed
11
12
namespace IMATHUZH\Qfq\Core\Report;

13
14
15
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\DownloadPage;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
Marc Egger's avatar
Marc Egger committed
16
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
17
use IMATHUZH\Qfq\Core\Helper\Logger;
Marc Egger's avatar
Marc Egger committed
18
19
20
21
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
22
23
24
25
use IMATHUZH\Qfq\Core\QuickFormQuery;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
Carsten  Rose's avatar
Carsten Rose committed
26
27
28
29

/**
 * Class Download
 *
30
31
 * Documentation: PROTOCOL.md >> Download
 *
Carsten  Rose's avatar
Carsten Rose committed
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
 * Param: i=1..n
 *   <i>_mode=direct | html2pdf
 *   <i>_id=<pageId>
 *   <i>_<key i>=<value i>
 *
 * @package qfq
 */
class Download {

    /**
     * @var Store
     */
    private $store = null;

    /**
     * @var Session
     */
    private $session = null;

    /**
     * @var Database
     */
    private $db = null;

    /**
     * @var Html2Pdf
     */
    private $html2pdf = null;

61
62
63
64
65
    /**
     * @var string Filename where to write download Information
     */
    private $downloadDebugLog = '';

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    /**
     * @var string Name of command
     */
    private $qpdf = '';

    /**
     * @var string Name of command
     */
    private $gs = '';

    /**
     * @var string Name of command
     */
    private $pdfunite = '';

81
82
83
84
85
    /**
     * @var string DOWNLOAD_OUTPUT_FORMAT_RAW | DOWNLOAD_OUTPUT_FORMAT_JSON
     */
    private $outputFormat = DOWNLOAD_OUTPUT_FORMAT_RAW;

Carsten  Rose's avatar
Carsten Rose committed
86
87
    /**
     * @param bool|false $phpUnit
Marc Egger's avatar
Marc Egger committed
88
89
90
91
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
92
93
94
     */
    public function __construct($phpUnit = false) {

95
        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
Carsten  Rose's avatar
Carsten Rose committed
96
97
98
        $this->session = Session::getInstance($phpUnit);
        $this->store = Store::getInstance('', $phpUnit);
        $this->db = new Database();
99
        $this->html2pdf = new Html2Pdf($this->store->getStore(STORE_SYSTEM), $phpUnit);
100

101
102
103
        $this->qpdf = $this->store->getVar(SYSTEM_CMD_QPDF, STORE_SYSTEM);
        $this->gs = $this->store->getVar(SYSTEM_CMD_GS, STORE_SYSTEM);
        $this->pdfunite = $this->store->getVar(SYSTEM_CMD_PDFUNITE, STORE_SYSTEM);
104
        $this->img2Pdf = $this->store->getVar(SYSTEM_CMD_IMG2PDF, STORE_SYSTEM);
105

106
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
107
108
            $this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM);
        }
Carsten  Rose's avatar
Carsten Rose committed
109
110
111
112
    }

    /**
     * Concatenate all named files to one PDF file. Return name of new full PDF.
113
     * If a source file is an image, it will be converted to a PDF.
Carsten  Rose's avatar
Carsten Rose committed
114
     *
115
116
     * @param array $files Array of source files
     * @param array $filesCleanLater Array of temporarily created files
Carsten  Rose's avatar
Carsten Rose committed
117
     *
Carsten  Rose's avatar
Carsten Rose committed
118
     * @return string  - fileName of concatenated file
Marc Egger's avatar
Marc Egger committed
119
120
121
     * @throws \CodeException
     * @throws \DownloadException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
122
     */
123
    private function concatFilesToPdf(array $files, array &$filesCleanLater) {
Carsten  Rose's avatar
Carsten Rose committed
124

125
126
127
        // Remove empty entries. Might happen if there was no upload
        $files = OnArray::removeEmptyElementsFromArray($files);

128
129
130
        // Check that all files are of type 'application/pdf'
        foreach ($files as $key => $filename) {
            // Check that all files exist and are readable
131
            if (!is_readable($filename)) {
Marc Egger's avatar
Marc Egger committed
132
                throw new \DownloadException("Error reading file $filename. Not found or no permission", ERROR_DOWNLOAD_FILE_NOT_READABLE);
133
134
            }

Carsten  Rose's avatar
Carsten Rose committed
135
            $mimetype = mime_content_type($filename);
136
137
138
139
140
141
142
143
144

            // If file is an image, convert to PDF
            if (substr($mimetype, 0, 6) == 'image/') {
                $tmpName = $this->doImgToPdf($filename);
                $filesCleanLater[] = $tmpName; // remember to purge later.
                $files[$key] = $tmpName; // replace original reference against temporary one.
                $mimetype = 'application/pdf';
            }

Carsten  Rose's avatar
Carsten Rose committed
145
            if ($mimetype != 'application/pdf') {
Marc Egger's avatar
Marc Egger committed
146
                throw new \DownloadException("Error concat file $filename. Mimetype 'application/pdf' expected, got: $mimetype", ERROR_DOWNLOAD_UNEXPECTED_MIME_TYPE);
Carsten  Rose's avatar
Carsten Rose committed
147
148
149
            }
        }

150
151
152
153
        if (count($files) === 0) {
            return '';
        }

Carsten  Rose's avatar
Carsten Rose committed
154
155
156
        if (count($files) == 1) {
            return $files[0];
        }
Carsten  Rose's avatar
Carsten Rose committed
157

Carsten  Rose's avatar
Carsten Rose committed
158
        $files = OnArray::arrayEscapeshellarg($files);
Carsten  Rose's avatar
Carsten Rose committed
159
160
        $inputFiles = implode(' ', $files);
        if (trim($inputFiles) == '') {
Marc Egger's avatar
Marc Egger committed
161
            throw new \DownloadException('No files to concatenate.', ERROR_DOWNLOAD_NO_FILES);
Carsten  Rose's avatar
Carsten Rose committed
162
163
        }

Carsten  Rose's avatar
Carsten Rose committed
164
165
166
        // Need to create a separate result file, even if it is just a single file (#6929)
        $concatFile = HelperFile::tempnam();
        if (false === $concatFile) {
Marc Egger's avatar
Marc Egger committed
167
            throw new \DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE);
Carsten  Rose's avatar
Carsten Rose committed
168
169
        }

170
171
//        $cmd = "pdftk $inputFiles cat output $concatFile 2>&1";  # Outdated. Hard to install on Ubuntu 18. Fails for recent PDFs.
//        $cmd = "qpdf --empty --pages $inputFiles -- $concatFile 2>&1"; # Fails to merge identical files, if they contain references.
172
        $cmd = $this->pdfunite . " $inputFiles $concatFile 2>&1"; // Based on poppler. URLs are preserved. Orientation and size are preserved.
Carsten  Rose's avatar
Carsten Rose committed
173

174
175
176
177
        if ($this->downloadDebugLog != '') {
            Logger::logMessage("Download: $cmd", $this->downloadDebugLog);
        }

178
        $rc = $this->concatPdfFilesPdfUnite($cmd, $output);
Carsten  Rose's avatar
Carsten Rose committed
179
180

        if ($rc != 0) {
Marc Egger's avatar
Marc Egger committed
181
            throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to merge PDF file",
182
183
                    ERROR_MESSAGE_TO_DEVELOPER => "CMD: " . $cmd . "<br>RC: $rc<br>Output: " . implode("<br>", $output)])
                , ERROR_DOWNLOAD_MERGE_FAILED);
Carsten  Rose's avatar
Carsten Rose committed
184
185
186
187
188
        }

        return $concatFile;
    }

189
190
191
192
193
194
195
196
197
198
199
200
201
202
    /**
     * Convert an image to PDF
     *
     * @param $fileImage
     * @throws \DownloadException
     */
    private function doImgToPdf($fileImage) {

        $filePdf = HelperFile::tempnam();
        if (false === $filePdf) {
            throw new \DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE);
        }

        // img2pdf --pagesize A4 -o out.pdf *.jpg
203
        $cmd = $this->img2Pdf . ' --pagesize A4 -o ' . $filePdf . ' ' . escapeshellarg($fileImage) . ' 2>&1';
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218

        if ($this->downloadDebugLog != '') {
            Logger::logMessage("Download: $cmd", $this->downloadDebugLog);
        }

        exec($cmd, $rcOutput, $rc);
        if ($rc != 0) {
            throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to convert image to PDF.",
                    ERROR_MESSAGE_TO_DEVELOPER => "CMD: $cmd<br>RC: $rc<br>Output: " . implode("<br>", $rcOutput)])
                , ERROR_DOWNLOAD_CONVERT_FAILED);
        }

        return $filePdf;
    }

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
    /**
     * Fires the merge command.
     * If for any reason the command fails: check if the reason is 'unencrypted files'.
     * If 'yes': try to decrypt them with qpdf.
     * After one decrypt, try merge again.
     * Try to merge and decrypt as long as there are encrypted files.
     *
     * @param $cmd
     * @param $rcOutput
     * @return mixed
     * @throws \DownloadException
     * @throws \UserFormException
     */
    private function concatPdfFilesPdfUnite($cmd, &$rcOutput) {
        $last = '';
        $rcOutput = '-';

236
        // Try to merge the PDFs as long as a problematic PDF has been repaired. Check this by comparing the last and the current output.
237
238
239
240
        while ($last != $rcOutput) {

            $last = $rcOutput; // Remember last

241
            // Merge:
242
243
244
245
246
247
248
249
            exec($cmd, $rcOutput, $rc);

            if ($rc == 0) {
                break; // skip rest if everything is fine
            }

            // Possible output: "Unimplemented Feature: Could not merge encrypted files ('ct.18.06.092-097.pdf')"
            $line = implode(',', $rcOutput);
250
            if (false !== ($line = strstr($line, "Unimplemented Feature: Could not merge encrypted files ("))) {
251
252
253
254
255
256
257
258
259
260
261

                $arr = explode("'", $line, 3);
                if (!empty($arr[1]) && file_exists($arr[1])) {
                    $file = $arr[1]; // problematic file

                    // Create a backup file: only one per day!
                    $backup = $file . date('.Y-m-d');
                    if (!file_exists($backup)) {
                        HelperFile::copy($file, $backup);
                    }

262
                    // Try 1: via 'qpdf --decrypt'
263
                    $cmdQpdf = $this->qpdf . " --decrypt '$backup' '$file' 2>&1"; // Try to decrypt file
264
265
266
                    exec($cmdQpdf, $outputQpdf, $rcQpdf);

                    if ($rcQpdf != 0) {
267
268

                        // Try 2: via 'gs -sDEVICE=pdfwrite'
269
                        $cmdGs = $this->gs . " -sDEVICE=pdfwrite -dNOPAUSE -sOutputFile=\"$file\" -- \"$backup\" 2>&1";
270
271
272
273
274
275
276
277
278
279
                        exec($cmdGs, $outputGs, $rcGs);

                        if ($rcGs != 0) {
                            // qpdf failed: restore origfile in case the $file has been destroyed.
                            HelperFile::copy($backup, $file);
                            throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to decrypt PDF",
                                    ERROR_MESSAGE_TO_DEVELOPER => "CMD1: " . $cmdQpdf . "<br>RC: $rcQpdf<br>Output: " . implode("<br>", $outputQpdf) . '<br>' .
                                        "CMD2: " . $cmdGs . "<br>RC: $rcGs<br>Output: " . implode("<br>", $outputGs)])
                                , ERROR_DOWNLOAD_MERGE_FAILED);
                        }
280
281
282
283
284
285
286
287
                    }
                }
            } else {
                throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Merge PDF file failed.",
                        ERROR_MESSAGE_TO_DEVELOPER => "CMD: " . $cmd . "<br>RC: $rc<br>Output: " . implode("<br>", $rcOutput)])
                    , ERROR_DOWNLOAD_MERGE_FAILED);
            }
        }
288

289
290
291
        return $rc;
    }

Carsten  Rose's avatar
Carsten Rose committed
292
    /**
293
     * Get the mimetype of $filename and store them in $rcMimetype.
Carsten  Rose's avatar
Carsten Rose committed
294
     *
295
     * @param string $pathFileName
296
297
     * @param string $outputFilename
     * @param string $rcMimetype
Carsten  Rose's avatar
Carsten Rose committed
298
     *
299
     * @return string possible updated $outputFilename, according the mimetype.
Carsten  Rose's avatar
Carsten Rose committed
300
     */
301
    private function targetFilenameExtension($pathFileName, $outputFilename, &$rcMimetype) {
Carsten  Rose's avatar
Carsten Rose committed
302

303
        if ($pathFileName != '' && file_exists($pathFileName)) {
304

305
306
            $rcMimetype = mime_content_type($pathFileName);
        }
307
308
309
310
311
312
        return $outputFilename;
    }

    /**
     * Set header type and output $filename. Be careful not to send any additional characters.
     *
313
     * @param $file
314
     * @param $outputFilename
Marc Egger's avatar
Marc Egger committed
315
     * @throws \DownloadException
316
     */
317
    private function outputFile($file, $outputFilename) {
318

319
320
        $json = '';
        $flagJson = ($this->getOutputFormat() === DOWNLOAD_OUTPUT_FORMAT_JSON);
321

322
        $outputFilename = $this->targetFilenameExtension($file, $outputFilename, $mimeType);
323
        $outputFilename = Sanitize::safeFilename($outputFilename); // be sure that there are no problematic chars in the filename. E.g. MacOS X don't like spaces for downloads.
324

325
326
        if ($flagJson) {
            if (false === ($json = json_encode([JSON_TEXT => file_get_contents($file)]))) {
Marc Egger's avatar
Marc Egger committed
327
                throw new \DownloadException(json_encode(
328
                    [ERROR_MESSAGE_TO_USER => 'Error converting to JSON',
Marc Egger's avatar
Marc Egger committed
329
                        ERROR_MESSAGE_TO_DEVELOPER => "json_last_error()=" . json_last_error() . ", File=" . $file]), ERROR_DOWNLOAD_JSON_CONVERT);
330
331
332
333
334
335
336
337
338
            }
            $length = strlen($json);

            $mimeType = 'application/json';
        } else {
            $length = filesize($file);
        }

        header("Content-type: $mimeType");
Carsten  Rose's avatar
Carsten Rose committed
339
        header("Content-Length: $length");
340
        if (!$flagJson) {
341
342
343
344
            // If defined as 'attachment': PDFs are not shown inside the browser (if user configured that). Instead, always a 'save as'-dialog appears (Chrome, FF)
            // header("Content-Disposition: attachment; filename=$outputFilename");
            header("Content-Disposition: inline; filename=\"$outputFilename\"; name=\"$outputFilename\"");
        }
Carsten  Rose's avatar
Carsten Rose committed
345
346
        header("Pragma: no-cache");
        header("Expires: 0");
Carsten  Rose's avatar
Carsten Rose committed
347

348
349
        if ($flagJson) {
            print $json;
350
        } else {
351
            readfile($file);
352
        }
Carsten  Rose's avatar
Carsten Rose committed
353
354
355
    }

    /**
356
     * Interprets $element and fetches corresponding content, either as a file or the content in a variable.
Carsten  Rose's avatar
Carsten Rose committed
357
     *
358
     * @param string $element - U:id=myExport&r=12, u:http://www.nzz.ch/issue?nr=21, F:fileadmin/sample.pdf
Carsten  Rose's avatar
Carsten Rose committed
359
     *
360
     * @param string $downloadMode - DOWNLOAD_MODE_EXCEL | ....
361
     * @param string $rcData - With $downloadMode=DOWNLOAD_MODE_EXCEL, this contains the rendered code from the given T3 page.
Carsten  Rose's avatar
Carsten Rose committed
362
     * @return string filename - already ready or fresh exported. Fresh exported needs to be deleted later.
Marc Egger's avatar
Marc Egger committed
363
364
365
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
366
367
368
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
369
370
371
372
373
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
374
     */
375
    private function getElement($element, $downloadMode, &$rcData) {
Carsten  Rose's avatar
Carsten Rose committed
376

377
378
379
380
        $filename = '';
        $rcArgs = array();
        $rcSipEncode = false;

Carsten  Rose's avatar
Carsten Rose committed
381
382
        $arr = explode(':', $element, 2);
        if (count($arr) != 2) {
383
            $possibleReason = ($element === '') ? 'If this is a download link, did you forget to include s:1?' : '';
Marc Egger's avatar
Marc Egger committed
384
            throw new \DownloadException("Missing parameter for '$element'. $possibleReason", ERROR_MISSING_REQUIRED_PARAMETER);
Carsten  Rose's avatar
Carsten Rose committed
385
        }
Carsten  Rose's avatar
Carsten Rose committed
386

Carsten  Rose's avatar
Carsten Rose committed
387
388
        $token = $arr[0];
        $value = $arr[1];
389
390
391
392
393
        if ($token === TOKEN_UID) { // extract uid
            $uidParamsArr = explode('&', $value, 2);
            $uid = $uidParamsArr[0];
            $value = $uidParamsArr[1] ?? ''; // additional params
        }
Carsten  Rose's avatar
Carsten Rose committed
394

Carsten  Rose's avatar
Carsten Rose committed
395
396
397
        switch ($token) {
            case TOKEN_URL:
            case TOKEN_URL_PARAM:
398
            case TOKEN_PAGE:
399
            case TOKEN_UID:
400
401
402
403
404
405
                $urlParam = OnString::splitParam($value, $rcArgs, $rcSipEncode);
                $urlParamString = KeyValueStringParser::unparse($urlParam, '=', '&');
                if ($rcSipEncode) {
                    $sip = new Sip();
                    $urlParamString = $sip->queryStringToSip($urlParamString, RETURN_URL);
                }
406

407
                if ($downloadMode == DOWNLOAD_MODE_EXCEL) {
408
                    if ($token === TOKEN_UID) {
409
                        $rcData = $this->getEvaluatedBodytext($uid, $urlParam);
410
411
412
413
                    } else {
                        $baseUrl = $this->store->getVar(SYSTEM_BASE_URL, STORE_SYSTEM);
                        $rcData = DownloadPage::getContent($urlParamString, $baseUrl);
                    }
414
                } else {
415
416
417
418
419
                    if ($token === TOKEN_UID) {
                        // create tmp html document with bodytext
                        $htmlText = $this->getEvaluatedBodytext($uid, $urlParam);
                        $tmpFilename = HelperFile::tempnam() . '.html';

420
                        $tmpFile = fopen($tmpFilename, "w") or die('Cannot create file:  ' . $tmpFilename);
421
422
423
424
425
                        fwrite($tmpFile, $htmlText);
                        fclose($tmpFile);

                        $rcArgsString = KeyValueStringParser::unparse($rcArgs, '=', '&');
                        $url = Support::mergeUrlComponents('', $tmpFilename, $rcArgsString);
426
427
428
                        $filename = $this->html2pdf->page2pdf($token, $url);
                        HelperFile::cleanTempFiles([$tmpFilename]);

429
                    } else {
430
                        $filename = $this->html2pdf->page2pdf($token, $value);
431
                    }
432
                }
Carsten  Rose's avatar
Carsten Rose committed
433
                break;
Carsten  Rose's avatar
Carsten Rose committed
434

Carsten  Rose's avatar
Carsten Rose committed
435
            case TOKEN_FILE:
436
            case TOKEN_FILE_DEPRECATED:
Carsten  Rose's avatar
Carsten Rose committed
437
438
439
                $filename = $value;
                break;
            default:
Marc Egger's avatar
Marc Egger committed
440
                throw new \DownloadException('Unknown token: "' . $token . '"', ERROR_UNKNOWN_TOKEN);
Carsten  Rose's avatar
Carsten Rose committed
441
                break;
Carsten  Rose's avatar
Carsten Rose committed
442
443
444
445
446
        }

        return $filename;
    }

447
448
449
450
451
    /**
     * @param $uid
     * @param array $urlParam
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
452
453
454
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
455
456
457
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
458
459
460
461
462
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
463
     */
464
    private function getEvaluatedBodyText($uid, array $urlParam) {
465
        foreach ($urlParam as $key => $paramValue) {
466
467
468
            $this->store->setVar($key, $paramValue, STORE_SIP);
        }

469
        $dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
470
        $sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
471
472
        $tt_content = $this->db->sql($sql, ROW_EXPECT_1, [$uid]);

473
        $qfq = new QuickFormQuery([T3DATA_BODYTEXT => $tt_content[T3DATA_BODYTEXT]], false, false);
474
475
476
        return $qfq->process();
    }

Carsten  Rose's avatar
Carsten Rose committed
477
478
479
480
    /**
     * Creates a ZIP Files of all given $files
     *
     * @param array $files
Carsten  Rose's avatar
Carsten Rose committed
481
     *
Carsten  Rose's avatar
Carsten Rose committed
482
     * @return string ZIP filename - has to be deleted later.
Marc Egger's avatar
Marc Egger committed
483
     * @throws \DownloadException
Carsten  Rose's avatar
Carsten Rose committed
484
     */
485
    private function zipFiles(array $files) {
Carsten  Rose's avatar
Carsten Rose committed
486

487
        $zipFile = HelperFile::tempnam();
Carsten  Rose's avatar
Carsten Rose committed
488
        if (false === $zipFile) {
Marc Egger's avatar
Marc Egger committed
489
            throw new \DownloadException("Error creating output file.", ERROR_DOWNLOAD_CREATE_NEW_FILE);
Carsten  Rose's avatar
Carsten Rose committed
490
491
492
493
        }

        $zip = new \ZipArchive();

Carsten  Rose's avatar
Carsten Rose committed
494
        if ($zip->open($zipFile, \ZipArchive::CREATE) !== true) {
Marc Egger's avatar
Marc Egger committed
495
            throw new \DownloadException("Error creating/opening new empty zip file: $zipFile", ERROR_IO_OPEN);
Carsten  Rose's avatar
Carsten Rose committed
496
497
        }

498
        $len = strlen(TMP_FILE_PREFIX);
499
        $ii = 1;
500
501
502
503
        foreach ($files as $item) {
            $arr = explode(PARAM_TOKEN_DELIMITER, $item);
            $filename = $arr[0] ?? '';
            $localName = ($arr[1] == '') ? substr($filename, strrpos($filename, '/') + 1) : $arr[1];
504

505
506
            if (substr($localName, 0, $len) == TMP_FILE_PREFIX) {
                $localName = 'file-' . $ii;
507
508
509
                $ii++;
            }

510
            $zip->addFile($filename, Sanitize::safeFilename($localName, false, true));
Carsten  Rose's avatar
Carsten Rose committed
511
512
513
514
515
516
        }
        $zip->close();

        return $zipFile;
    }

Carsten  Rose's avatar
Carsten Rose committed
517
    /**
518
519
     * $vars[DOWNLOAD_EXPORT_FILENAME] - Optional. '<new filename>'
     * $vars[DOWNLOAD_MODE] - Optional.  file | pdf | excel | thumbnail | monitor - default is a) 'file' in case of only one or b) 'pdf' in case of multiple sources.
Carsten  Rose's avatar
Carsten Rose committed
520
521
522
523
     * HTML to PDF | Excel
     *   <i>_id=<Typo3 pageId>
     *   <i>_<key>=<value i>
     * Direct
524
     *   <i>_file=<filename>
Carsten  Rose's avatar
Carsten Rose committed
525
     *
Carsten  Rose's avatar
Carsten Rose committed
526
     * @param array $vars [ DOWNLOAD_EXPORT_FILENAME, DOWNLOAD_MODE, SIP_DOWNLOAD_PARAMETER ]
Carsten  Rose's avatar
Carsten Rose committed
527
     *
Carsten  Rose's avatar
Carsten Rose committed
528
529
     * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE | OUTPUT_MODE_COPY_TO_FILE
     * @return string            Filename of the generated file. The filename only points to a real existing filename with  $outputMode=OUTPUT_MODE_FILE | OUTPUT_MODE_COPY_TO_FILE
Marc Egger's avatar
Marc Egger committed
530
531
532
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
533
534
535
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
536
537
538
539
540
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
541
     */
542
    private function doElements(array $vars, $outputMode) {
Carsten  Rose's avatar
Carsten Rose committed
543

544
545
        $srcFiles = array();
        $filesCleanLater = array();
Carsten  Rose's avatar
Carsten Rose committed
546
547

        $workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
548
        HelperFile::chdir($workDir);
Carsten  Rose's avatar
Carsten Rose committed
549

Carsten  Rose's avatar
Carsten Rose committed
550
        $downloadMode = $vars[DOWNLOAD_MODE];
551

552
        if ($downloadMode == DOWNLOAD_MODE_MONITOR) {
553
            $monitor = new Monitor();
Carsten  Rose's avatar
Carsten Rose committed
554
555

            return $monitor->dump($vars[TOKEN_L_FILE], $vars[TOKEN_L_TAIL], $vars[TOKEN_L_APPEND]);
556
557
        }

558
559
560
561
562
563
564
        if ($downloadMode == DOWNLOAD_MODE_THUMBNAIL) {
            // Fake $vars control array.
            $pathFilenameThumbnail = $this->doThumbnail($vars[SIP_DOWNLOAD_PARAMETER]);
            $downloadMode = DOWNLOAD_MODE_FILE;
            $vars[SIP_DOWNLOAD_PARAMETER] = TOKEN_FILE . ':' . $pathFilenameThumbnail;
        }

Carsten  Rose's avatar
Carsten Rose committed
565
        $elements = explode(PARAM_DELIMITER, $vars[SIP_DOWNLOAD_PARAMETER]);
Carsten  Rose's avatar
Carsten Rose committed
566

567
        // Get all files / content
568
        $tmpData = array();
Carsten  Rose's avatar
Carsten Rose committed
569
        foreach ($elements as $element) {
570
            $data = '';
571
            $srcFiles[] = $this->getElement($element, $downloadMode, $data);
572
573
            if (!empty($data)) {
                $tmpData[] = $data;
574
            }
Carsten  Rose's avatar
Carsten Rose committed
575
576
        }

Carsten  Rose's avatar
Carsten Rose committed
577
578
579
        // Export, Concat File(s)
        switch ($downloadMode) {
            case DOWNLOAD_MODE_ZIP:
580
                $filename = $this->zipFiles($srcFiles);
581
582
583
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
                }
Carsten  Rose's avatar
Carsten Rose committed
584
585
586
                break;

            case DOWNLOAD_MODE_EXCEL:
587
                $excel = new Excel();
588
                $filename = $excel->process($srcFiles, $tmpData);
589
590
591

                if (empty($filename) || !file_exists($filename)) {
                    throw new \DownloadException(json_encode(
Carsten  Rose's avatar
Carsten Rose committed
592
593
                        [ERROR_MESSAGE_TO_USER => 'New created Excel file is broken.',
                            ERROR_MESSAGE_TO_DEVELOPER => "File: '$filename'"]), ERROR_IO_READ_FILE);
594
595
                }

596
597
598
599
600
601
602
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
                    if (HelperFile::isQfqTemp($filename)) {
                        $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".xlsx";
                    } else {
                        $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
                    }
                }
Carsten  Rose's avatar
Carsten Rose committed
603
604
                break;

Carsten  Rose's avatar
Carsten Rose committed
605
            case DOWNLOAD_MODE_FILE:
606
                $filename = $srcFiles[0];
607
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
608
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
609
                }
Carsten  Rose's avatar
Carsten Rose committed
610
                break;
Carsten  Rose's avatar
Carsten Rose committed
611
612

            case DOWNLOAD_MODE_PDF:
613

614
                $filename = $this->concatFilesToPdf($srcFiles, $filesCleanLater);
615

616
617
                // try to find a meaningful filename
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
618
                    if (count($srcFiles) > 1) {
619
620
621
622
623
624
625
626
627
                        $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf";
                    } else {
                        if (HelperFile::isQfqTemp($filename)) {
                            $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf";
                        } else {
                            $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
                        }
                    }
                }
Carsten  Rose's avatar
Carsten Rose committed
628
629
630
                break;

            default:
Marc Egger's avatar
Marc Egger committed
631
                throw new \DownloadException("Unknown downloadMode: $downloadMode", ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
632
633
634
                break;
        }

635
        if ($filename != '' && !file_exists($filename)) {
Marc Egger's avatar
Marc Egger committed
636
            throw new \DownloadException(json_encode(
637
                [ERROR_MESSAGE_TO_USER => 'Can\'t read file',
Marc Egger's avatar
Marc Egger committed
638
                    ERROR_MESSAGE_TO_DEVELOPER => "File: $filename"]), ERROR_IO_FILE_EXIST);
639
640
        }

641
642
643
        switch ($outputMode) {

            case OUTPUT_MODE_FILE:
644
                // At least for sendmail with dynamic PDF created from URL: unlink temporary files later, not now.
645
646
                break;

Carsten  Rose's avatar
Carsten Rose committed
647
648
            case OUTPUT_MODE_COPY_TO_FILE:
                HelperFile::copy($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
649
                $filesCleanLater[] = $filename;
Carsten  Rose's avatar
Carsten Rose committed
650
651
                break;

652
            case OUTPUT_MODE_DIRECT:
653
                $this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
654
                $filesCleanLater[] = $filename;
655
                $filename = '';
656
                break;
Carsten  Rose's avatar
Carsten Rose committed
657

658
            default:
Marc Egger's avatar
Marc Egger committed
659
                throw new \CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
660
661
        }

662
663
        HelperFile::cleanTempFiles($filesCleanLater);

664
        return $filename;
Carsten  Rose's avatar
Carsten Rose committed
665
666
    }

667
668
669
    /**
     * @param string $urlParam
     * @return string
Marc Egger's avatar
Marc Egger committed
670
671
672
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
673
     */
674
    private function doThumbnail($urlParam) {
675
676
677
678
679
680
681

        $thumbnail = new Thumbnail();
        $pathFilenameThumbnail = $thumbnail->process($urlParam, THUMBNAIL_VIA_DOWNLOAD);

        return $pathFilenameThumbnail;
    }

Carsten  Rose's avatar
Carsten Rose committed
682
    /**
683
684
     * Process download as requested in $vars. Output is either directly send to the browser, or a file which has to be deleted later.
     *
685
     * @param string|array $vars - If $config is an array, take it, else get values from STORE_SIP
686
687
     * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE
     *
Carsten  Rose's avatar
Carsten Rose committed
688
     * @return string
Marc Egger's avatar
Marc Egger committed
689
690
691
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
692
693
694
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
695
696
697
698
699
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
700
     */
701
    public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) {
Carsten  Rose's avatar
Carsten Rose committed
702

703
704
705
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
706

707
708
        $this->setOutputFormat(empty($vars[DOWNLOAD_OUTPUT_FORMAT]) ? DOWNLOAD_OUTPUT_FORMAT_RAW : $vars[DOWNLOAD_OUTPUT_FORMAT]);

709
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
710
711
    }

712
713
714
    /**
     * @param $outputFormat
     */
715
    private function setOutputFormat($outputFormat) {
716
717
718
719
720
721
        $this->outputFormat = $outputFormat;
    }

    /**
     * @return string - DOWNLOAD_OUTPUT_FORMAT_RAW | DOWNLOAD_OUTPUT_FORMAT_JSON
     */
722
    public function getOutputFormat() {
723
724
        return $this->outputFormat;
    }
725
}
Carsten  Rose's avatar
Carsten Rose committed
726