File.php 7.48 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
2
3
4
5
6
7
8
9
10
11
12
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 4/25/16
 * Time: 10:39 PM
 */

namespace qfq;

require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/Constants.php');
13
require_once(__DIR__ . '/helper/HelperFile.php');
Carsten  Rose's avatar
Carsten Rose committed
14

15
16
17
18
/**
 * Class File
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
19
20
class File {

21
22
    private $uploadErrMsg = array();

Carsten  Rose's avatar
Carsten Rose committed
23
    /**
24
     * @var Store
Carsten  Rose's avatar
Carsten Rose committed
25
26
27
     */
    private $store = null;

28
29
30
31
32
33
34
    /**
     * @var Session
     */
    private $session = null;

    /**
     * @param bool|false $phpUnit
35
36
     * @throws CodeException
     * @throws UserFormException
37
     * @throws UserReportException
38
     */
Carsten  Rose's avatar
Carsten Rose committed
39
    public function __construct($phpUnit = false) {
40
        $this->session = Session::getInstance($phpUnit);
41

42
        $this->store = Store::getInstance('', $phpUnit);
43

44
        $this->uploadErrMsg = [
Carsten  Rose's avatar
Carsten Rose committed
45
            UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini",
46
            UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form",
Carsten  Rose's avatar
Carsten Rose committed
47
48
            UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded",
            UPLOAD_ERR_NO_FILE => "No file was uploaded",
49
50
            UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder",
            UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk",
Carsten  Rose's avatar
Carsten Rose committed
51
            UPLOAD_ERR_EXTENSION => "File upload stopped by extension",
52
        ];
Carsten  Rose's avatar
Carsten Rose committed
53
54
    }

55
    /**
56
     * @throws CodeException
57
58
     * @throws UserFormException
     */
Carsten  Rose's avatar
Carsten Rose committed
59
60
    public function process() {

61
62
        $sipUpload = $this->store->getVar(SIP_SIP, STORE_SIP);
        if ($sipUpload === false) {
63
64
65
66
67
68
69
70

            // Throws an exception if content is too big - if content is bigger than 'post_max_size', the POST is lost together with the PHP Upload error message.
            $this->checkMaxFileSize($_SERVER['CONTENT_LENGTH']);

            if(empty($_FILES)){
                throw new UserFormException('Missing $_FILES[] - the whole request seems broken', ERROR_UPLOAD_FILES_BROKEN);
            }

71
72
            throw new UserFormException('SIP invalid: ' . $sipUpload, ERROR_SIP_INVALID);
        }
73

74
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA, SANITIZE_ALLOW_ALL);
75
76
        if ($statusUpload === false) {
            $statusUpload = array();
Carsten  Rose's avatar
Carsten Rose committed
77
78
        }

79
80
81
82
83
84
85
86
87
        $action = $this->store->getVar(FILE_ACTION, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
        switch ($action) {
            case FILE_ACTION_UPLOAD:
                $this->doUpload($sipUpload, $statusUpload);
                break;
            case FILE_ACTION_DELETE:
                $this->doDelete($sipUpload, $statusUpload);
                break;
            default:
Carsten  Rose's avatar
Carsten Rose committed
88
                throw new UserFormException("Unknown FILE_ACTION: $action", ERROR_UPLOAD_UNKNOWN_ACTION);
89
90
        }
    }
Carsten  Rose's avatar
Carsten Rose committed
91

92
    /**
93
94
95
     * Checks the max file size, defined per FormElement.
     * Only possible if there is a valid SIP Store.
     *
96
97
98
99
100
101
102
103
104
105
106
107
     * @param $size
     * @throws CodeException
     * @throws UserFormException
     */
    private function checkMaxFileSize($size) {

        $maxFileSize = $this->store->getVar(FE_FILE_MAX_FILE_SIZE, STORE_SIP);

        $msg = 'File too big.';
        if($maxFileSize!==false){
            $msg .= ' Max allowed size: ' . $maxFileSize;
        }
108
109

        if ($maxFileSize===false || $size >= $maxFileSize) {
110
111
112
113
            throw new UserFormException($msg, ERROR_UPLOAD_TOO_BIG);
        }
    }

