Download.php 16.1 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
11
12
 */

namespace qfq;

13
//use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
Carsten  Rose's avatar
Carsten Rose committed
14
15
16
17

require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../store/Store.php');
18
require_once(__DIR__ . '/../store/Sip.php');
Carsten  Rose's avatar
Carsten Rose committed
19
require_once(__DIR__ . '/../helper/OnArray.php');
20
require_once(__DIR__ . '/../helper/OnString.php');
21
require_once(__DIR__ . '/../helper/Logger.php');
22
require_once(__DIR__ . '/../helper/Sanitize.php');
23
require_once(__DIR__ . '/../helper/HelperFile.php');
Carsten  Rose's avatar
Carsten Rose committed
24
require_once(__DIR__ . '/../report/Html2Pdf.php');
25
require_once(__DIR__ . '/Thumbnail.php');
Carsten  Rose's avatar
Carsten Rose committed
26
require_once(__DIR__ . '/Monitor.php');
Carsten  Rose's avatar
Carsten Rose committed
27
require_once(__DIR__ . '/../exceptions/DownloadException.php');
28
29
30
require_once(__DIR__ . '/Excel.php');
require_once(__DIR__ . '/../helper/DownloadPage.php');

Carsten  Rose's avatar
Carsten Rose committed
31
32
33
34
35
36
37
//require_once(__DIR__ . '/../Evaluate.php');
//require_once(__DIR__ . '/../helper/KeyValueStringParser.php');
//

/**
 * Class Download
 *
38
39
 * Documentation: PROTOCOL.md >> Download
 *
Carsten  Rose's avatar
Carsten Rose committed
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
 * 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;

69
70
71
72
73
    /**
     * @var string Filename where to write download Information
     */
    private $downloadDebugLog = '';

74
75
76
77
78
    /**
     * @var string DOWNLOAD_OUTPUT_FORMAT_RAW | DOWNLOAD_OUTPUT_FORMAT_JSON
     */
    private $outputFormat = DOWNLOAD_OUTPUT_FORMAT_RAW;

Carsten  Rose's avatar
Carsten Rose committed
79
80
    /**
     * @param bool|false $phpUnit
81
     * @throws CodeException
82
     * @throws DbException
83
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
84
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
85
86
87
88
89
90
     */
    public function __construct($phpUnit = false) {

        $this->session = Session::getInstance($phpUnit);
        $this->store = Store::getInstance('', $phpUnit);
        $this->db = new Database();
91
        $this->html2pdf = new Html2Pdf($this->store->getStore(STORE_SYSTEM), $phpUnit);
92

93
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
94
95
            $this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM);
        }
Carsten  Rose's avatar
Carsten Rose committed
96
97
98
99
100
101
    }

    /**
     * Concatenate all named files to one PDF file. Return name of new full PDF.
     *
     * @param array $files
Carsten  Rose's avatar
Carsten Rose committed
102
     *
Carsten  Rose's avatar
Carsten Rose committed
103
     * @return string  - fileName of concatenated file
104
105
     * @throws UserFormException
     * @throws downloadException
Carsten  Rose's avatar
Carsten Rose committed
106
107
108
     */
    private function concatPdfFiles(array $files) {

109
110
111
        // Remove empty entries. Might happen if there was no upload
        $files = OnArray::removeEmptyElementsFromArray($files);

112
113
        // Check that all files exist and readable
        foreach ($files AS $filename) {
114
            if (!is_readable($filename)) {
115
116
117
118
                throw new downloadException("Error reading file $filename. Not found or no permission", ERROR_DOWNLOAD_FILE_NOT_READABLE);
            }
        }

119
120
121
122
123
124
125
126
127
        switch (count($files)) {
            case 0:
                return '';
            case 1:
                return $files[0];
            default:
                break;
        }

128
        $concatFile = HelperFile::tempnam();
Carsten  Rose's avatar
Carsten Rose committed
129
130
131
132
133
134
135
136
        if (false === $concatFile) {
            throw new DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE);
        }

        // Check that all files are of type 'application/pdf'
        foreach ($files AS $filename) {
            $mimetype = mime_content_type($filename);
            if ($mimetype != 'application/pdf') {
137
                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
138
139
140
141
142
143
144
145
146
147
148
149
            }
        }

        $files = OnArray::arrayEscapeshellarg($files);

        $inputFiles = implode(' ', $files);
        if (trim($inputFiles) == '') {
            throw new DownloadException('No files to concatenate.', ERROR_DOWNLOAD_NO_FILES);
        }

        $cmd = "pdftk $inputFiles cat output $concatFile";

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

