Download.php 15.8 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
229
230
        $arr = explode(':', $element, 2);
        if (count($arr) != 2) {
            throw new DownloadException('Missing parameter for "' . $element . '"', ERROR_MISSING_REQUIRED_PARAMETER);
        }
Carsten  Rose's avatar
Carsten Rose committed
231

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

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

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

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

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

        return $filename;
    }

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

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

278
        $zipFile = HelperFile::tempnam();
Carsten  Rose's avatar
Carsten Rose committed
279
280
281
282
283
284
        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
285
        if ($zip->open($zipFile, \ZipArchive::CREATE) !== true) {
Carsten  Rose's avatar
Carsten Rose committed
286
287
288
            throw new DownloadException("Error creating/opening new empty zip file: $zipFile", ERROR_IO_OPEN);
        }

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

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

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

        return $zipFile;
    }

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

        $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
336
        $downloadMode = $vars[DOWNLOAD_MODE];
337

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

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

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

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

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

            case DOWNLOAD_MODE_EXCEL:
373
374
375
376
377
378
379
380
381
                $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
382
383
                break;

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

            case DOWNLOAD_MODE_PDF:
392

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

395
396
397
398
399
400
401
402
403
404
405
406
                // 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
407
408
409
410
                break;

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

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

            case OUTPUT_MODE_FILE:
                break;

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

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

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

432
433
434
    /**
     * @param string $urlParam
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
435
     * @throws CodeException
436
     * @throws UserFormException
437
438
439
440
441
442
443
444
445
446
     * @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
447
    /**
448
449
450
451
452
     * Process download as requested in $vars. Output is either directly send to the browser, or a file which has to be deleted later.
     *
     * @param string|array $vars If $config is not an array, get values from STORE_SIP. If $config is an array, take it.
     * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE
     *
Carsten  Rose's avatar
Carsten Rose committed
453
454
     * @return string
     * @throws CodeException
455
     * @throws DbException
456
     * @throws DownloadException
Carsten  Rose's avatar
Carsten Rose committed
457
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
458
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
459
     */
460
    public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) {
Carsten  Rose's avatar
Carsten Rose committed
461

462
463
464
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
465

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

468
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
469
470
    }

471
472
473
474
475
476
477
478
479
480
481
482
483
    /**
     * @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;
    }
484
}
Carsten  Rose's avatar
Carsten Rose committed
485