Download.php 25.9 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
        foreach ($files as $filename) {
501
            $localName = substr($filename, strrpos($filename, '/') + 1);
502

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

508
            $zip->addFile($filename, $localName);
Carsten  Rose's avatar
Carsten Rose committed
509
510
511
512
513
514
        }
        $zip->close();

        return $zipFile;
    }

Carsten  Rose's avatar
Carsten Rose committed
515
    /**
516
517
     * $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
518
519
520
521
     * HTML to PDF | Excel
     *   <i>_id=<Typo3 pageId>
     *   <i>_<key>=<value i>
     * Direct
522
     *   <i>_file=<filename>
Carsten  Rose's avatar
Carsten Rose committed
523
     *
Carsten  Rose's avatar
Carsten Rose committed
524
     * @param array $vars [ DOWNLOAD_EXPORT_FILENAME, DOWNLOAD_MODE, SIP_DOWNLOAD_PARAMETER ]
Carsten  Rose's avatar
Carsten Rose committed
525
     *
Carsten  Rose's avatar
Carsten Rose committed
526
527
     * @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
528
529
530
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
531
532
533
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
534
535
536
537
538
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
539
     */
540
    private function doElements(array $vars, $outputMode) {
Carsten  Rose's avatar
Carsten Rose committed
541

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

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

Carsten  Rose's avatar
Carsten Rose committed
548
        $downloadMode = $vars[DOWNLOAD_MODE];
549

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

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

556
557
558
559
560
561
562
        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
563
        $elements = explode(PARAM_DELIMITER, $vars[SIP_DOWNLOAD_PARAMETER]);
Carsten  Rose's avatar
Carsten Rose committed
564

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

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

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

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

594
595
596
597
598
599
600
                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
601
602
                break;

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

            case DOWNLOAD_MODE_PDF:
611

612
                $filename = $this->concatFilesToPdf($srcFiles, $filesCleanLater);
613

614
615
                // try to find a meaningful filename
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
616
                    if (count($srcFiles) > 1) {
617
618
619
620
621
622
623
624
625
                        $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
626
627
628
                break;

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

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

639
640
        $filesCleanLater[] = $filename;

641
642
643
644
645
        switch ($outputMode) {

            case OUTPUT_MODE_FILE:
                break;

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

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

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

659
660
        HelperFile::cleanTempFiles($filesCleanLater);

661
        return $filename;
Carsten  Rose's avatar
Carsten Rose committed
662
663
    }

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

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

        return $pathFilenameThumbnail;
    }

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

700
701
702
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
703

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

706
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
707
708
    }

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

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