Completed
Push — php7-support ( eda8a5...04e19e )
by Sam
06:09
created

DBFile::validate()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 8.5907
cc 5
eloc 16
nc 6
nop 2
1
<?php
2
3
namespace SilverStripe\Model\FieldType;
4
5
use SilverStripe\Filesystem\ImageManipulation;
6
use SilverStripe\Filesystem\Storage\AssetContainer;
7
use SilverStripe\Filesystem\Storage\AssetStore;
8
9
use Injector;
10
use AssetField;
11
use File;
12
use Director;
13
use Permission;
14
use ShortcodeHandler;
15
use ValidationResult;
16
use ValidationException;
17
18
// Un-comment once https://github.com/silverstripe/silverstripe-framework/pull/4551/ is merged
19
// namespace SilverStripe\Filesystem\Storage;
20
21
/**
22
 * Represents a file reference stored in a database
23
 *
24
 * @property string $Hash SHA of the file
25
 * @property string $Filename Name of the file, including directory
26
 * @property string $Variant Variant of the file
27
 *
28
 * @package framework
29
 * @subpackage filesystem
30
 */
31
class DBFile extends DBComposite implements AssetContainer, ShortcodeHandler {
32
33
	use ImageManipulation;
34
35
	/**
36
	 * List of allowed file categories.
37
	 *
38
	 * {@see File::$app_categories}
39
	 *
40
	 * @var array
41
	 */
42
	protected $allowedCategories = array();
43
44
	/**
45
	 * List of image mime types supported by the image manipulations API
46
	 *
47
	 * {@see File::app_categories} for matching extensions.
48
	 *
49
	 * @config
50
	 * @var array
51
	 */
52
	private static $supported_images = array(
53
		'image/jpeg',
54
		'image/gif',
55
		'image/png'
56
	);
57
58
	/**
59
	 * Create a new image manipulation
60
	 *
61
	 * @param string $name
62
	 * @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
63
	 */
64
	public function __construct($name = null, $allowed = array()) {
65
		parent::__construct($name);
66
		$this->setAllowedCategories($allowed);
67
	}
68
69
	/**
70
	 * Determine if a valid non-empty image exists behind this asset, which is a format
71
	 * compatible with image manipulations
72
	 *
73
	 * @return boolean
74
	 */
75
	public function getIsImage() {
76
		// Check file type
77
		$mime = $this->getMimeType();
78
		return $mime && in_array($mime, $this->config()->supported_images);
0 ignored issues
show
Bug Best Practice introduced by
The expression $mime of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
79
	}
80
81
	/**
82
	 * @return AssetStore
83
	 */
84
	protected function getStore() {
85
		return Injector::inst()->get('AssetStore');
86
	}
87
88
	private static $composite_db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
89
		"Hash" => "Varchar(255)", // SHA of the base content
90
		"Filename" => "Varchar(255)", // Path identifier of the base content
91
		"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
92
	);
93
94
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
95
		'URL' => 'Varchar',
96
		'AbsoluteURL' => 'Varchar',
97
		'Basename' => 'Varchar',
98
		'Title' => 'Varchar',
99
		'MimeType' => 'Varchar',
100
		'String' => 'Text',
101
		'Tag' => 'HTMLText',
102
		'Size' => 'Varchar'
103
	);
104
105
	public function scaffoldFormField($title = null, $params = null) {
106
		return AssetField::create($this->getName(), $title);
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
107
	}
108
109
	/**
110
	 * Return a html5 tag of the appropriate for this file (normally img or a)
111
	 *
112
	 * @return string
113
	 */
114
	public function forTemplate() {
115
		return $this->getTag() ?: '';
116
	}
117
118
	/**
119
	 * Return a html5 tag of the appropriate for this file (normally img or a)
120
	 *
121
	 * @return string
122
	 */
123
	public function getTag() {
124
		$template = $this->getFrontendTemplate();
125
		if(empty($template)) {
126
			return '';
127
		}
128
		return (string)$this->renderWith($template);
129
	}
130
131
	/**
132
	 * Determine the template to render as on the frontend
133
	 *
134
	 * @return string Name of template
135
	 */
136
	public function getFrontendTemplate() {
137
		// Check that path is available
138
		$url = $this->getURL();
139
		if(empty($url)) {
140
			return null;
141
		}
142
143
		// Image template for supported images
144
		if($this->getIsImage()) {
145
			return 'DBFile_image';
146
		}
147
148
		// Default download
149
		return 'DBFile_download';
150
	}
151
152
	/**
153
	 * Get trailing part of filename
154
	 *
155
	 * @return string
156
	 */
157
	public function getBasename() {
158
		if($this->exists()) {
159
			return basename($this->getSourceURL());
160
		}
161
	}
