HelperFile.php 18 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/28/16
 * Time: 8:05 AM
 */

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core\Helper;
10

Marc Egger's avatar
Marc Egger committed
11

12
13
14
15
/**
 * Class HelperFile
 * @package qfq
 */
16
17
18
19
20
21
class HelperFile {

    /**
     * Iterate over array $files. Delete only named files which are stored in '/tmp/' . DOWNLOAD_FILE_PREFIX.
     *
     * @param array $files
Marc Egger's avatar
Marc Egger committed
22
23
     * @throws \CodeException
     * @throws \UserFormException
24
25
26
27
28
     */
    public static function cleanTempFiles(array $files) {

        foreach ($files as $file) {
            if (self::isQfqTemp($file)) {
29
                self::unlink($file);
30
31
32
33
            }

            $dir = dirname($file);
            if (self::isQfqTemp($dir)) {
34
                self::rmdir($dir);
35
36
37
38
            }
        }
    }

39
    /**
40
     * Returns a uniq (use for temporary) filename, prefixed with QFQ TMP_FILE_PREFIX
41
42
43
     *
     * @return bool|string
     */
44
    public static function tempnam() {
45
46
        return tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
    }
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

    /**
     * Check against standard QFQ Temp location. If it is a Qfq Temp location, return true, else false.
     *
     * @param string $name Absolute filename.
     *
     * @return bool
     */
    public static function isQfqTemp($name) {
        $prefix = sys_get_temp_dir() . '/' . TMP_FILE_PREFIX;
        $len = strlen($prefix);

        return (substr($name, 0, $len) == $prefix);
    }

    /**
     * Creates a temporary directory.
64
65
     * Be aware: '/tmp' is under systemd/apache2 (Ubuntu 18...) remapped to something like: '/tmp/systemd-private-...-apache2.service-.../tmp'
     *
Carsten  Rose's avatar
Carsten Rose committed
66
     * @return bool|string
Marc Egger's avatar
Marc Egger committed
67
68
     * @throws \CodeException
     * @throws \UserFormException
69
70
71
     */
    public static function mktempdir() {
        $name = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX);
72
        self::unlink($name);
73
74
75
76
        mkdir($name);

        return $name;
    }
77
78
79
80
81

    /**
     * Return the mimetype of $pathFilename
     *
     * @param string $pathFilename
82
     * @param bool $flagIgnoreError
83
     * @return string
Marc Egger's avatar
Marc Egger committed
84
     * @throws \UserFormException
85
     */
86
    public static function getMimeType($pathFilename, $flagIgnoreError = false) {
87
88
89
90

        // E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
        $fileMimeType = exec('file --brief --mime ' . $pathFilename, $output, $return_var);
        if ($return_var != 0) {
91
92
93
94
95

            if ($flagIgnoreError) {
                return '';
            }

Marc Egger's avatar
Marc Egger committed
96
            throw new \UserFormException('Error get mime type of upload.', ERROR_UPLOAD_GET_MIME_TYPE);
97
98
99
100
101
102
103
        }

        return $fileMimeType;
    }

    /**
     * Returns an array with filestat information to $pathFileName
104
105
     * - mimeType
     * - fileSize
106
107
108
     *
     * @param $pathFileName
     * @return array
Marc Egger's avatar
Marc Egger committed
109
     * @throws \UserFormException
110
111
     */
    public static function getFileStat($pathFileName) {
112
        $vars = [VAR_FILE_MIME_TYPE => '-', VAR_FILE_SIZE => '-'];
113

Carsten  Rose's avatar
Carsten Rose committed
114
        if (empty($pathFileName)) {
115
            return $vars;
116
117
        }

Carsten  Rose's avatar
Carsten Rose committed
118
        $pathFileName = self::correctRelativePathFileName($pathFileName);
119

Carsten  Rose's avatar
Carsten Rose committed
120
        if (!file_exists($pathFileName)) {
121
            return $vars;
122
123
        }

124
125
126
127
128
        $vars[VAR_FILE_MIME_TYPE] = self::getMimeType($pathFileName);
        $vars[VAR_FILE_SIZE] = filesize($pathFileName);

        if ($vars[VAR_FILE_SIZE] === false) {
            $vars[VAR_FILE_SIZE] = '-';
129
        }
130
131
132
133
134

        return $vars;
    }

    /**
Marc Egger's avatar
Marc Egger committed
135
     * Correct $pathFilename, if the cwd is .../qfq/Api'
136
137
138
139
     *
     * @param $pathFileName
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
140
    public static function correctRelativePathFileName($pathFileName) {
141
142
143
144
145
146
147
148
149

        if (empty($pathFileName)) {
            return '';
        }

        if ($pathFileName[0] == '/') {
            return $pathFileName;
        }

150
151
        $length = strlen('/' . API_DIR_EXT);
        if (substr(getcwd(), -$length) == '/' . API_DIR_EXT) {
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
            return '../../../../../' . $pathFileName;
        }

        return $pathFileName;
    }

    /**
     * Split $pathFileName into it's components and fill an array, with array keys like used in STORE_VAR.
     *
     * @param string $pathFileName
     * @return array
     */
    public static function pathInfo($pathFileName) {
        $vars = array();

167
        $pathParts = pathinfo($pathFileName);
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
        $vars[VAR_FILENAME] = $pathFileName;

        if (isset($pathParts['basename'])) {
            $vars[VAR_FILENAME_ONLY] = $pathParts['basename'];
        }

        if (isset($pathParts['filename'])) {
            $vars[VAR_FILENAME_BASE] = $pathParts['filename'];
        }

        if (isset($pathParts['extension'])) {
            $vars[VAR_FILENAME_EXT] = $pathParts['extension'];
        }

        return $vars;
    }