Carsten  Rose's avatar
Carsten Rose committed
154
155
156
157
158
159
160
161
162
163
        exec($cmd, $output, $rc);

        if ($rc != 0) {
            throw new DownloadException ("<p>Failed: RC=$rc   $cmd</p>" . implode("<br>", $output));
        }

        return $concatFile;
    }

    /**
164
165
     * Get the mimetype of $filename and store them in $rcMimetype.
     * Checks if the extension of $outputFilename fit's to the mimetype. If not, append the mimetype extension.
Carsten  Rose's avatar
Carsten Rose committed
166
     *
167
168
169
     * @param string $filename
     * @param string $outputFilename
     * @param string $rcMimetype
Carsten  Rose's avatar
Carsten Rose committed
170
     *
171
     * @return string possible updated $outputFilename, according the mimetype.
Carsten  Rose's avatar
Carsten Rose committed
172
     */
173
    private function targetFilenameExtension($filename, $outputFilename, &$rcMimetype) {
Carsten  Rose's avatar
Carsten Rose committed
174

175
        $rcMimetype = mime_content_type($filename);
176

177
178
179
180
181
182
        return $outputFilename;
    }

    /**
     * Set header type and output $filename. Be careful not to send any additional characters.
     *
183
     * @param $file
184
185
     * @param $outputFilename
     */
186
    private function outputFile($file, $outputFilename) {
187

188
        $length = filesize($file);
189

190
191
        $outputFilename = $this->targetFilenameExtension($file, $outputFilename, $mimetype);
        $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.
192

Carsten  Rose's avatar
Carsten Rose committed
193
194
        header("Content-type: $mimetype");
        header("Content-Length: $length");
195
196
        // 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");
197
        header("Content-Disposition: inline; filename=\"$outputFilename\"; name=\"$outputFilename\"");
Carsten  Rose's avatar
Carsten Rose committed
198
199
        header("Pragma: no-cache");
        header("Expires: 0");
Carsten  Rose's avatar
Carsten Rose committed
200

201
202
203
204
205
        if ($this->getOutputFormat() === DOWNLOAD_OUTPUT_FORMAT_JSON) {
            print json_encode([JSON_TEXT => file_get_contents($file)]);
        } else {
            print file_get_contents($file);
        }
Carsten  Rose's avatar
Carsten Rose committed
206
207
208
    }

    /**
209
     * Interprets $element and fetches corresponding content, either as a file or the content in a variable.
Carsten  Rose's avatar
Carsten Rose committed
210
211
     *
     * @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
212
     *
213
     * @param string $downloadMode - DOWNLOAD_MODE_EXCEL | ....
214
     * @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
215
     * @return string filename - already ready or fresh exported. Fresh exported needs to be deleted later.
216
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
217
     * @throws DownloadException
218
     * @throws UserFormException
219
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
220
     */
221
    private function getElement($element, $downloadMode, &$rcData) {
Carsten  Rose's avatar
Carsten Rose committed
222

223
224
225
226
        $filename = '';
        $rcArgs = array();
        $rcSipEncode = false;

Carsten  Rose's avatar
Carsten Rose committed
227
228
        $arr = explode(':', $element, 2);
        if (count($arr) != 2) {
229
230
            $possibleReason = ($element === '') ? 'If this is a download link, did you forget to include s:1?' : '';
            throw new DownloadException("Missing parameter for '$element'. $possibleReason", ERROR_MISSING_REQUIRED_PARAMETER);
Carsten  Rose's avatar
Carsten Rose committed
231
        }
Carsten  Rose's avatar
Carsten Rose committed
232

Carsten  Rose's avatar
Carsten Rose committed
233
234
        $token = $arr[0];
        $value = $arr[1];
Carsten  Rose's avatar
Carsten Rose committed
235

Carsten  Rose's avatar
Carsten Rose committed
236
237
238
        switch ($token) {
            case TOKEN_URL:
            case TOKEN_URL_PARAM:
239
            case TOKEN_PAGE:
240
241
                if ($downloadMode == DOWNLOAD_MODE_EXCEL) {

242
243
244
245
246
247
248
                    $urlParam = OnString::splitParam($value, $rcArgs, $rcSipEncode);
                    $urlParamString = KeyValueStringParser::unparse($urlParam, '=', '&');
                    if ($rcSipEncode) {
                        $sip = new Sip();
                        $urlParamString = $sip->queryStringToSip($urlParamString, RETURN_URL);
                    }

249
                    $baseUrl = $this->store->getVar(SYSTEM_BASE_URL, STORE_SYSTEM);
250
                    $rcData = DownloadPage::getContent($urlParamString, $baseUrl);
251
252
253
                } else {
                    $filename = $this->html2pdf->page2pdf($token, $value);
                }
Carsten  Rose's avatar
Carsten Rose committed
254
                break;
Carsten  Rose's avatar
Carsten Rose committed
255

Carsten  Rose's avatar
Carsten Rose committed
256
            case TOKEN_FILE:
257
            case TOKEN_FILE_DEPRECATED:
Carsten  Rose's avatar
Carsten Rose committed
258
259
260
261
262
                $filename = $value;
                break;
            default:
                throw new DownloadException('Unknown token: "' . $token . '"', ERROR_UNKNOWN_TOKEN);
                break;
Carsten  Rose's avatar
Carsten Rose committed
263
264
265
266
267
        }

        return $filename;
    }

