Completed
Push — master ( 03a324...633eb0 )
by Damian
21s
created

DBFile   D

Complexity

Total Complexity 83

Size/Duplication

Total Lines 509
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 83
c 1
b 0
f 0
lcom 1
cbo 10
dl 0
loc 509
rs 4.8717

40 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getIsImage() 0 5 2
A getStore() 0 3 1
A scaffoldFormField() 0 3 1
A forTemplate() 0 3 2
A getTag() 0 7 2
A getFrontendTemplate() 0 15 3
A getBasename() 0 6 2
A getExtension() 0 6 2
A getTitle() 0 8 3
A setFromLocalFile() 0 11 3
A setFromStream() 0 11 2
A setFromString() 0 11 2
A getStream() 0 8 2
A getString() 0 8 2
A getURL() 0 9 2
A getSourceURL() 0 5 1
A getAbsoluteURL() 0 6 2
A getMetaData() 0 8 2
A getMimeType() 0 8 2
A getValue() 0 10 2
A getVisibility() 0 8 2
A exists() 0 8 2
A getFilename() 0 3 1
A getHash() 0 3 1
A getVariant() 0 3 1
A getAbsoluteSize() 0 7 2
A setOriginal() 0 4 1
A getAllowedCategories() 0 3 1
A setAllowedCategories() 0 7 2
A getAllowedExtensions() 0 4 1
A isValidFilename() 0 18 4
A assertFilenameValid() 0 7 2
B validate() 0 23 5
A setField() 0 8 3
A getSize() 0 7 2
A deleteFile() 0 9 2
A publishFile() 0 7 2
A protectFile() 0 7 2
A grantFile() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like DBFile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DBFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Filesystem\Storage;
4
5
use SilverStripe\Filesystem\ImageManipulation;
6
use SilverStripe\Filesystem\Storage\AssetContainer;
7
use SilverStripe\Filesystem\Storage\AssetStore;
8
use SilverStripe\Model\FieldType\DBComposite;
9
10
use Injector;
11
use AssetField;
12
use File;
13
use Director;
14
use Permission;
15
use ShortcodeHandler;
16
use ValidationResult;
17
use ValidationException;
18
19
/**
20
 * Represents a file reference stored in a database
21
 *
22
 * @property string $Hash SHA of the file
23
 * @property string $Filename Name of the file, including directory
24
 * @property string $Variant Variant of the file
25
 *
26
 * @package framework
27
 * @subpackage filesystem
28
 */