185
186
187
188
189
190
191
192
193
194
195
196
197
    /**
     * Checks the file filetype against the allowed mimeType definition. Return true as soon as one match is found.
     * Types recognized:
     *   * 'mime type' as delivered by `file` which matches a definition on
     *   http://www.iana.org/assignments/media-types/media-types.xhtml
     *   * Joker based: audio/*, video/*, image/*
     *   * Filename extension based: .pdf,.doc,..
     *
     * @param string $pathFileName
     * @param string $origFileName
     * @param string $mimeTypeListAccept
     *
     * @return bool
Marc Egger's avatar
Marc Egger committed
198
     * @throws \UserFormException
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
     */
    public static function checkFileType($pathFileName, $origFileName, $mimeTypeListAccept) {

        // E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
        $fileMimeType = self::getMimeType($pathFileName);

        // Strip optional '; charset=binary'
        $arr = explode(';', $fileMimeType, 2);
        $fileMimeType = $arr[0];

        // Split between 'Media Type' and 'Media Subtype'
        $fileMimeTypeSplitted = explode('/', $arr[0], 2);

        $path_parts = pathinfo($origFileName); // to extract the filename extension of the uploaded file.

        // Process all listed mimeTypes (incl. filename extension and joker)
        // $accept e.g.: 'image/*,application/pdf,.pdf'
        $arr = explode(',', $mimeTypeListAccept); // Split multiple defined mimetypes/extensions in single chunks.
        foreach ($arr as $listElementMimeType) {
            $listElementMimeType = trim(strtolower($listElementMimeType));
            if ($listElementMimeType == '') {
                continue; // will be skipped
            } elseif ($listElementMimeType[0] == '.') { // Check for definition 'filename extension'
                if ('.' . strtolower($path_parts['extension']) == $listElementMimeType) {
                    return true;
                }
            } else {
                // Check for Joker, e.g.: 'image/*'
                $splitted = explode('/', $listElementMimeType, 2);

                if ($splitted[1] == '*') {
                    if ($splitted[0] == $fileMimeTypeSplitted[0]) {
                        return true;
                    }
                } elseif ($fileMimeType == $listElementMimeType) {
                    return true;
                }
            }
        }

        return false;
    }
241
242
243

    /**
     * @param $pathFileName
244
     * @param int|bool $mode
Marc Egger's avatar
Marc Egger committed
245
     * @throws \UserFormException
246
     */
247
    public static function chmod($pathFileName, $mode = false) {
248

249
        if ($mode !== false) {
250
            if (false === @chmod($pathFileName, $mode)) {
Marc Egger's avatar
Marc Egger committed
251
                throw new \UserFormException(
252
                    json_encode([ERROR_MESSAGE_TO_USER => 'Failed: chmod', ERROR_MESSAGE_TO_DEVELOPER => self::errorGetLastAsString()]),
253
                    ERROR_IO_CHMOD);
254
255
256
            }
        }
    }