Carsten  Rose's avatar
Carsten Rose committed
268
269
270
271
272

    /**
     * Creates a ZIP Files of all given $files
     *
     * @param array $files
Carsten  Rose's avatar
Carsten Rose committed
273
     *
Carsten  Rose's avatar
Carsten Rose committed
274
275
276
277
278
     * @return string ZIP filename - has to be deleted later.
     * @throws DownloadException
     */
    private function zipFiles(array $files) {

279
        $zipFile = HelperFile::tempnam();
Carsten  Rose's avatar
Carsten Rose committed
280
281
282
283
284
285
        if (false === $zipFile) {
            throw new DownloadException("Error creating output file.", ERROR_DOWNLOAD_CREATE_NEW_FILE);
        }

        $zip = new \ZipArchive();

Carsten  Rose's avatar
Carsten Rose committed
286
        if ($zip->open($zipFile, \ZipArchive::CREATE) !== true) {
Carsten  Rose's avatar
Carsten Rose committed
287
288
289
            throw new DownloadException("Error creating/opening new empty zip file: $zipFile", ERROR_IO_OPEN);
        }

290
        $len = strlen(TMP_FILE_PREFIX);
291
        $ii = 1;
Carsten  Rose's avatar
Carsten Rose committed
292
        foreach ($files AS $filename) {
293
            $localName = substr($filename, strrpos($filename, '/') + 1);
294

295
296
            if (substr($localName, 0, $len) == TMP_FILE_PREFIX) {
                $localName = 'file-' . $ii;
297
298
299
                $ii++;
            }

300
            $zip->addFile($filename, $localName);
Carsten  Rose's avatar
Carsten Rose committed
301
302
303
304
305
306
        }
        $zip->close();

        return $zipFile;
    }

Carsten  Rose's avatar
Carsten Rose committed
307
    /**
308
309
     * $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
310
311
312
313
     * HTML to PDF | Excel
     *   <i>_id=<Typo3 pageId>
     *   <i>_<key>=<value i>
     * Direct
314
     *   <i>_file=<filename>
Carsten  Rose's avatar
Carsten Rose committed
315
     *
Carsten  Rose's avatar
Carsten Rose committed
316
     * @param array $vars [ DOWNLOAD_EXPORT_FILENAME, DOWNLOAD_MODE, SIP_DOWNLOAD_PARAMETER ]
Carsten  Rose's avatar
Carsten Rose committed
317
     *
318
319
320
     * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE
     * @return string            Filename of the generated file. The filename only points to a real existing filename with  $outputMode=OUTPUT_MODE_FILE
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
321
     * @throws DownloadException
322
323
     * @throws UserFormException
     * @throws UserReportException
324
325
326
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
327
     */
