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

namespace qfq;

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

require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../store/Store.php');
require_once(__DIR__ . '/../helper/OnArray.php');
19
require_once(__DIR__ . '/../helper/Logger.php');
20
require_once(__DIR__ . '/../helper/Sanitize.php');
21
require_once(__DIR__ . '/../helper/HelperFile.php');
Carsten  Rose's avatar
Carsten Rose committed
22
23
24
25
26
27
28
29
30
31
32
require_once(__DIR__ . '/../report/Html2Pdf.php');
//require_once(__DIR__ . '/Link.php');
//require_once(__DIR__ . '/Sendmail.php');
require_once(__DIR__ . '/../exceptions/DownloadException.php');
//require_once(__DIR__ . '/../Evaluate.php');
//require_once(__DIR__ . '/../helper/KeyValueStringParser.php');
//

/**
 * Class Download
 *
33
34
 * Documentation: PROTOCOL.md >> Download
 *
Carsten  Rose's avatar
Carsten Rose committed
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
61
62
63
 * 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;

64
65
66
67
68
    /**
     * @var string Filename where to write download Information
     */
    private $downloadDebugLog = '';

Carsten  Rose's avatar
Carsten Rose committed
69
70
71
72
73
74
75
76
    /**
     * @param bool|false $phpUnit
     */
    public function __construct($phpUnit = false) {

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

79
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
80
81
            $this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM);
        }
Carsten  Rose's avatar
Carsten Rose committed
82
83
84
85
86
87
    }

    /**
     * Concatenate all named files to one PDF file. Return name of new full PDF.
     *
     * @param array $files
Carsten  Rose's avatar
Carsten Rose committed
88
     *
Carsten  Rose's avatar
Carsten Rose committed
89
90
91
92
93
     * @return string  - fileName of concatenated file
     * @throws DownloadException
     */
    private function concatPdfFiles(array $files) {

94
95
96
97
98
99
100
101
102
        switch (count($files)) {
            case 0:
                return '';
            case 1:
                return $files[0];
            default:
                break;
        }

103
104

        $concatFile = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
Carsten  Rose's avatar
Carsten Rose committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
        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') {
                throw new downloadException("Error concat file $filename. Mimetype 'application/pdf' expected, got: $mimetype", ERROR_DOWNLOAD_UNEXPECTED_MIMETYPE);
            }
        }

        $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";

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

Carsten  Rose's avatar
Carsten Rose committed
130
131
132
133
134
135
136
137
138
139
        exec($cmd, $output, $rc);

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

        return $concatFile;
    }

    /**
140
141
     * 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
142
     *
143
144
145
     * @param string $filename
     * @param string $outputFilename
     * @param string $rcMimetype
Carsten  Rose's avatar
Carsten Rose committed
146
     *
147
     * @return string possible updated $outputFilename, according the mimetype.
Carsten  Rose's avatar
Carsten Rose committed
148
     */
149
    private function targetFilenameExtension($filename, $outputFilename, &$rcMimetype) {
Carsten  Rose's avatar
Carsten Rose committed
150

151
        $rcMimetype = mime_content_type($filename);
152

Carsten  Rose's avatar
Carsten Rose committed
153
154
155
        // See #4303 / Bug: Download von doc/docx-Dateien - the following approach makes more trouble than it helps. Removed completely.

        // Main motivation: protect against hardcoded filename extension in FE.parameter.fileDestination
156
        // In case there is a wrong filenameextension on the outputFilename: extend it.
157
158
159
160
161
//        $ext = '.' . substr($rcMimetype, strrpos($rcMimetype, '/') + 1); // very very dirty way of getting an extension - only valid for a limited set of mimetypes
//        $len = strlen($ext);
//        if (substr($outputFilename, 0 - $len) != $ext) {
//            $outputFilename .= $ext;
//        }
Carsten  Rose's avatar
Carsten Rose committed
162

163
164
165
166
167
168
        return $outputFilename;
    }

    /**
     * Set header type and output $filename. Be careful not to send any additional characters.
     *
169
     * @param $file
170
171
     * @param $outputFilename
     */