257
258
259
260

    /**
     * @return string
     */
261
    public static function errorGetLastAsString() {
262
263

        if (NULL === ($errors = error_get_last())) {
Carsten  Rose's avatar
Carsten Rose committed
264
            return 'error_get_last(): no error';
265
266
267
268
269
        }

        return $errors['type'] . ' - ' . $errors['message'];

    }
270
271
272
273
274
275

    /**
     * PHP System function: chdir() with QFQ exception
     *
     * @param $cwd
     * @return string
Marc Egger's avatar
Marc Egger committed
276
     * @throws \UserFormException
277
278
279
     */
    public static function chdir($cwd) {

280
        if (false === @chdir($cwd)) {
281
            $msg = self::errorGetLastAsString() . " - chdir($cwd)";
Marc Egger's avatar
Marc Egger committed
282
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'chdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_CHDIR);
283
284
285
286
287
288
289
290
        }

        return true;
    }

    /**
     * PHP System function: unlink() with QFQ exception
     *
291
292
     * @param $filename
     * @param string $logFilename
293
     * @return string
Marc Egger's avatar
Marc Egger committed
294
295
     * @throws \CodeException
     * @throws \UserFormException
296
     */
297
    public static function unlink($filename, $logFilename = '') {
298

299
300
301
302
        if ($logFilename != '') {
            Logger::logMessageWithPrefix("Unlink: $filename", $logFilename);
        }

303
        if (false === @unlink($filename)) {
304
            $msg = self::errorGetLastAsString() . " - unlink($filename)";
Marc Egger's avatar
Marc Egger committed
305
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_UNLINK);
306
307
308
309
310
311
312
313
314
315
        }

        return true;
    }

    /**
     * PHP System function: rmdir() with QFQ exception
     *
     * @param $tempDir
     * @return string
Marc Egger's avatar
Marc Egger committed
316
     * @throws \UserFormException
317
318
319
     */
    public static function rmdir($tempDir) {

320
        if (false === @rmdir($tempDir)) {
321
            $msg = self::errorGetLastAsString() . " - rmdir($tempDir)";
Marc Egger's avatar
Marc Egger committed
322
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'rmdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RMDIR);
323
324
325
326
327
328
329
330
331
332
333
        }

        return true;
    }

    /**
     * PHP System function: rename() with QFQ exception
     *
     * @param $oldname
     * @param $newname
     * @return string
Marc Egger's avatar
Marc Egger committed
334
     * @throws \UserFormException
335
336
337
     */
    public static function rename($oldname, $newname) {

338
        if (false === @rename($oldname, $newname)) {
339
            $msg = self::errorGetLastAsString() . " - rename($oldname, $newname)";
Marc Egger's avatar
Marc Egger committed
340
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RENAME);
341
342
343
344
345
346
347
348
349
350
351
        }

        return true;
    }

    /**
     * PHP System function: copy() with QFQ exception
     *
     * @param $source
     * @param $dest
     * @return string
Marc Egger's avatar
Marc Egger committed
352
     * @throws \UserFormException
353
354
355
     */
    public static function copy($source, $dest) {

Carsten  Rose's avatar
Carsten Rose committed
356
357
358
359
360
        self::mkDirParent($dest);
        if (!is_file($dest)) {
            touch($dest);
        }

361
        if (false === @copy($source, $dest)) {
Carsten  Rose's avatar
Carsten Rose committed
362
363

            if (!is_readable($source)) {
Marc Egger's avatar
Marc Egger committed
364
                throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "Can't read file '$source'"]), ERROR_IO_READ_FILE);
Carsten  Rose's avatar
Carsten Rose committed
365
366
367
            }

            if (!is_writeable($dest)) {
Marc Egger's avatar
Marc Egger committed
368
                throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$dest'"]), ERROR_IO_WRITE_FILE);
Carsten  Rose's avatar
Carsten Rose committed
369
370
371
372
            }

            $msg = self::errorGetLastAsString(); // Often, there is no specific error string.
            $msg .= " - copy($source, $dest)";
Marc Egger's avatar
Marc Egger committed
373
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_COPY);
374
375
376
377
        }

        return true;
    }