162
163
	/**
164
	 * Get file extension
165
	 *
166
	 * @return string
167
	 */
168
	public function getExtension() {
169
		if($this->exists()) {
170
			return pathinfo($this->Filename, PATHINFO_EXTENSION);
171
		}
172
	}
173
174
	/**
175
	 * Alt title for this
176
	 *
177
	 * @return string
178
	 */
179
	public function getTitle() {
180
		// If customised, use the customised title
181
		if($this->failover && ($title = $this->failover->Title)) {
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
182
			return $title;
183
		}
184
		// fallback to using base name
185
		return $this->getBasename();
186
	}
187
188 View Code Duplication
	public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
189
		$this->assertFilenameValid($filename ?: $path);
190
		$result = $this
191
			->getStore()
192
			->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
193
		// Update from result
194
		if($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
195
			$this->setValue($result);
196
		}
197
		return $result;
198
	}
199
200 View Code Duplication
	public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
201
		$this->assertFilenameValid($filename);
202
		$result = $this
203
			->getStore()
204
			->setFromStream($stream, $filename, $hash, $variant, $conflictResolution);
205
		// Update from result
206
		if($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
207
			$this->setValue($result);
208
		}
209
		return $result;
210
	}
211
212 View Code Duplication
	public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
213
		$this->assertFilenameValid($filename);
214
		$result = $this
215
			->getStore()
216
			->setFromString($data, $filename, $hash, $variant, $conflictResolution);
217
		// Update from result
218
		if($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
219
			$this->setValue($result);
220
		}
221
		return $result;
222
	}
223
224 View Code Duplication
	public function getStream() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
225
		if(!$this->exists()) {
226
			return null;
227
		}
228
		return $this
229
			->getStore()
230
			->getAsStream($this->Filename, $this->Hash, $this->Variant);
231
	}
232
233 View Code Duplication
	public function getString() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
234
		if(!$this->exists()) {
235
			return null;
236
		}
237
		return $this
238
			->getStore()
239
			->getAsString($this->Filename, $this->Hash, $this->Variant);
240
	}
241
242
	public function getURL() {
243
		if(!$this->exists()) {
244
			return null;
245
		}
246
		$url = $this->getSourceURL();
247
		$this->updateURL($url);
248
		$this->extend('updateURL', $url);
249
		return $url;
250
	}
251
252
	/**
253
	 * Get URL, but without resampling.
254
	 * Note that this will return the url even if the file does not exist.
255
	 *
256
	 * @return string
257
	 */
258
	public function getSourceURL() {
259
		return $this
260
			->getStore()
261
			->getAsURL($this->Filename, $this->Hash, $this->Variant);
262
	}
263
264
	/**
265
	 * Get the absolute URL to this resource
266
	 *
267
	 * @return type
268
	 */
269
	public function getAbsoluteURL() {
270
		if(!$this->exists()) {
271
			return null;
272
		}
273
		return Director::absoluteURL($this->getURL());
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($this->getURL()); of type string|false adds false to the return on line 273 which is incompatible with the return type declared by the interface SilverStripe\Filesystem\...ntainer::getAbsoluteURL of type string. It seems like you forgot to handle an error condition.
Loading history...
274
	}
275
276 View Code Duplication
	public function getMetaData() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
277
		if(!$this->exists()) {
278
			return null;
279
		}
280
		return $this
281
			->getStore()
282
			->getMetadata($this->Filename, $this->Hash, $this->Variant);
283
	}
284
285 View Code Duplication
	public function getMimeType() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
286
		if(!$this->exists()) {
287
			return null;
288
		}
289
		return $this
290
			->getStore()
291
			->getMimeType($this->Filename, $this->Hash, $this->Variant);
292
	}
293
	
294
	public function getValue() {
295
		if($this->exists()) {
296
			return array(
297
				'Filename' => $this->Filename,
298
				'Hash' => $this->Hash,
299
				'Variant' => $this->Variant
300
			);
301
		}
302
	}
303
304 View Code Duplication
	public function exists() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
305
		if(empty($this->Filename)) {
306
			return false;
307
		}
308
		return $this
309
			->getStore()
310
			->exists($this->Filename, $this->Hash, $this->Variant);
311
	}
312
313
	public static function get_shortcodes() {
314
		return 'dbfile_link';
315
	}
316
317
	public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
318
		// @todo
319
	}
320
	
321
	public function getFilename() {
322
		return $this->getField('Filename');
323
	}
324
325
	public function getHash() {
326
		return $this->getField('Hash');
327
	}
328
329
	public function getVariant() {
330
		return $this->getField('Variant');
331
	}
332
333
	/**
334
	 * Return file size in bytes.
335
	 *
336
	 * @return int
337
	 */