172
    private function outputFile($file, $outputFilename) {
173

174
175
176
        $length = filesize($file);
        $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.
177

Carsten  Rose's avatar
Carsten Rose committed
178
179
        header("Content-type: $mimetype");
        header("Content-Length: $length");
180
181
        // 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");
182
        header("Content-Disposition: inline; filename=\"$outputFilename\"; name=\"$outputFilename\"");
Carsten  Rose's avatar
Carsten Rose committed
183
184
        header("Pragma: no-cache");
        header("Expires: 0");
Carsten  Rose's avatar
Carsten Rose committed
185

186
        print file_get_contents($file);
Carsten  Rose's avatar
Carsten Rose committed
187
188
189
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
190
191
192
     * Interprets $element and fetches corresponding content as file.
     *
     * @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
193
     *
Carsten  Rose's avatar
Carsten Rose committed
194
     * @return string filename - already ready or fresh exported. Fresh exported needs to be deleted later.
Carsten  Rose's avatar
Carsten Rose committed
195
196
197
     * @throws DownloadException
     * @throws \exception
     */
Carsten  Rose's avatar
Carsten Rose committed
198
    private function getElement($element) {
Carsten  Rose's avatar
Carsten Rose committed
199

Carsten  Rose's avatar
Carsten Rose committed
200
201
202
203
        $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
204

Carsten  Rose's avatar
Carsten Rose committed
205
206
        $token = $arr[0];
        $value = $arr[1];
Carsten  Rose's avatar
Carsten Rose committed
207

Carsten  Rose's avatar
Carsten Rose committed
208
209
210
        switch ($token) {
            case TOKEN_URL:
            case TOKEN_URL_PARAM:
211
            case TOKEN_PAGE:
Carsten  Rose's avatar
Carsten Rose committed
212
213
                $filename = $this->html2pdf->page2pdf($token, $value);
                break;
Carsten  Rose's avatar
Carsten Rose committed
214

Carsten  Rose's avatar
Carsten Rose committed
215
            case TOKEN_FILE:
216
            case TOKEN_FILE_DEPRECATED:
Carsten  Rose's avatar
Carsten Rose committed
217
218
219
220
221
                $filename = $value;
                break;
            default:
                throw new DownloadException('Unknown token: "' . $token . '"', ERROR_UNKNOWN_TOKEN);
                break;
Carsten  Rose's avatar
Carsten Rose committed
222
223
224
225
226
        }

        return $filename;
    }

Carsten  Rose's avatar
Carsten Rose committed
227
228
229
230
231

    /**
     * Creates a ZIP Files of all given $files
     *
     * @param array $files
Carsten  Rose's avatar
Carsten Rose committed
232
     *
Carsten  Rose's avatar
Carsten Rose committed
233
234
235
236
237
     * @return string ZIP filename - has to be deleted later.
     * @throws DownloadException
     */
    private function zipFiles(array $files) {

238
        $zipFile = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
Carsten  Rose's avatar
Carsten Rose committed
239
240
241
242
243
244
        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
245
        if ($zip->open($zipFile, \ZipArchive::CREATE) !== true) {
Carsten  Rose's avatar
Carsten Rose committed
246
247
248
            throw new DownloadException("Error creating/opening new empty zip file: $zipFile", ERROR_IO_OPEN);
        }

249
        $len = strlen(TMP_FILE_PREFIX);
250
        $ii = 1;
Carsten  Rose's avatar
Carsten Rose committed
251
        foreach ($files AS $filename) {
252
253
            $localname = substr($filename, strrpos($filename, '/') + 1);

254
            if (substr($localname, 0, $len) == TMP_FILE_PREFIX) {
255
256
257
                $localname = 'file-' . $ii;
                $ii++;
            }
258
//            $localname = $this->targetFilenameExtension($filename, $localname, $mimetype);
259

Carsten  Rose's avatar
Carsten Rose committed
260
261
262
263
264
265
266
            $zip->addFile($filename, $localname);
        }
        $zip->close();

        return $zipFile;
    }

Carsten  Rose's avatar
Carsten Rose committed
267
268
269
270
271
272
273
    /**
     * exportFilename=<new filename>
     * mode=file | pdf | excel - default is 'file' in case of only one or 'pdf' in case of multiple sources.
     * HTML to PDF | Excel
     *   <i>_id=<Typo3 pageId>
     *   <i>_<key>=<value i>
     * Direct
274
     *   <i>_file=<filename>
Carsten  Rose's avatar
Carsten Rose committed
275
     *
Carsten  Rose's avatar
Carsten Rose committed
276
     * @param array $vars [ DOWNLOAD_EXPORT_FILENAME, DOWNLOAD_MODE, SIP_DOWNLOAD_PARAMETER ]
Carsten  Rose's avatar
Carsten Rose committed
277
     *
278
279
280
     * @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
281
282
     * @throws DownloadException
     */
283
    private function doElements(array $vars, $outputMode) {
Carsten  Rose's avatar
Carsten Rose committed
284
285
286
287
288
289
290
291

        $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
292
293
        $downloadMode = $vars[DOWNLOAD_MODE];
        $elements = explode(PARAM_DELIMITER, $vars[SIP_DOWNLOAD_PARAMETER]);
Carsten  Rose's avatar
Carsten Rose committed
294

Carsten  Rose's avatar
Carsten Rose committed
295
        // Get all files
Carsten  Rose's avatar
Carsten Rose committed
296
        foreach ($elements as $element) {
Carsten  Rose's avatar
Carsten Rose committed
297
            $tmpFiles[] = $this->getElement($element);
Carsten  Rose's avatar
Carsten Rose committed
298
299
        }

Carsten  Rose's avatar
Carsten Rose committed
300
301
302
303
        // Export, Concat File(s)
        switch ($downloadMode) {
            case DOWNLOAD_MODE_ZIP:
                $filename = $this->zipFiles($tmpFiles);
304
305
306
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
                }
Carsten  Rose's avatar
Carsten Rose committed
307
308
309
310
                break;

            case DOWNLOAD_MODE_EXCEL:
                throw new DownloadException("Not implemented: $downloadMode", ERROR_NOT_IMPLEMENTED);
Carsten  Rose's avatar
Carsten Rose committed
311
312
                break;

Carsten  Rose's avatar
Carsten Rose committed
313
            case DOWNLOAD_MODE_FILE:
Carsten  Rose's avatar
Carsten Rose committed
314
                $filename = $tmpFiles[0];
315
                if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
316
                    $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
317
                }
Carsten  Rose's avatar
Carsten Rose committed
318
                break;
Carsten  Rose's avatar
Carsten Rose committed
319
320

            case DOWNLOAD_MODE_PDF:
Carsten  Rose's avatar
Carsten Rose committed
321
                $filename = $this->concatPdfFiles($tmpFiles);
322
323
324
325
326
327
328
329
330
331
332
333
                // 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
334
335
336
337
                break;

            default:
                throw new DownloadException("Unknown downloadMode: $downloadMode", ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
338
339
340
                break;
        }

341
342
343
344
345
346
        switch ($outputMode) {

            case OUTPUT_MODE_FILE:
                break;

            case OUTPUT_MODE_DIRECT:
347
                $this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
348
349
                HelperFile::cleanTempFiles([$filename]);
                break;
Carsten  Rose's avatar
Carsten Rose committed
350

351
352
353
354
355
            default:
                throw new CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
        }

        return $filename;
Carsten  Rose's avatar
Carsten Rose committed
356
357
358
    }

    /**
359
360
361
362
363
     * 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
364
365
     * @return string
     * @throws CodeException
366
     * @throws DownloadException
Carsten  Rose's avatar
Carsten Rose committed
367
368
     * @throws UserFormException
     */
369
    public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) {
Carsten  Rose's avatar
Carsten Rose committed
370

371
372
373
        if (!is_array($vars)) {
            $vars = $this->store->getStore(STORE_SIP);
        }
374

375
        return $this->doElements($vars, $outputMode);
Carsten  Rose's avatar
Carsten Rose committed
376
377
378
379
380
    }
}