Carsten  Rose's avatar
Carsten Rose committed
378
379
380
381
382
383
384

    /**
     * Creates all necessary directories in $pathFileName, but not the last part, the filename. A filename has to be
     * specified.
     *
     * @param string $pathFileName Path with Filename
     * @param bool|int $chmodDir false if not explicit set
Marc Egger's avatar
Marc Egger committed
385
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
386
387
388
     */
    public static function mkDirParent($pathFileName, $chmodDir = false) {
        $path = "";
389
390
391
392
393
394
395
        $cwd = '';

        // Leading '/' will be removed - chdir to / to still use correct path
        if ($pathFileName[0] == '/') {
            $cwd = getcwd();
            self::chdir('/');
        }
Carsten  Rose's avatar
Carsten Rose committed
396
397
398
399

        // Teile "Directory/File.Extension" auf
        $pathParts = pathinfo($pathFileName);

400

Carsten  Rose's avatar
Carsten Rose committed
401
402
403
404
405
406
        // Zerlege Pfad in einzelne Directories
        $arr = explode("/", $pathParts["dirname"]);

        // Durchlaufe die einzelnen Dirs und überprüfe ob sie angelegt sind.
        // Wenn nicht, lege sie an.
        foreach ($arr as $part) {
Carsten  Rose's avatar
Carsten Rose committed
407

Carsten  Rose's avatar
Carsten Rose committed
408
            if ($part == '') {// Happens with '/...'
Carsten  Rose's avatar
Carsten Rose committed
409
410
411
                continue;
            }

Carsten  Rose's avatar
Carsten Rose committed
412
413
414
415
416
            $path .= $part;

            // Check ob der Pfad ein Link ist
            if ("link" == @filetype($path)) {
                if ("0" == ($path1 = readlink($path))) {
Marc Egger's avatar
Marc Egger committed
417
                    throw new \UserFormException("Can't create '$pathFileName': '$path' contains an invalid link.", ERROR_IO_INVALID_LINK);
Carsten  Rose's avatar
Carsten Rose committed
418
419
420
421
                }
            } else {
                if (file_exists($path)) {
                    if ("dir" != filetype($path)) {
Marc Egger's avatar
Marc Egger committed
422
                        throw new \UserFormException("Can't create '$pathFileName': There is already a file with the same name as '$path'", ERROR_IO_DIR_EXIST_AS_FILE);
Carsten  Rose's avatar
Carsten Rose committed
423
424
425
426
427
428
429
430
                    }
                } else {
                    mkdir($path);
                    HelperFile::chmod($path, $chmodDir);
                }
            }
            $path .= "/";
        }
431
432
433
434

        if ($cwd != '') {
            self::chdir($cwd);
        }
Carsten  Rose's avatar
Carsten Rose committed
435
436
    }

Carsten  Rose's avatar
Carsten Rose committed
437
438
439
440
441
442
    /**
     * Determine highlight name, based on the given $highlight or provided filename  (the extension will be used)
     *
     * @param $highlight
     * @param $fileName
     * @return string
Marc Egger's avatar
Marc Egger committed
443
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
     */
    public static function getFileTypeHighlight($highlight, $fileName) {

        $extToFileType = [
            'js' => 'javascript.json'
            , 'javascript' => 'javascript.json'
            , 'qfq' => 'highlight.qfq.json'
            , 'php' => 'highlight.php.json'
            , 'py' => 'highlight.py.json'
            , 'python' => 'highlight.py.json'
            , 'matlab' => 'highlight.m.json'
            , 'm' => 'highlight.m.json'
        ];

        switch ($highlight) {
            case FE_HIGHLIGHT_AUTO:
                $arr = explode('.', $fileName);
                $ext = strtolower(end($arr));
                $highlight = (isset($extToFileType[$ext])) ? $extToFileType[$ext] : '';
                break;
            case FE_HIGHLIGHT_JAVASCRIPT:
            case FE_HIGHLIGHT_QFQ:
            case FE_HIGHLIGHT_PYTHON:
            case FE_HIGHLIGHT_MATLAB:
                $ext = $highlight;
                break;
            case FE_HIGHLIGHT_OFF:
            case '':
                $ext = '';
                break;
            default:
Marc Egger's avatar
Marc Egger committed
475
                throw new \UserFormException("Unknown highlight type: " . $highlight, ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
476
477
478
479
480
481
482
483
484
485
                break;
        }

        if (isset($extToFileType[$ext])) {
            return DIR_HIGHLIGHT_JSON . '/' . $extToFileType[$ext];
        }

        return '';
    }

486
487
488
489
    /**
     * Get array of split file names. Remove '.', '..', QFQ_TEMP_SOURCE.
     * @param $dir
     * @return array
Marc Egger's avatar
Marc Egger committed
490
     * @throws \UserFormException
491
492
493
494
495
     */
    public static function getSplitFileNames($dir) {

        // Array of created file names.
        if (false === ($files = scandir($dir))) {
Marc Egger's avatar
Marc Egger committed
496
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
497
                json_encode([ERROR_MESSAGE_TO_USER => 'Splitted files not found', ERROR_MESSAGE_TO_DEVELOPER => "[cwd=$dir] scandir(.)" . HelperFile::errorGetLastAsString()]),
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
                ERROR_PDF2JPEG);
        }

        $new = Array();
        foreach ($files as $key => $value) {

            if ($value == '.' || $value == '..' || $value == QFQ_TEMP_SOURCE) {
                continue;
            }

            // IM 'convert' use a strange auto numbering: if there is only one page: split.jpg, if there are multiple pages, first: split-0.jpg, ....
            if ($value == 'split.jpg') {
                $value = 'split-0.jpg';
                self::rename($dir . '/' . $files[$key], $dir . '/' . $value);
            }

            $new[] = $value;
        }

        return $new;
    }
