1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Assets; |
4
|
|
|
|
5
|
|
|
use SilverStripe\Core\Config\Config; |
6
|
|
|
use SilverStripe\Dev\SapphireTest; |
7
|
|
|
|
8
|
|
|
class Upload_Validator |
9
|
|
|
{ |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Contains a list of the max file sizes shared by |
13
|
|
|
* all upload fields. This is then duplicated into the |
14
|
|
|
* "allowedMaxFileSize" instance property on construct. |
15
|
|
|
* |
16
|
|
|
* @config |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
private static $default_max_file_size = array(); |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Information about the temporary file produced |
23
|
|
|
* by the PHP-runtime. |
24
|
|
|
* |
25
|
|
|
* @var array |
26
|
|
|
*/ |
27
|
|
|
protected $tmpFile; |
28
|
|
|
|
29
|
|
|
protected $errors = array(); |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Restrict filesize for either all filetypes |
33
|
|
|
* or a specific extension, with extension-name |
34
|
|
|
* as array-key and the size-restriction in bytes as array-value. |
35
|
|
|
* |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
public $allowedMaxFileSize = array(); |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var array Collection of extensions. |
42
|
|
|
* Extension-names are treated case-insensitive. |
43
|
|
|
* |
44
|
|
|
* Example: |
45
|
|
|
* <code> |
46
|
|
|
* array("jpg","GIF") |
47
|
|
|
* </code> |
48
|
|
|
*/ |
49
|
|
|
public $allowedExtensions = array(); |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Return all errors that occurred while validating |
53
|
|
|
* the temporary file. |
54
|
|
|
* |
55
|
|
|
* @return array |
56
|
|
|
*/ |
57
|
|
|
public function getErrors() |
58
|
|
|
{ |
59
|
|
|
return $this->errors; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Clear out all errors |
64
|
|
|
*/ |
65
|
|
|
public function clearErrors() |
66
|
|
|
{ |
67
|
|
|
$this->errors = array(); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Set information about temporary file produced by PHP. |
72
|
|
|
* @param array $tmpFile |
73
|
|
|
*/ |
74
|
|
|
public function setTmpFile($tmpFile) |
75
|
|
|
{ |
76
|
|
|
$this->tmpFile = $tmpFile; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Get maximum file size for all or specified file extension. |
81
|
|
|
* |
82
|
|
|
* @param string $ext |
83
|
|
|
* @return int Filesize in bytes |
84
|
|
|
*/ |
85
|
|
|
public function getAllowedMaxFileSize($ext = null) |
86
|
|
|
{ |
87
|
|
|
|
88
|
|
|
// Check if there is any defined instance max file sizes |
89
|
|
|
if (empty($this->allowedMaxFileSize)) { |
90
|
|
|
// Set default max file sizes if there isn't |
91
|
|
|
$fileSize = Config::inst()->get('SilverStripe\\Assets\\Upload_Validator', 'default_max_file_size'); |
92
|
|
|
if ($fileSize) { |
93
|
|
|
$this->setAllowedMaxFileSize($fileSize); |
94
|
|
|
} else { |
95
|
|
|
// When no default is present, use maximum set by PHP |
96
|
|
|
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize')); |
97
|
|
|
$maxPost = File::ini2bytes(ini_get('post_max_size')); |
98
|
|
|
$this->setAllowedMaxFileSize(min($maxUpload, $maxPost)); |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$ext = strtolower($ext); |
103
|
|
|
if ($ext) { |
104
|
|
|
if (isset($this->allowedMaxFileSize[$ext])) { |
105
|
|
|
return $this->allowedMaxFileSize[$ext]; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
$category = File::get_app_category($ext); |
109
|
|
|
if ($category && isset($this->allowedMaxFileSize['[' . $category . ']'])) { |
110
|
|
|
return $this->allowedMaxFileSize['[' . $category . ']']; |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Set filesize maximums (in bytes or INI format). |
119
|
|
|
* Automatically converts extensions to lowercase |
120
|
|
|
* for easier matching. |
121
|
|
|
* |
122
|
|
|
* Example: |
123
|
|
|
* <code> |
124
|
|
|
* array('*' => 200, 'jpg' => 1000, '[doc]' => '5m') |
125
|
|
|
* </code> |
126
|
|
|
* |
127
|
|
|
* @param array|int $rules |
128
|
|
|
*/ |
129
|
|
|
public function setAllowedMaxFileSize($rules) |
130
|
|
|
{ |
131
|
|
|
if (is_array($rules) && count($rules)) { |
132
|
|
|
// make sure all extensions are lowercase |
133
|
|
|
$rules = array_change_key_case($rules, CASE_LOWER); |
134
|
|
|
$finalRules = array(); |
135
|
|
|
|
136
|
|
|
foreach ($rules as $rule => $value) { |
137
|
|
|
if (is_numeric($value)) { |
138
|
|
|
$tmpSize = $value; |
139
|
|
|
} else { |
140
|
|
|
$tmpSize = File::ini2bytes($value); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$finalRules[$rule] = (int)$tmpSize; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
$this->allowedMaxFileSize = $finalRules; |
147
|
|
|
} elseif (is_string($rules)) { |
148
|
|
|
$this->allowedMaxFileSize['*'] = File::ini2bytes($rules); |
149
|
|
|
} elseif ((int)$rules > 0) { |
150
|
|
|
$this->allowedMaxFileSize['*'] = (int)$rules; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @return array |
156
|
|
|
*/ |
157
|
|
|
public function getAllowedExtensions() |
158
|
|
|
{ |
159
|
|
|
return $this->allowedExtensions; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Limit allowed file extensions. Empty by default, allowing all extensions. |
164
|
|
|
* To allow files without an extension, use an empty string. |
165
|
|
|
* See {@link File::$allowed_extensions} to get a good standard set of |
166
|
|
|
* extensions that are typically not harmful in a webserver context. |
167
|
|
|
* See {@link setAllowedMaxFileSize()} to limit file size by extension. |
168
|
|
|
* |
169
|
|
|
* @param array $rules List of extensions |
170
|
|
|
*/ |
171
|
|
|
public function setAllowedExtensions($rules) |
172
|
|
|
{ |
173
|
|
|
if (!is_array($rules)) { |
174
|
|
|
return; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// make sure all rules are lowercase |
178
|
|
|
foreach ($rules as &$rule) { |
179
|
|
|
$rule = strtolower($rule); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$this->allowedExtensions = $rules; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Determines if the bytesize of an uploaded |
187
|
|
|
* file is valid - can be defined on an |
188
|
|
|
* extension-by-extension basis in {@link $allowedMaxFileSize} |
189
|
|
|
* |
190
|
|
|
* @return boolean |
191
|
|
|
*/ |
192
|
|
|
public function isValidSize() |
193
|
|
|
{ |
194
|
|
|
// If file was blocked via PHP for being excessive size, shortcut here |
195
|
|
|
switch ($this->tmpFile['error']) { |
196
|
|
|
case UPLOAD_ERR_INI_SIZE: |
197
|
|
|
case UPLOAD_ERR_FORM_SIZE: |
198
|
|
|
return false; |
199
|
|
|
} |
200
|
|
|
$pathInfo = pathinfo($this->tmpFile['name']); |
201
|
|
|
$extension = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : null; |
202
|
|
|
$maxSize = $this->getAllowedMaxFileSize($extension); |
203
|
|
|
return (!$this->tmpFile['size'] || !$maxSize || (int)$this->tmpFile['size'] < $maxSize); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Determine if this file is valid but empty |
208
|
|
|
* |
209
|
|
|
* @return bool |
210
|
|
|
*/ |
211
|
|
|
public function isFileEmpty() { |
212
|
|
|
// Don't check file size for errors |
213
|
|
|
if ($this->tmpFile['error'] !== UPLOAD_ERR_OK) { |
214
|
|
|
return false; |
215
|
|
|
} |
216
|
|
|
return empty($this->tmpFile['size']); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Determines if the temporary file has a valid extension |
221
|
|
|
* An empty string in the validation map indicates files without an extension. |
222
|
|
|
* @return boolean |
223
|
|
|
*/ |
224
|
|
|
public function isValidExtension() |
225
|
|
|
{ |
226
|
|
|
$pathInfo = pathinfo($this->tmpFile['name']); |
227
|
|
|
|
228
|
|
|
// Special case for filenames without an extension |
229
|
|
|
if (!isset($pathInfo['extension'])) { |
230
|
|
|
return in_array('', $this->allowedExtensions, true); |
231
|
|
|
} else { |
232
|
|
|
return (!count($this->allowedExtensions) |
233
|
|
|
|| in_array(strtolower($pathInfo['extension']), $this->allowedExtensions)); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Run through the rules for this validator checking against |
239
|
|
|
* the temporary file set by {@link setTmpFile()} to see if |
240
|
|
|
* the file is deemed valid or not. |
241
|
|
|
* |
242
|
|
|
* @return boolean |
243
|
|
|
*/ |
244
|
|
|
public function validate() |
245
|
|
|
{ |
246
|
|
|
// we don't validate for empty upload fields yet |
247
|
|
|
if (empty($this->tmpFile['name'])) { |
248
|
|
|
return true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
// Check file upload |
252
|
|
|
if (!$this->isValidUpload()) { |
253
|
|
|
$this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload'); |
254
|
|
|
return false; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
// Check file isn't empty |
258
|
|
|
if ($this->isFileEmpty()) { |
259
|
|
|
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.'); |
260
|
|
|
return false; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
// filesize validation |
264
|
|
View Code Duplication |
if (!$this->isValidSize()) { |
|
|
|
|
265
|
|
|
$pathInfo = pathinfo($this->tmpFile['name']); |
266
|
|
|
$ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : ''; |
267
|
|
|
$arg = File::format_size($this->getAllowedMaxFileSize($ext)); |
268
|
|
|
$this->errors[] = _t( |
269
|
|
|
'File.TOOLARGE', |
270
|
|
|
'Filesize is too large, maximum {size} allowed', |
271
|
|
|
'Argument 1: Filesize (e.g. 1MB)', |
272
|
|
|
array('size' => $arg) |
273
|
|
|
); |
274
|
|
|
return false; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// extension validation |
278
|
|
View Code Duplication |
if (!$this->isValidExtension()) { |
|
|
|
|
279
|
|
|
$this->errors[] = _t( |
280
|
|
|
'File.INVALIDEXTENSION', |
281
|
|
|
'Extension is not allowed (valid: {extensions})', |
282
|
|
|
'Argument 1: Comma-separated list of valid extensions', |
283
|
|
|
array('extensions' => wordwrap(implode(', ', $this->allowedExtensions))) |
284
|
|
|
); |
285
|
|
|
return false; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
return true; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Check that a valid file was given for upload (ignores file size) |
293
|
|
|
* |
294
|
|
|
* @return bool |
295
|
|
|
*/ |
296
|
|
|
public function isValidUpload() { |
297
|
|
|
// Check file upload |
298
|
|
|
if ($this->tmpFile['error'] === UPLOAD_ERR_NO_FILE) { |
299
|
|
|
return false; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
// Check if file is valid uploaded (with exception for unit testing) |
303
|
|
|
// Note that some "max file size" errors leave "temp_name" empty, so don't fail on this. |
304
|
|
|
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()); |
305
|
|
|
if (!empty($this->tmpFile['tmp_name']) && !is_uploaded_file($this->tmpFile['tmp_name']) && !$isRunningTests) { |
306
|
|
|
return false; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return true; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
} |
313
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.