Download.php 15.7 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
214
     * @param string $downloadMode - DOWNLOAD_MODE_EXCEL | ....
     * @param string $tmpData - With $downloadMod=DOWNLOAD_MODE_EXCEL, this contains the rendered code from the give 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
Carsten  Rose's avatar
Carsten Rose committed
219
     */
220
    private function getElement($element, $downloadMode, &$tmpData) {
Carsten  Rose's avatar
Carsten Rose committed
221

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

Carsten  Rose's avatar
Carsten Rose committed
226
227
228
229
        $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
230

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

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

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

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

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

        return $filename;
    }

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

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

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

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

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

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

        return $zipFile;
    }

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

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

335
        if ($downloadMode == DOWNLOAD_MODE_MONITOR) {
336
            $monitor = new Monitor();
Carsten  Rose's avatar
Carsten Rose committed
337
338

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

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

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

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

            case DOWNLOAD_MODE_EXCEL:
370
371
372
373
374
375
376
377
378
                $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
379
380
                break;

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

            case DOWNLOAD_MODE_PDF:
389

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

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

            default:
                throw new DownloadException("Unknown downloadMode: $downloadMode", ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
408
409
410
                break;
        }

411
412
413
414
415
416
        switch ($outputMode) {

            case OUTPUT_MODE_FILE:
                break;

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

422
423
424
425
426
            default:
                throw new CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
        }

        return $filename;
Carsten  Rose's avatar
Carsten Rose committed
427
428
    }

429
430
431
    /**
     * @param string $urlParam
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
432
     * @throws CodeException
433
     * @throws UserFormException
434
435
436
437
438
439
440
441
442
443
     * @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
444
    /**
445
446
447
448
449
     * 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
450
451
     * @return string
     * @throws CodeException
452
     * @throws DbException
453
     * @throws DownloadException
Carsten  Rose's avatar
Carsten Rose committed
454
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
455
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
456
     */
457
    public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) {
Carsten  Rose's avatar
Carsten Rose committed
458

459
460
461
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
462

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

465
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
466
467
    }

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