Carsten  Rose's avatar
Carsten Rose committed
519

520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
    /**
     * Joins $pre and $post. If $post is absolute, returns only $post. If $pre ends without '/', a '/' is injected.
     *
     * @param $pre
     * @param $post
     *
     * @return string
     */
    public static function joinPathFilename($pre, $post) {
        $separator = '';

        if ($pre == '' || $post == '') {
            return $pre . $post;
        }

        if ($post[0] == '/') {
            return $post;
        }

        if ((substr($pre, -1) != '/')) {
            $separator = '/';
        }

        return $pre . $separator . $post;
    }
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

    /**
     * Translates ZIP error codes to text.
     *
     * @param $errno
     * @return string
     */
    public static function zipFileErrMsg($errno) {

        // using constant name as a string to make this function PHP4 compatible
        $zipFileFunctionsErrors = array(
            'ZIPARCHIVE::ER_MULTIDISK' => 'Multi-disk zip archives not supported.',
            'ZIPARCHIVE::ER_RENAME' => 'Renaming temporary file failed.',
            'ZIPARCHIVE::ER_CLOSE' => 'Closing zip archive failed',
            'ZIPARCHIVE::ER_SEEK' => 'Seek error',
            'ZIPARCHIVE::ER_READ' => 'Read error',
            'ZIPARCHIVE::ER_WRITE' => 'Write error',
            'ZIPARCHIVE::ER_CRC' => 'CRC error',
            'ZIPARCHIVE::ER_ZIPCLOSED' => 'Containing zip archive was closed',
            'ZIPARCHIVE::ER_NOENT' => 'No such file.',
            'ZIPARCHIVE::ER_EXISTS' => 'File already exists',
            'ZIPARCHIVE::ER_OPEN' => 'Can\'t open file',
            'ZIPARCHIVE::ER_TMPOPEN' => 'Failure to create temporary file.',
            'ZIPARCHIVE::ER_ZLIB' => 'Zlib error',
            'ZIPARCHIVE::ER_MEMORY' => 'Memory allocation failure',
            'ZIPARCHIVE::ER_CHANGED' => 'Entry has been changed',
            'ZIPARCHIVE::ER_COMPNOTSUPP' => 'Compression method not supported.',
            'ZIPARCHIVE::ER_EOF' => 'Premature EOF',
            'ZIPARCHIVE::ER_INVAL' => 'Invalid argument',
            'ZIPARCHIVE::ER_NOZIP' => 'Not a zip archive',
            'ZIPARCHIVE::ER_INTERNAL' => 'Internal error',
            'ZIPARCHIVE::ER_INCONS' => 'Zip archive inconsistent',
            'ZIPARCHIVE::ER_REMOVE' => 'Can\'t remove file',
            'ZIPARCHIVE::ER_DELETED' => 'Entry has been deleted',
        );

        return $zipFileFunctionsErrors[$errno] ?? 'unknown';
    }
Carsten  Rose's avatar
Carsten Rose committed
583
584
}