328
    private function doElements(array $vars, $outputMode) {
Carsten  Rose's avatar
Carsten Rose committed
329
330
331
332
333
334
335
336

        $tmpFiles = array();

        $workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
        if (!chdir($workDir)) {
            throw new DownloadException ("Error chdir($workDir)", ERROR_IO_CHDIR);
        }

Carsten  Rose's avatar
Carsten Rose committed
337
        $downloadMode = $vars[DOWNLOAD_MODE];
338

339
        if ($downloadMode == DOWNLOAD_MODE_MONITOR) {
340
            $monitor = new Monitor();
Carsten  Rose's avatar
Carsten Rose committed
341
342

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

345
346
347
348
349
350
351
        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
352
        $elements = explode(PARAM_DELIMITER, $vars[SIP_DOWNLOAD_PARAMETER]);
Carsten  Rose's avatar
Carsten Rose committed
353

354
        // Get all files / content
355
        $tmpData = array();
Carsten  Rose's avatar
Carsten Rose committed
356
        foreach ($elements as $element) {
357
            $data = '';
358
            $tmpFiles[] = $this->getElement($element, $downloadMode, $data);
359
360
            if (!empty($data)) {
                $tmpData[] = $data;
361
            }
Carsten  Rose's avatar
Carsten Rose committed
362
363
        }

Carsten  Rose's avatar
Carsten Rose committed
364
365
366
367
        // Export, Concat File(s)
        switch ($downloadMode) {
            case DOWNLOAD_MODE_ZIP:
                $filename = $this->zipFiles($tmpFiles);
368
369
370
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
                }
Carsten  Rose's avatar
Carsten Rose committed
371
372
373
                break;

            case DOWNLOAD_MODE_EXCEL:
374
375
376
377
378
379
380
381
382
                $excel = new Excel();
                $filename = $excel->process($tmpFiles, $tmpData);
                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
383
384
                break;

Carsten  Rose's avatar
Carsten Rose committed
385
            case DOWNLOAD_MODE_FILE:
Carsten  Rose's avatar
Carsten Rose committed
386
                $filename = $tmpFiles[0];
387
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
388
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
389
                }
Carsten  Rose's avatar
Carsten Rose committed
390
                break;
Carsten  Rose's avatar
Carsten Rose committed
391
392

            case DOWNLOAD_MODE_PDF:
393

Carsten  Rose's avatar
Carsten Rose committed
394
                $filename = $this->concatPdfFiles($tmpFiles);
395

396
397
398
399
400
401
402
403
404
405
406
407
                // try to find a meaningful filename
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
                    if (count($tmpFiles) > 1) {
                        $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
408
409
410
411
                break;

            default:
                throw new DownloadException("Unknown downloadMode: $downloadMode", ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
412
413
414
                break;
        }

415
416
417
418
419
420
        switch ($outputMode) {

            case OUTPUT_MODE_FILE:
                break;

            case OUTPUT_MODE_DIRECT:
421
                $this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
422
                HelperFile::cleanTempFiles([$filename]);
423
                $filename = '';
424
                break;
Carsten  Rose's avatar
Carsten Rose committed
425

426
427
428
429
430
            default:
                throw new CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
        }

        return $filename;
Carsten  Rose's avatar
Carsten Rose committed
431
432
    }

433
434
435
    /**
     * @param string $urlParam
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
436
     * @throws CodeException
437
     * @throws UserFormException
438
439
440
441
442
443
444
445
446
447
     * @throws UserReportException
     */
    private function doThumbnail($urlParam) {

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

        return $pathFilenameThumbnail;
    }

Carsten  Rose's avatar
Carsten Rose committed
448
    /**
449
450
     * Process download as requested in $vars. Output is either directly send to the browser, or a file which has to be deleted later.
     *
451
     * @param string|array $vars - If $config is an array, take it, else get values from STORE_SIP
452
453
     * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE
     *
Carsten  Rose's avatar
Carsten Rose committed
454
455
     * @return string
     * @throws CodeException
456
     * @throws DownloadException
Carsten  Rose's avatar
Carsten Rose committed
457
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
458
     * @throws UserReportException
459
460
461
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
462
     */
463
    public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) {
Carsten  Rose's avatar
Carsten Rose committed
464

465
466
467
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
468

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

471
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
472
473
    }

474
475
476
477
478
479
480
481
482
483
484
485
486
    /**
     * @param $outputFormat
     */
    private function setOutputFormat($outputFormat) {
        $this->outputFormat = $outputFormat;
    }

    /**
     * @return string - DOWNLOAD_OUTPUT_FORMAT_RAW | DOWNLOAD_OUTPUT_FORMAT_JSON
     */
    public function getOutputFormat() {
        return $this->outputFormat;
    }
487
}
Carsten  Rose's avatar
Carsten Rose committed
488