114
    /**
115
     * @param string $sipUpload
Carsten  Rose's avatar
Carsten Rose committed
116
     * @param array $statusUpload
Carsten  Rose's avatar
Carsten Rose committed
117
     *
118
119
120
     * @throws CodeException
     * @throws UserFormException
     */
121
    private function doUpload($sipUpload, array $statusUpload) {
122
        $newArr = reset($_FILES);
123
        $statusUpload = array_merge($statusUpload, $newArr);
124

125
        if ($statusUpload[FILES_ERROR] !== UPLOAD_ERR_OK) {
126
            throw new UserFormException($this->uploadErrMsg[$newArr[FILES_ERROR]], ERROR_UPLOAD);
Carsten  Rose's avatar
Carsten Rose committed
127
128
        }

129
        $this->checkMaxFileSize($statusUpload['size']);
130

131
132
133
134
        $accept = $this->store->getVar(FE_FILE_MIME_TYPE_ACCEPT, STORE_SIP);
        if (!$this->checkFileType($statusUpload['tmp_name'], $statusUpload['name'], $accept)) {
            throw new UserFormException('Filetype not allowed. Allowed: ' . $accept, ERROR_UPLOAD_FILE_TYPE);
        }
135
136
137

        // rename uploaded file: ?.cached
        $filenameCached = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
Carsten  Rose's avatar
Carsten Rose committed
138
        move_uploaded_file($newArr[FILES_TMP_NAME], $filenameCached);
Carsten  Rose's avatar
Carsten Rose committed
139

140
        $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA);
Carsten  Rose's avatar
Carsten Rose committed
141
    }
Carsten  Rose's avatar
Carsten Rose committed
142

143
    /**
144
145
     * Checks the file filetype against the allowed mimetype definition. Return true as soon as one match is found.
     * Types recognized:
Carsten  Rose's avatar
Carsten Rose committed
146
147
     *   * 'mime type' as delivered by `file` which matches a definition on
     *   http://www.iana.org/assignments/media-types/media-types.xhtml
148
149
150
151
152
153
     *   * Joker based: audio/*, video/*, image/*
     *   * Filename extension based: .pdf,.doc,..
     *
     * @param string $tmp_name
     * @param string $name
     * @param string $accept
Carsten  Rose's avatar
Carsten Rose committed
154
     *
155
156
157
158
159
160
     * @return bool
     * @throws UserFormException
     */
    private function checkFileType($tmp_name, $name, $accept) {

        // E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
161
162
        $fileMimeType = HelperFile::getMimeType($tmp_name);

163
164
165
166
167
168
169
170
171
172
173
174
175
        // 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($name); // 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(',', $accept); // Split multiple defined mimetypes/extensions in single chunks.
        foreach ($arr as $listElementMimeType) {
176
            $listElementMimeType = trim(strtolower($listElementMimeType));
177
178
            if ($listElementMimeType == '') {
                continue; // will be skipped
179
            } elseif ($listElementMimeType[0] == '.') { // Check for definition 'filename extension'
180
                if ('.' . strtolower($path_parts['extension']) == $listElementMimeType) {
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
                    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;
    }

    /**
     * @param $sipUpload
     * @param $statusUpload
Carsten  Rose's avatar
Carsten Rose committed
203
     *
204
205
     * @throws CodeException
     * @throws UserFormException
206
     * @internal param string $keyStoreExtra
207
208
209
210
211
212
213
214
215
216
217
218
     */
    private function doDelete($sipUpload, $statusUpload) {

        if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') {
            $file = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
            if (file_exists($file)) {
                if (!unlink($file)) {
                    throw new UserFormException('unlink file: ' . $file, ERROR_IO_UNLINK);
                }
            }
            $statusUpload[FILES_TMP_NAME] = '';
        }
219

220
221
222
        $statusUpload[FILES_FLAG_DELETE] = '1';
        $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA);
    }
Carsten  Rose's avatar
Carsten Rose committed
223
}