338
	public function getAbsoluteSize() {
339
		$metadata = $this->getMetaData();
340
		if(isset($metadata['size'])) {
341
			return $metadata['size'];
342
		}
343
	}
344
345
	/**
346
	 * Customise this object with an "original" record for getting other customised fields
347
	 *
348
	 * @param AssetContainer $original
349
	 * @return $this
350
	 */
351
	public function setOriginal($original) {
352
		$this->failover = $original;
0 ignored issues
show
Documentation Bug introduced by
It seems like $original of type object<SilverStripe\File...Storage\AssetContainer> is incompatible with the declared type object<ViewableData> of property $failover.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
353
		return $this;
354
	}
355
356
	/**
357
	 * Get list of allowed file categories
358
	 *
359
	 * @return array
360
	 */
361
	public function getAllowedCategories() {
362
		return $this->allowedCategories;
363
	}
364
365
	/**
366
	 * Assign allowed categories
367
	 *
368
	 * @param array|string $categories
369
	 * @return $this
370
	 */
371
	public function setAllowedCategories($categories) {
372
		if(is_string($categories)) {
373
			$categories = preg_split('/\s*,\s*/', $categories);
374
		}
375
		$this->allowedCategories = (array)$categories;
376
		return $this;
377
	}
378
379
	/**
380
	 * Gets the list of extensions (if limited) for this field. Empty list
381
	 * means there is no restriction on allowed types.
382
	 *
383
	 * @return array
384
	 */
385
	protected function getAllowedExtensions() {
386
		$categories = $this->getAllowedCategories();
387
		return File::get_category_extensions($categories);
388
	}
389
390
	/**
391
	 * Validate that this DBFile accepts this filename as valid
392
	 *
393
	 * @param string $filename
394
	 * @throws ValidationException
395
	 * @return bool
396
	 */
397
	protected function isValidFilename($filename) {
398
		$extension = strtolower(File::get_file_extension($filename));
399
400
		// Validate true if within the list of allowed extensions
401
		$allowed = $this->getAllowedExtensions();
402
		if($allowed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
403
			return in_array($extension, $allowed);
404
		}
405
		
406
		// If no extensions are configured, fallback to global list
407
		$globalList = File::config()->allowed_extensions;
0 ignored issues
show
Documentation introduced by
The property allowed_extensions does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
408
		if(in_array($extension, $globalList)) {
409
			return true;
410
		}
411
412
		// Only admins can bypass global rules
413
		return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
414
	}
415
416
	/**
417
	 * Check filename, and raise a ValidationException if invalid
418
	 *
419
	 * @param string $filename
420
	 * @throws ValidationException
421
	 */
422
	protected function assertFilenameValid($filename) {
423
		$result = new ValidationResult();
424
		$this->validate($result, $filename);
425
		if(!$result->valid()) {
426
			throw new ValidationException($result);
427
		}
428
	}
429
430
431
	/**
432
	 * Hook to validate this record against a validation result
433
	 *
434
	 * @param ValidationResult $result
435
	 * @param string $filename Optional filename to validate. If omitted, the current value is validated.
436
	 * @return bool Valid flag
437
	 */
438
	public function validate(ValidationResult $result, $filename = null) {
439
		if(empty($filename)) {
440
			$filename = $this->getFilename();
441
		}
442
		if(empty($filename) || $this->isValidFilename($filename)) {
443
			return true;
444
		}
445
446
		// Check allowed extensions
447
		$extensions = $this->getAllowedExtensions();
448
		if(empty($extensions)) {
449
			$extensions = File::config()->allowed_extensions;
0 ignored issues
show
Documentation introduced by
The property allowed_extensions does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
450
		}
451
		sort($extensions);
452
		$message = _t(
453
			'File.INVALIDEXTENSION',
454
			'Extension is not allowed (valid: {extensions})',
455
			'Argument 1: Comma-separated list of valid extensions',
456
			array('extensions' => wordwrap(implode(', ',$extensions)))
0 ignored issues
show
Documentation introduced by
array('extensions' => wo...de(', ', $extensions))) is of type array<string,string,{"extensions":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
457
		);
458
		$result->error($message);
459
		return false;
460
	}
461
462
	public function setField($field, $value, $markChanged = true) {
463
		// Catch filename validation on direct assignment
464
		if($field === 'Filename' && $value) {
465
			$this->assertFilenameValid($value);
466
		}
467
		
468
		return parent::setField($field, $value, $markChanged);
469
	}
470
471
472
	/**
473
	 * Returns the size of the file type in an appropriate format.
474
	 *
475
	 * @return string|false String value, or false if doesn't exist
476
	 */
477
	public function getSize() {
478
		$size = $this->getAbsoluteSize();
479
		if($size) {
480
			return \File::format_size($size);
481
		}
482
		return false;
483
	}
484
}
485