29
class DBFile extends DBComposite implements AssetContainer {
30
31
	use ImageManipulation;
32
33
	/**
34
	 * List of allowed file categories.
35
	 *
36
	 * {@see File::$app_categories}
37
	 *
38
	 * @var array
39
	 */
40
	protected $allowedCategories = array();
41
42
	/**
43
	 * List of image mime types supported by the image manipulations API
44
	 *
45
	 * {@see File::app_categories} for matching extensions.
46
	 *
47
	 * @config
48
	 * @var array
49
	 */
50
	private static $supported_images = array(
51
		'image/jpeg',
52
		'image/gif',
53
		'image/png'
54
	);
55
56
	/**
57
	 * Create a new image manipulation
58
	 *
59
	 * @param string $name
60
	 * @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
61
	 */
62
	public function __construct($name = null, $allowed = array()) {
63
		parent::__construct($name);
64
		$this->setAllowedCategories($allowed);
65
	}
66
67
	/**
68
	 * Determine if a valid non-empty image exists behind this asset, which is a format
69
	 * compatible with image manipulations
70
	 *
71
	 * @return boolean
72
	 */
73
	public function getIsImage() {
74
		// Check file type
75
		$mime = $this->getMimeType();
76
		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...
77
	}
78
79
	/**
80
	 * @return AssetStore
81
	 */
82
	protected function getStore() {
83
		return Injector::inst()->get('AssetStore');
84
	}
85
86
	private static $composite_db = array(
87
		"Hash" => "Varchar(255)", // SHA of the base content
88
		"Filename" => "Varchar(255)", // Path identifier of the base content
89
		"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
90
	);
91
92
	private static $casting = array(
93
		'URL' => 'Varchar',
94
		'AbsoluteURL' => 'Varchar',
95
		'Basename' => 'Varchar',
96
		'Title' => 'Varchar',
97
		'MimeType' => 'Varchar',
98
		'String' => 'Text',
99
		'Tag' => 'HTMLText',
100
		'Size' => 'Varchar'
101
	);
102
103
	public function scaffoldFormField($title = null, $params = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
104
		return AssetField::create($this->getName(), $title);
105
	}
106
107
	/**
108
	 * Return a html5 tag of the appropriate for this file (normally img or a)
109
	 *
110
	 * @return string
111
	 */
112
	public function forTemplate() {
113
		return $this->getTag() ?: '';
114
	}
115
116
	/**
117
	 * Return a html5 tag of the appropriate for this file (normally img or a)
118
	 *
119
	 * @return string
120
	 */
121
	public function getTag() {
122
		$template = $this->getFrontendTemplate();
123
		if(empty($template)) {
124
			return '';
125
		}
126
		return (string)$this->renderWith($template);
127
	}
128
129
	/**
130
	 * Determine the template to render as on the frontend
131
	 *
132
	 * @return string Name of template
133
	 */
134
	public function getFrontendTemplate() {
135
		// Check that path is available
136
		$url = $this->getURL();
137
		if(empty($url)) {
138
			return null;
139
		}
140
141
		// Image template for supported images
142
		if($this->getIsImage()) {
143
			return 'DBFile_image';
144
		}
145
146
		// Default download
147
		return 'DBFile_download';
148
	}
149
150
	/**
151
	 * Get trailing part of filename
152
	 *
153
	 * @return string
154
	 */
155
	public function getBasename() {
156
		if(!$this->exists()) {
157
			return null;
158
		}
159
		return basename($this->getSourceURL());
160
	}
161
162
	/**
163
	 * Get file extension
164
	 *
165
	 * @return string
166
	 */
167
	public function getExtension() {
168
		if(!$this->exists()) {
169
			return null;
170
		}
171
		return pathinfo($this->Filename, PATHINFO_EXTENSION);
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
	public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array()) {
189
		$this->assertFilenameValid($filename ?: $path);
190
		$result = $this
191
			->getStore()
192
			->setFromLocalFile($path, $filename, $hash, $variant, $config);
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
	public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array()) {
201
		$this->assertFilenameValid($filename);
202
		$result = $this
203
			->getStore()
204
			->setFromStream($stream, $filename, $hash, $variant, $config);
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
	public function setFromString($data, $filename, $hash = null, $variant = null, $config = array()) {
213
		$this->assertFilenameValid($filename);
214
		$result = $this
215
			->getStore()
216
			->setFromString($data, $filename, $hash, $variant, $config);
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
	public function getStream() {
225
		if(!$this->exists()) {
226
			return null;
227
		}
228
		return $this
229
			->getStore()
230
			->getAsStream($this->Filename, $this->Hash, $this->Variant);
231
	}
232
233
	public function getString() {
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($grant = true) {
243
		if(!$this->exists()) {
244
			return null;
245
		}
246
		$url = $this->getSourceURL($grant);
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
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
257
	 * @return string
258
	 */
259
	public function getSourceURL($grant = true) {
260
		return $this
261
			->getStore()
262
			->getAsURL($this->Filename, $this->Hash, $this->Variant, $grant);
263
	}
264
265
	/**
266
	 * Get the absolute URL to this resource
267
	 *
268
	 * @return string
269
	 */
270
	public function getAbsoluteURL() {
271
		if(!$this->exists()) {
272
			return null;
273
		}
274
		return Director::absoluteURL($this->getURL());
275
	}
276
277
	public function getMetaData() {
278
		if(!$this->exists()) {
279
			return null;
280
		}
281
		return $this
282
			->getStore()
283
			->getMetadata($this->Filename, $this->Hash, $this->Variant);
284
	}
285
286
	public function getMimeType() {
287
		if(!$this->exists()) {
288
			return null;
289
		}
290
		return $this
291
			->getStore()
292
			->getMimeType($this->Filename, $this->Hash, $this->Variant);
293
	}
294
295
	public function getValue() {
296
		if(!$this->exists()) {
297
			return null;
298
		}
299
		return array(
300
			'Filename' => $this->Filename,
301
			'Hash' => $this->Hash,
302
			'Variant' => $this->Variant
303
		);
304
	}
305
306
	public function getVisibility() {
307
		if(empty($this->Filename)) {
308
			return null;
309
		}
310
		return $this
311
			->getStore()
312
			->getVisibility($this->Filename, $this->Hash);
313
	}
314
315
	public function exists() {
316
		if(empty($this->Filename)) {
317
			return false;
318
		}
319
		return $this
320
			->getStore()
321
			->exists($this->Filename, $this->Hash, $this->Variant);
322
	}
323
324
	public function getFilename() {
325
		return $this->getField('Filename');
326
	}
327
328
	public function getHash() {
329
		return $this->getField('Hash');
330
	}
331
332
	public function getVariant() {
333
		return $this->getField('Variant');
334
	}
335
336
	/**
337
	 * Return file size in bytes.
338
	 *
339
	 * @return int
340
	 */
341
	public function getAbsoluteSize() {
342
		$metadata = $this->getMetaData();
343
		if(isset($metadata['size'])) {
344
			return $metadata['size'];
345
		}
346
		return 0;
347
	}
348
349
	/**
350
	 * Customise this object with an "original" record for getting other customised fields
351
	 *
352
	 * @param AssetContainer $original
353
	 * @return $this
354
	 */
355
	public function setOriginal($original) {
356
		$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...
357
		return $this;
358
	}
359
360
	/**
361
	 * Get list of allowed file categories
362
	 *
363
	 * @return array
364
	 */
365
	public function getAllowedCategories() {
366
		return $this->allowedCategories;
367
	}
368
369
	/**
370
	 * Assign allowed categories
371
	 *
372
	 * @param array|string $categories
373
	 * @return $this
374
	 */
375
	public function setAllowedCategories($categories) {
376
		if(is_string($categories)) {
377
			$categories = preg_split('/\s*,\s*/', $categories);
378
		}
379
		$this->allowedCategories = (array)$categories;
380
		return $this;
381
	}
382
383
	/**
384
	 * Gets the list of extensions (if limited) for this field. Empty list
385
	 * means there is no restriction on allowed types.
386
	 *
387
	 * @return array
388
	 */
389
	protected function getAllowedExtensions() {
390
		$categories = $this->getAllowedCategories();
391
		return File::get_category_extensions($categories);
392
	}
393
394
	/**
395
	 * Validate that this DBFile accepts this filename as valid
396
	 *
397
	 * @param string $filename
398
	 * @throws ValidationException
399
	 * @return bool
400
	 */
401
	protected function isValidFilename($filename) {
402
		$extension = strtolower(File::get_file_extension($filename));
403
404
		// Validate true if within the list of allowed extensions
405
		$allowed = $this->getAllowedExtensions();
406
		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...
407
			return in_array($extension, $allowed);
408
		}
409
410
		// If no extensions are configured, fallback to global list
411
		$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...
412
		if(in_array($extension, $globalList)) {
413
			return true;
414
		}
415
416
		// Only admins can bypass global rules
417
		return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
418
	}
419
420
	/**
421
	 * Check filename, and raise a ValidationException if invalid
422
	 *
423
	 * @param string $filename
424
	 * @throws ValidationException
425
	 */
426
	protected function assertFilenameValid($filename) {
427
		$result = new ValidationResult();
428
		$this->validate($result, $filename);
429
		if(!$result->valid()) {
430
			throw new ValidationException($result);
431
		}
432
	}
433
434
435
	/**
436
	 * Hook to validate this record against a validation result
437
	 *
438
	 * @param ValidationResult $result
439
	 * @param string $filename Optional filename to validate. If omitted, the current value is validated.
440
	 * @return bool Valid flag
441
	 */
442
	public function validate(ValidationResult $result, $filename = null) {
443
		if(empty($filename)) {
444
			$filename = $this->getFilename();
445
		}
446
		if(empty($filename) || $this->isValidFilename($filename)) {
447
			return true;
448
		}
449
450
		// Check allowed extensions
451
		$extensions = $this->getAllowedExtensions();
452
		if(empty($extensions)) {
453
			$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...
454
		}
455
		sort($extensions);
456
		$message = _t(
457
			'File.INVALIDEXTENSION',
458
			'Extension is not allowed (valid: {extensions})',
459
			'Argument 1: Comma-separated list of valid extensions',
460
			array('extensions' => wordwrap(implode(', ',$extensions)))
461
		);
462
		$result->error($message);
463
		return false;
464
	}
465
466
	public function setField($field, $value, $markChanged = true) {
467
		// Catch filename validation on direct assignment
468
		if($field === 'Filename' && $value) {
469
			$this->assertFilenameValid($value);
470
		}
471
472
		return parent::setField($field, $value, $markChanged);
473
	}
474
475
476
	/**
477
	 * Returns the size of the file type in an appropriate format.
478
	 *
479
	 * @return string|false String value, or false if doesn't exist
480
	 */
481
	public function getSize() {
482
		$size = $this->getAbsoluteSize();
483
		if($size) {
484
			return \File::format_size($size);
485
		}
486
		return false;
487
	}
488
489
	public function deleteFile() {
490
		if(!$this->Filename) {
491
			return false;
492
		}
493
494
		return $this
495
			->getStore()
496
			->delete($this->Filename, $this->Hash);
497
	}
498
499
	public function publishFile() {
500
		if($this->Filename) {
501
			$this
502
				->getStore()
503
				->publish($this->Filename, $this->Hash);
504
		}
505
	}
506
507
	public function protectFile() {
508
		if($this->Filename) {
509
			$this
510
				->getStore()
511
				->protect($this->Filename, $this->Hash);
512
		}
513
	}
514
515
	public function grantFile() {
516
		if($this->Filename) {
517
			$this
518
				->getStore()
519
				->grant($this->Filename, $this->Hash);
520
		}
521
	}
522
523
	public function revokeFile() {
524
		if($this->Filename) {
525
			$this
526
				->getStore()
527
				->revoke($this->Filename, $this->Hash);
528
		}
529
	}
530
531
	public function canViewFile() {
532
		return $this->Filename
533
			&& $this
534
				->getStore()
535
				->canView($this->Filename, $this->Hash);
536
	}
537
}
538