Completed
Push — master ( 301436...602946 )
by Ingo
17:26 queued 05:06
created

Upload_Validator::isValidUpload()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 7
nc 5
nop 0
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
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
		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
		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