Completed
Pull Request — master (#5157)
by Damian
11:28
created

File::validate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
3
use SilverStripe\Filesystem\ImageManipulation;
4
use SilverStripe\Filesystem\Storage\AssetContainer;
5
use SilverStripe\Filesystem\Storage\AssetStore;
6
7
/**
8
 * This class handles the representation of a file on the filesystem within the framework.
9
 * Most of the methods also handle the {@link Folder} subclass.
10
 *
11
 * Note: The files are stored in the assets/ directory, but SilverStripe
12
 * looks at the db object to gather information about a file such as URL
13
 * It then uses this for all processing functions (like image manipulation).
14
 *
15
 * <b>Security</b>
16
 *
17
 * Caution: It is recommended to disable any script execution in the"assets/"
18
 * directory in the webserver configuration, to reduce the risk of exploits.
19
 * See http://doc.silverstripe.org/secure-development#filesystem
20
 *
21
 * <b>Asset storage</b>
22
 *
23
 * As asset storage is configured separately to any File DataObject records, this class
24
 * does not make any assumptions about how these records are saved. They could be on
25
 * a local filesystem, remote filesystem, or a virtual record container (such as in local memory).
26
 *
27
 * The File dataobject simply represents an externally facing view of shared resources
28
 * within this asset store.
29
 *
30
 * Internally individual files are referenced by a"Filename" parameter, which represents a File, extension,
31
 * and is optionally prefixed by a list of custom directories. This path is root-agnostic, so it does not
32
 * automatically have a direct url mapping (even to the site's base directory).
33
 *
34
 * Additionally, individual files may have several versions distinguished by sha1 hash,
35
 * of which a File DataObject can point to a single one. Files can also be distinguished by
36
 * variants, which may be resized images or format-shifted documents.
37
 *
38
 * <b>Properties</b>
39
 *
40
 * -"Title": Optional title of the file (for display purposes only).
41
 *   Defaults to"Name". Note that the Title field of Folder (subclass of File)
42
 *   is linked to Name, so Name and Title will always be the same.
43
 * -"File": Physical asset backing this DB record. This is a composite DB field with
44
 *   its own list of properties. {@see DBFile} for more information
45
 * -"Content": Typically unused, but handy for a textual representation of
46
 *   files, e.g. for fulltext indexing of PDF documents.
47
 * -"ParentID": Points to a {@link Folder} record. Should be in sync with
48
 *  "Filename". A ParentID=0 value points to the"assets/" folder, not the webroot.
49
 * -"ShowInSearch": True if this file is searchable
50
 *
51
 * @package framework
52
 * @subpackage filesystem
53
 *
54
 * @property string $Name Basename of the file
55
 * @property string $Title Title of the file
56
 * @property DBFile $File asset stored behind this File record
57
 * @property string $Content
58
 * @property string $ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folders
59
 * @property int $ParentID ID of parent File/Folder
60
 * @property int $OwnerID ID of Member who owns the file
61
 *
62
 * @method File Parent() Returns parent File
63
 * @method Member Owner() Returns Member object of file owner.
64
 *
65
 * @mixin Hierarchy
66
 * @mixin Versioned
67
 */
68
class File extends DataObject implements ShortcodeHandler, AssetContainer {
69
70
	use ImageManipulation;
71
72
	private static $default_sort = "\"Name\"";
73
74
	private static $singular_name = "File";
75
76
	private static $plural_name = "Files";
77
78
	/**
79
	 * Permissions necessary to view files outside of the live stage (e.g. archive / draft stage).
80
	 *
81
	 * @config
82
	 * @var array
83
	 */
84
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_AssetAdmin', 'VIEW_DRAFT_CONTENT');
85
86
	private static $db = array(
87
		"Name" =>"Varchar(255)",
88
		"Title" =>"Varchar(255)",
89
		"File" =>"DBFile",
90
		// Only applies to files, doesn't inherit for folder
91
		'ShowInSearch' => 'Boolean(1)',
92
	);
93
94
	private static $has_one = array(
95
		"Parent" => "File",
96
		"Owner" => "Member"
97
	);
98
99
	private static $defaults = array(
100
		"ShowInSearch" => 1,
101
	);
102
103
	private static $extensions = array(
104
		"Hierarchy",
105
		"Versioned"
106
	);
107
108
	private static $casting = array(
109
		'TreeTitle' => 'HTMLText'
110
	);
111
112
	/**
113
	 * @config
114
	 * @var array List of allowed file extensions, enforced through {@link validate()}.
115
	 *
116
	 * Note: if you modify this, you should also change a configuration file in the assets directory.
117
	 * Otherwise, the files will be able to be uploaded but they won't be able to be served by the
118
	 * webserver.
119
	 *
120
	 *  - If you are running Apache you will need to change assets/.htaccess
121
	 *  - If you are running IIS you will need to change assets/web.config
122
	 *
123
	 * Instructions for the change you need to make are included in a comment in the config file.
124
	 */
125
	private static $allowed_extensions = array(
126
		'', 'ace', 'arc', 'arj', 'asf', 'au', 'avi', 'bmp', 'bz2', 'cab', 'cda', 'css', 'csv', 'dmg', 'doc',
127
		'docx', 'dotx', 'dotm', 'flv', 'gif', 'gpx', 'gz', 'hqx', 'ico', 'jar', 'jpeg', 'jpg', 'js', 'kml',
128
		'm4a', 'm4v', 'mid', 'midi', 'mkv', 'mov', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'ogg', 'ogv', 'pages',
129
		'pcx', 'pdf', 'png', 'pps', 'ppt', 'pptx', 'potx', 'potm', 'ra', 'ram', 'rm', 'rtf', 'sit', 'sitx',
130
		'tar', 'tgz', 'tif', 'tiff', 'txt', 'wav', 'webm', 'wma', 'wmv', 'xls', 'xlsx', 'xltx', 'xltm', 'zip',
131
		'zipx',
132
	);
133
134
	/**
135
	 * @config
136
	 * @var array Category identifiers mapped to commonly used extensions.
137
	 */
138
	private static $app_categories = array(
139
		'archive' => array(
140
			'ace', 'arc', 'arj', 'bz', 'bz2', 'cab', 'dmg', 'gz', 'hqx', 'jar', 'rar', 'sit', 'sitx', 'tar', 'tgz',
141
			'zip', 'zipx',
142
		),
143
		'audio' => array(
144
			'aif', 'aifc', 'aiff', 'apl', 'au', 'avr', 'cda', 'm4a', 'mid', 'midi', 'mp3', 'ogg', 'ra',
145
			'ram', 'rm', 'snd', 'wav', 'wma',
146
		),
147
		'document' => array(
148
			'css', 'csv', 'doc', 'docx', 'dotm', 'dotx', 'htm', 'html', 'gpx', 'js', 'kml', 'pages', 'pdf',
149
			'potm', 'potx', 'pps', 'ppt', 'pptx', 'rtf', 'txt', 'xhtml', 'xls', 'xlsx', 'xltm', 'xltx', 'xml',
150
		),
151
		'image' => array(
152
			'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff',
153
		),
154
		'image/supported' => array(
155
			'gif', 'jpeg', 'jpg', 'png'
156
		),
157
		'flash' => array(
158
			'fla', 'swf'
159
		),
160
		'video' => array(
161
			'asf', 'avi', 'flv', 'ifo', 'm1v', 'm2v', 'm4v', 'mkv', 'mov', 'mp2', 'mp4', 'mpa', 'mpe', 'mpeg',
162
			'mpg', 'ogv', 'qt', 'vob', 'webm', 'wmv',
163
		),
164
	);
165
166
	/**
167
	 * Map of file extensions to class type
168
	 *
169
	 * @config
170
	 * @var
171
	 */
172
	private static $class_for_file_extension = array(
173
		'*' => 'File',
174
		'jpg' => 'Image',
175
		'jpeg' => 'Image',
176
		'png' => 'Image',
177
		'gif' => 'Image',
178
	);
179
180
	/**
181
	 * @config
182
	 * @var If this is true, then restrictions set in {@link $allowed_max_file_size} and
183
	 * {@link $allowed_extensions} will be applied to users with admin privileges as
184
	 * well.
185
	 */
186
	private static $apply_restrictions_to_admin = true;
187
188
	/**
189
	 * If enabled, legacy file dataobjects will be automatically imported into the APL
190
	 *
191
	 * @config
192
	 * @var bool
193
	 */
194
	private static $migrate_legacy_file = false;
195
196
	/**
197
	 * @config
198
	 * @var boolean
199
	 */
200
	private static $update_filesystem = true;
201
202
	public static function get_shortcodes() {
203
		return 'file_link';
204
	}
205
206
	/**
207
	 * Replace"[file_link id=n]" shortcode with an anchor tag or link to the file.
208
	 *
209
	 * @param array $arguments Arguments passed to the parser
210
	 * @param string $content Raw shortcode
211
	 * @param ShortcodeParser $parser Parser
212
	 * @param string $shortcode Name of shortcode used to register this handler
213
	 * @param array $extra Extra arguments
214
	 * @return string Result of the handled shortcode
215
	 */
216
	public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
217
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) {
218
			return null;
219
		}
220
221
		/** @var File|SiteTree $record */
222
		$record = DataObject::get_by_id('File', $arguments['id']);
223
224
		// Check record for common errors
225
		$errorCode = null;
226
		if (!$record) {
227
			$errorCode = 404;
228
		} elseif(!$record->canView()) {
229
			$errorCode = 403;
230
		}
231
		if($errorCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errorCode of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
232
			$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
233
			$result = array_filter($result);
234
			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...
235
				$record = reset($result);
236
			}
237
		}
238
239
		if (!$record) {
240
			return null; // There were no suitable matches at all.
241
		}
242
243
		// build the HTML tag
244
		if($content) {
245
			// build some useful meta-data (file type and size) as data attributes
246
			$attrs = ' ';
247
			if($record instanceof File) {
248
				foreach(array(
249
					'class' => 'file',
250
					'data-type' => $record->getExtension(),
251
					'data-size' => $record->getSize()
252
				) as $name => $value) {
253
					$attrs .= sprintf('%s="%s" ', $name, $value);
254
				}
255
			}
256
257
			return sprintf('<a href="%s"%s>%s</a>', $record->Link(), rtrim($attrs), $parser->parse($content));
258
		} else {
259
			return $record->Link();
260
		}
261
	}
262
263
	/**
264
	 * A file only exists if the file_exists() and is in the DB as a record
265
	 *
266
	 * Use $file->isInDB() to only check for a DB record
267
	 * Use $file->File->exists() to only check if the asset exists
268
	 *
269
	 * @return bool
270
	 */
271
	public function exists() {
272
		return parent::exists() && $this->File->exists();
273
	}
274
275
	/**
276
	 * Find a File object by the given filename.
277
	 *
278
	 * @param string $filename Filename to search for, including any custom parent directories.
279
	 * @return File
280
	 */
281
	public static function find($filename) {
282
		// Split to folders and the actual filename, and traverse the structure.
283
		$parts = explode("/", $filename);
284
		$parentID = 0;
285
		$item = null;
286
		foreach($parts as $part) {
287
			$item = File::get()->filter(array(
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
288
				'Name' => $part,
289
				'ParentID' => $parentID
290
			))->first();
291
			if(!$item) break;
292
			$parentID = $item->ID;
293
		}
294
295
		return $item;
296
	}
297
298
	/**
299
	 * Just an alias function to keep a consistent API with SiteTree
300
	 *
301
	 * @return string The link to the file
302
	 */
303
	public function Link() {
304
		return $this->getURL();
305
	}
306
307
	/**
308
	 * @deprecated 4.0
309
	 */
310
	public function RelativeLink() {
311
		Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
312
		return Director::makeRelative($this->getURL());
313
	}
314
315
	/**
316
	 * Just an alias function to keep a consistent API with SiteTree
317
	 *
318
	 * @return string The absolute link to the file
319
	 */
320
	public function AbsoluteLink() {
321
		return $this->getAbsoluteURL();
322
	}
323
324
	/**
325
	 * @return string
326
	 */
327
	public function getTreeTitle() {
328
		return Convert::raw2xml($this->Title);
329
	}
330
331
	/**
332
	 * @param Member $member
333
	 * @return bool
334
	 */
335
	public function canView($member = null) {
336
		if(!$member) {
337
			$member = Member::currentUser();
338
		}
339
340
		$result = $this->extendedCan('canView', $member);
341
		if($result !== null) {
342
			return $result;
343
		}
344
345
		return true;
346
	}
347
348
	/**
349
	 * Check if this file can be modified
350
	 *
351
	 * @param Member $member
352
	 * @return boolean
353
	 */
354
	public function canEdit($member = null) {
355
		if(!$member) {
356
			$member = Member::currentUser();
357
		}
358
359
		$result = $this->extendedCan('canEdit', $member);
360
		if($result !== null) {
361
			return $result;
362
		}
363
364
		return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
365
	}
366
367
	/**
368
	 * Check if a file can be created
369
	 *
370
	 * @param Member $member
371
	 * @param array $context
372
	 * @return boolean
373
	 */
374
	public function canCreate($member = null, $context = array()) {
375
		if(!$member) {
376
			$member = Member::currentUser();
377
		}
378
379
		$result = $this->extendedCan('canCreate', $member, $context);
380
		if($result !== null) {
381
			return $result;
382
		}
383
384
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 376 can also be of type object<DataObject>; however, File::canEdit() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
385
	}
386
387
	/**
388
	 * Check if this file can be deleted
389
	 *
390
	 * @param Member $member
391
	 * @return boolean
392
	 */
393
	public function canDelete($member = null) {
394
		if(!$member) {
395
			$member = Member::currentUser();
396
		}
397
398
		$result = $this->extendedCan('canDelete', $member);
399
		if($result !== null) {
400
			return $result;
401
		}
402
403
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 395 can also be of type object<DataObject>; however, File::canEdit() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
404
	}
405
406
	/**
407
	 * Returns the fields to power the edit screen of files in the CMS.
408
	 * You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
409
	 * and implemeting updateCMSFields(FieldList $fields) on that extension.
410
	 *
411
	 * @return FieldList
412
	 */
413
	public function getCMSFields() {
414
		// Preview
415
		$filePreview = CompositeField::create(
416
			CompositeField::create(new LiteralField("ImageFull", $this->PreviewThumbnail()))
417
				->setName("FilePreviewImage")
418
				->addExtraClass('cms-file-info-preview'),
419
			CompositeField::create(
420
				CompositeField::create(
421
					new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'),
422
					new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()),
423
					ReadonlyField::create(
424
						'ClickableURL',
425
						_t('AssetTableField.URL','URL'),
426
						sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->Link())
427
					)
428
						->setDontEscape(true),
429
					new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
430
					new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
431
				)
432
			)
433
				->setName("FilePreviewData")
434
				->addExtraClass('cms-file-info-data')
435
		)
436
			->setName("FilePreview")
437
			->addExtraClass('cms-file-info');
438
439
		//get a tree listing with only folder, no files
440
		$fields = new FieldList(
441
			new TabSet('Root',
442
				new Tab('Main',
443
					$filePreview,
444
					new TextField("Title", _t('AssetTableField.TITLE','Title')),
445
					new TextField("Name", _t('AssetTableField.FILENAME','Filename')),
446
					DropdownField::create("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups())
447
						->setHasEmptyDefault(true),
448
					new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder')
449
				)
450
			)
451
		);
452
453
		$this->extend('updateCMSFields', $fields);
454
		return $fields;
455
	}
456
457
	/**
458
	 * Returns a category based on the file extension.
459
	 * This can be useful when grouping files by type,
460
	 * showing icons on filelinks, etc.
461
	 * Possible group values are:"audio","mov","zip","image".
462
	 *
463
	 * @param string $ext Extension to check
464
	 * @return string
465
	 */
466
	public static function get_app_category($ext) {
467
		$ext = strtolower($ext);
468
		foreach(Config::inst()->get('File', 'app_categories') as $category => $exts) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('File', 'app_categories') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
469
			if(in_array($ext, $exts)) return $category;
470
		}
471
		return false;
472
	}
473
474
	/**
475
	 * For a category or list of categories, get the list of file extensions
476
	 *
477
	 * @param array|string $categories List of categories, or single category
478
	 * @return array
479
	 */
480
	public static function get_category_extensions($categories) {
481
		if(empty($categories)) {
482
			return array();
483
		}
484
485
		// Fix arguments into a single array
486
		if(!is_array($categories)) {
487
			$categories = array($categories);
488
		} elseif(count($categories) === 1 && is_array(reset($categories))) {
489
			$categories = reset($categories);
490
		}
491
492
		// Check configured categories
493
		$appCategories = self::config()->app_categories;
0 ignored issues
show
Documentation introduced by
The property app_categories 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...
494
495
		// Merge all categories into list of extensions
496
		$extensions = array();
497
		foreach(array_filter($categories) as $category) {
498
			if(isset($appCategories[$category])) {
499
				$extensions = array_merge($extensions, $appCategories[$category]);
500
			} else {
501
				throw new InvalidArgumentException("Unknown file category: $category");
502
			}
503
		}
504
		$extensions = array_unique($extensions);
505
		sort($extensions);
506
		return $extensions;
507
	}
508
509
	/**
510
	 * Returns a category based on the file extension.
511
	 *
512
	 * @return string
513
	 */
514
	public function appCategory() {
515
		return self::get_app_category($this->getExtension());
516
	}
517
518
519
	/**
520
	 * Should be called after the file was uploaded
521
	 */
522
	public function onAfterUpload() {
523
		$this->extend('onAfterUpload');
524
	}
525
526
	/**
527
	 * Make sure the file has a name
528
	 */
529
	protected function onBeforeWrite() {
530
		// Set default owner
531
		if(!$this->isInDB() && !$this->OwnerID) {
532
			$this->OwnerID = Member::currentUserID();
533
		}
534
535
		// Set default name
536
		if(!$this->getField('Name')) {
537
			$this->Name ="new-" . strtolower($this->class);
538
		}
539
540
		// Propegate changes to the AssetStore and update the DBFile field
541
		$this->updateFilesystem();
542
543
		parent::onBeforeWrite();
544
	}
545
546
	/**
547
	 * This will check if the parent record and/or name do not match the name on the underlying
548
	 * DBFile record, and if so, copy this file to the new location, and update the record to
549
	 * point to this new file.
550
	 *
551
	 * This method will update the File {@see DBFile} field value on success, so it must be called
552
	 * before writing to the database
553
	 *
554
	 * @return bool True if changed
555
	 */
556
	public function updateFilesystem() {
557
		if(!$this->config()->update_filesystem) {
558
			return false;
559
		}
560
561
		// Check the file exists
562
		if(!$this->File->exists()) {
563
			return false;
564
		}
565
566
		// Avoid moving files on live; Rely on this being done on stage prior to publish.
567
		if(Versioned::get_stage() !== Versioned::DRAFT) {
568
			return false;
569
		}
570
571
		// Check path updated record will point to
572
		// If no changes necessary, skip
573
		$pathBefore = $this->File->getFilename();
574
		$pathAfter = $this->generateFilename();
575
		if($pathAfter === $pathBefore) {
576
			return false;
577
		}
578
579
		// Copy record to new location via stream
580
		$stream = $this->File->getStream();
581
		$this->File->setFromStream($stream, $pathAfter);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->File->getStream() on line 580 can also be of type null; however, DBFile::setFromStream() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
582
		return true;
583
	}
584
585
	/**
586
	 * Collate selected descendants of this page.
587
	 * $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
588
	 * to the $collator array.
589
	 *
590
	 * @param string $condition The PHP condition to be evaluated.  The page will be called $item
591
	 * @param array $collator An array, passed by reference, to collect all of the matching descendants.
592
	 * @return true|null
593
	 */
594
	public function collateDescendants($condition, &$collator) {
595
		if($children = $this->Children()) {
596
			foreach($children as $item) {
597
				if(!$condition || eval("return $condition;")) $collator[] = $item;
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
598
				$item->collateDescendants($condition, $collator);
599
			}
600
			return true;
601
		}
602
	}
603
604
	/**
605
	 * Setter function for Name. Automatically sets a default title,
606
	 * and removes characters that might be invalid on the filesystem.
607
	 * Also adds a suffix to the name if the filename already exists
608
	 * on the filesystem, and is associated to a different {@link File} database record
609
	 * in the same folder. This means"myfile.jpg" might become"myfile-1.jpg".
610
	 *
611
	 * Does not change the filesystem itself, please use {@link write()} for this.
612
	 *
613
	 * @param string $name
614
	 * @return $this
615
	 */
616
	public function setName($name) {
617
		$oldName = $this->Name;
618
619
		// It can't be blank, default to Title
620
		if(!$name) {
621
			$name = $this->Title;
622
		}
623
624
		// Fix illegal characters
625
		$filter = FileNameFilter::create();
626
		$name = $filter->filter($name);
627
628
		// We might have just turned it blank, so check again.
629
		if(!$name) {
630
			$name = 'new-folder';
631
		}
632
633
		// If it's changed, check for duplicates
634
		if($oldName && $oldName != $name) {
635
			$base = pathinfo($name, PATHINFO_FILENAME);
636
			$ext = self::get_file_extension($name);
637
			$suffix = 1;
638
639
			while(File::get()->filter(array(
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
640
					'Name' => $name,
641
					'ParentID' => (int) $this->ParentID
642
				))->exclude(array(
643
					'ID' => $this->ID
644
				))->first()
645
			) {
646
				$suffix++;
647
				$name ="$base-$suffix.$ext";
648
			}
649
		}
650
651
		// Update actual field value
652
		$this->setField('Name', $name);
653
654
		// Update title
655
		if(!$this->Title) {
656
			$this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name));
657
		}
658
659
		return $this;
660
	}
661
662
	/**
663
	 * Gets the URL of this file
664
	 *
665
	 * @return string
666
	 */
667
	public function getAbsoluteURL() {
668
		$url = $this->getURL();
669
		if($url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|null 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...
670
			return Director::absoluteURL($url);
671
		}
672
	}
673
674
	/**
675
	 * Gets the URL of this file
676
	 *
677
	 * @uses Director::baseURL()
678
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
679
	 * @return string
680
	 */
681
	public function getURL($grant = true) {
682
		if($this->File->exists()) {
683
			return $this->File->getURL($grant);
684
		}
685
	}
686
687
	/**
688
	 * Get URL, but without resampling.
689
	 *
690
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
691
	 * @return string
692
	 */
693
	public function getSourceURL($grant = true) {
694
		if($this->File->exists()) {
695
			return $this->File->getSourceURL($grant);
696
		}
697
	}
698
699
	/**
700
	 * @todo Coupling with cms module, remove this method.
701
	 *
702
	 * @return string
703
	 */
704
	public function DeleteLink() {
705
		return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID;
706
	}
707
708
	/**
709
	 * Get expected value of Filename tuple value. Will be used to trigger
710
	 * a file move on draft stage.
711
	 *
712
	 * @return string
713
	 */
714
	public function generateFilename() {
715
		// Check if this file is nested within a folder
716
		$parent = $this->Parent();
717
		if($parent && $parent->exists()) {
718
			return $this->join_paths($parent->getFilename(), $this->Name);
719
		}
720
		return $this->Name;
721
	}
722
723
	/**
724
	 * Ensure that parent folders are published before this one is published
725
	 *
726
	 * @todo Solve this via triggered publishing / ownership in the future
727
	 */
728
	public function onBeforePublish() {
729
		// Relies on Parent() returning the stage record
730
		$parent = $this->Parent();
731
		if($parent && $parent->exists()) {
732
			$parent->doPublish();
733
		}
734
	}
735
736
	/**
737
	 * Update the ParentID and Name for the given filename.
738
	 *
739
	 * On save, the underlying DBFile record will move the underlying file to this location.
740
	 * Thus it will not update the underlying Filename value until this is done.
741
	 *
742
	 * @param string $filename
743
	 * @return $this
744
	 */
745
	public function setFilename($filename) {
746
		// Check existing folder path
747
		$folder = '';
748
		$parent = $this->Parent();
749
		if($parent && $parent->exists()) {
750
			$folder = $parent->Filename;
0 ignored issues
show
Documentation introduced by
The property Filename does not exist on object<File>. 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...
751
		}
752
753
		// Detect change in foldername
754
		$newFolder = ltrim(dirname(trim($filename, '/')), '.');
755
		if($folder !== $newFolder) {
756
			if(!$newFolder) {
757
				$this->ParentID = 0;
758
			} else {
759
				$parent = Folder::find_or_make($newFolder);
760
				$this->ParentID = $parent->ID;
761
			}
762
		}
763
764
		// Update base name
765
		$this->Name = basename($filename);
766
		return $this;
767
	}
768
769
	/**
770
	 * Returns the file extension
771
	 *
772
	 * @return string
773
	 */
774
	public function getExtension() {
775
		return self::get_file_extension($this->Name);
776
	}
777
778
	/**
779
	 * Gets the extension of a filepath or filename,
780
	 * by stripping away everything before the last"dot".
781
	 * Caution: Only returns the last extension in"double-barrelled"
782
	 * extensions (e.g."gz" for"tar.gz").
783
	 *
784
	 * Examples:
785
	 * -"myfile" returns""
786
	 * -"myfile.txt" returns"txt"
787
	 * -"myfile.tar.gz" returns"gz"
788
	 *
789
	 * @param string $filename
790
	 * @return string
791
	 */
792
	public static function get_file_extension($filename) {
793
		return pathinfo($filename, PATHINFO_EXTENSION);
794
	}
795
796
	/**
797
	 * Given an extension, determine the icon that should be used
798
	 *
799
	 * @param string $extension
800
	 * @return string Icon filename relative to base url
801
	 */
802
	public static function get_icon_for_extension($extension) {
803
		$extension = strtolower($extension);
804
805
		// Check if exact extension has an icon
806
		if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
807
			$extension = static::get_app_category($extension);
808
809
			// Fallback to category specific icon
810
			if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
811
				$extension ="generic";
812
			}
813
		}
814
815
		return FRAMEWORK_DIR ."/images/app_icons/{$extension}_32.gif";
816
	}
817
818
	/**
819
	 * Return the type of file for the given extension
820
	 * on the current file name.
821
	 *
822
	 * @return string
823
	 */
824
	public function getFileType() {
825
		return self::get_file_type($this->getFilename());
826
	}
827
828
	/**
829
	 * Get descriptive type of file based on filename
830
	 *
831
	 * @param string $filename
832
	 * @return string Description of file
833
	 */
834
	public static function get_file_type($filename) {
835
		$types = array(
836
			'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
837
			'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
838
			'jpeg' => _t('File.JpgType', 'JPEG image - good for photos'),
839
			'png' => _t('File.PngType', 'PNG image - good general-purpose format'),
840
			'ico' => _t('File.IcoType', 'Icon image'),
841
			'tiff' => _t('File.TiffType', 'Tagged image format'),
842
			'doc' => _t('File.DocType', 'Word document'),
843
			'xls' => _t('File.XlsType', 'Excel spreadsheet'),
844
			'zip' => _t('File.ZipType', 'ZIP compressed file'),
845
			'gz' => _t('File.GzType', 'GZIP compressed file'),
846
			'dmg' => _t('File.DmgType', 'Apple disk image'),
847
			'pdf' => _t('File.PdfType', 'Adobe Acrobat PDF file'),
848
			'mp3' => _t('File.Mp3Type', 'MP3 audio file'),
849
			'wav' => _t('File.WavType', 'WAV audo file'),
850
			'avi' => _t('File.AviType', 'AVI video file'),
851
			'mpg' => _t('File.MpgType', 'MPEG video file'),
852
			'mpeg' => _t('File.MpgType', 'MPEG video file'),
853
			'js' => _t('File.JsType', 'Javascript file'),
854
			'css' => _t('File.CssType', 'CSS file'),
855
			'html' => _t('File.HtmlType', 'HTML file'),
856
			'htm' => _t('File.HtmlType', 'HTML file')
857
		);
858
859
		// Get extension
860
		$extension = strtolower(self::get_file_extension($filename));
861
		return isset($types[$extension]) ? $types[$extension] : 'unknown';
862
	}
863
864
	/**
865
	 * Returns the size of the file type in an appropriate format.
866
	 *
867
	 * @return string|false String value, or false if doesn't exist
868
	 */
869
	public function getSize() {
870
		$size = $this->getAbsoluteSize();
871
		if($size) {
872
			return static::format_size($size);
873
		}
874
		return false;
875
	}
876
877
	/**
878
	 * Formats a file size (eg: (int)42 becomes string '42 bytes')
879
	 *
880
	 * @todo unit tests
881
	 *
882
	 * @param int $size
883
	 * @return string
884
	 */
885
	public static function format_size($size) {
886
		if($size < 1024) {
887
			return $size . ' bytes';
888
		}
889
		if($size < 1024*10) {
890
			return (round($size/1024*10)/10). ' KB';
891
		}
892
		if($size < 1024*1024) {
893
			return round($size/1024) . ' KB';
894
		}
895
		if($size < 1024*1024*10) {
896
			return (round(($size/1024)/1024*10)/10) . ' MB';
897
		}
898
		if($size < 1024*1024*1024) {
899
			return round(($size/1024)/1024) . ' MB';
900
		}
901
		return round($size/(1024*1024*1024)*10)/10 . ' GB';
902
	}
903
904
	/**
905
	 * Convert a php.ini value (eg: 512M) to bytes
906
	 *
907
	 * @todo unit tests
908
	 *
909
	 * @param string $iniValue
910
	 * @return int
911
	 */
912
	public static function ini2bytes($iniValue) {
913
		switch(strtolower(substr(trim($iniValue), -1))) {
914
			case 'g':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
915
				$iniValue *= 1024;
916
			case 'm':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
917
				$iniValue *= 1024;
918
			case 'k':
919
				$iniValue *= 1024;
920
		}
921
		return $iniValue;
922
	}
923
924
	/**
925
	 * Return file size in bytes.
926
	 *
927
	 * @return int
928
	 */
929
	public function getAbsoluteSize(){
930
		return $this->File->getAbsoluteSize();
931
	}
932
933
	public function validate() {
934
		$result = new ValidationResult();
935
		$this->File->validate($result, $this->Name);
936
		$this->extend('validate', $result);
937
		return $result;
938
	}
939
940
	/**
941
	 * Maps a {@link File} subclass to a specific extension.
942
	 * By default, files with common image extensions will be created
943
	 * as {@link Image} instead of {@link File} when using
944
	 * {@link Folder::constructChild}, {@link Folder::addUploadToFolder}),
945
	 * and the {@link Upload} class (either directly or through {@link FileField}).
946
	 * For manually instanciated files please use this mapping getter.
947
	 *
948
	 * Caution: Changes to mapping doesn't apply to existing file records in the database.
949
	 * Also doesn't hook into {@link Object::getCustomClass()}.
950
	 *
951
	 * @param String File extension, without dot prefix. Use an asterisk ('*')
952
	 * to specify a generic fallback if no mapping is found for an extension.
953
	 * @return String Classname for a subclass of {@link File}
954
	 */
955
	public static function get_class_for_file_extension($ext) {
956
		$map = array_change_key_case(self::config()->class_for_file_extension, CASE_LOWER);
957
		return (array_key_exists(strtolower($ext), $map)) ? $map[strtolower($ext)] : $map['*'];
958
	}
959
960
	/**
961
	 * See {@link get_class_for_file_extension()}.
962
	 *
963
	 * @param String|array
964
	 * @param String
965
	 */
966
	public static function set_class_for_file_extension($exts, $class) {
967
		if(!is_array($exts)) $exts = array($exts);
968
		foreach($exts as $ext) {
969
			if(!is_subclass_of($class, 'File')) {
970
				throw new InvalidArgumentException(
971
					sprintf('Class"%s" (for extension"%s") is not a valid subclass of File', $class, $ext)
972
				);
973
			}
974
			self::config()->class_for_file_extension = array($ext => $class);
0 ignored issues
show
Documentation introduced by
The property class_for_file_extension 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...
975
		}
976
	}
977
978
	public function getMetaData() {
979
		if($this->File->exists()) {
980
			return $this->File->getMetaData();
981
		}
982
	}
983
984
	public function getMimeType() {
985
		if($this->File->exists()) {
986
			return $this->File->getMimeType();
987
		}
988
	}
989
990
	public function getStream() {
991
		if($this->File->exists()) {
992
			return $this->File->getStream();
993
		}
994
	}
995
996
	public function getString() {
997
		if($this->File->exists()) {
998
			return $this->File->getString();
999
		}
1000
	}
1001
1002
	public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array()) {
1003
		$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $config);
1004
1005
		// Update File record to name of the uploaded asset
1006
		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...
1007
			$this->setFilename($result['Filename']);
1008
		}
1009
		return $result;
1010
	}
1011
1012
	public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array()) {
1013
		$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $config);
1014
1015
		// Update File record to name of the uploaded asset
1016
		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...
1017
			$this->setFilename($result['Filename']);
1018
		}
1019
		return $result;
1020
	}
1021
1022
	public function setFromString($data, $filename, $hash = null, $variant = null, $config = array()) {
1023
		$result = $this->File->setFromString($data, $filename, $hash, $variant, $config);
1024
1025
		// Update File record to name of the uploaded asset
1026
		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...
1027
			$this->setFilename($result['Filename']);
1028
		}
1029
		return $result;
1030
	}
1031
1032
	public function getIsImage() {
1033
		return false;
1034
	}
1035
1036
	public function getFilename() {
1037
		return $this->File->Filename;
1038
	}
1039
1040
	public function getHash() {
1041
		return $this->File->Hash;
1042
	}
1043
1044
	public function getVariant() {
1045
		return $this->File->Variant;
1046
	}
1047
1048
	/**
1049
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1050
	 *
1051
	 * @return string
1052
	 */
1053
	public function forTemplate() {
1054
		return $this->getTag() ?: '';
1055
	}
1056
1057
	/**
1058
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1059
	 *
1060
	 * @return string
1061
	 */
1062
	public function getTag() {
1063
		$template = $this->File->getFrontendTemplate();
1064
		if(empty($template)) {
1065
			return '';
1066
		}
1067
		return (string)$this->renderWith($template);
1068
	}
1069
1070
	public function requireDefaultRecords() {
1071
		parent::requireDefaultRecords();
1072
1073
		// Check if old file records should be migrated
1074
		if(!$this->config()->migrate_legacy_file) {
1075
			return;
1076
		}
1077
1078
		$migrated = FileMigrationHelper::singleton()->run();
1079
		if($migrated) {
1080
			DB::alteration_message("{$migrated} File DataObjects upgraded","changed");
1081
		}
1082
	}
1083
1084
	/**
1085
	 * Joins one or more segments together to build a Filename identifier.
1086
	 *
1087
	 * Note that the result will not have a leading slash, and should not be used
1088
	 * with local file paths.
1089
	 *
1090
	 * @param string $part,... Parts
0 ignored issues
show
Bug introduced by
There is no parameter named $part,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1091
	 * @return string
1092
	 */
1093
	public static function join_paths() {
1094
		$args = func_get_args();
1095
		if(count($args) === 1 && is_array($args[0])) {
1096
			$args = $args[0];
1097
		}
1098
1099
		$parts = array();
1100
		foreach($args as $arg) {
1101
			$part = trim($arg, ' \\/');
1102
			if($part) {
1103
				$parts[] = $part;
1104
			}
1105
		}
1106
1107
		return implode('/', $parts);
1108
	}
1109
1110
	public function deleteFile() {
1111
		return $this->File->deleteFile();
1112
	}
1113
1114
	public function getVisibility() {
1115
		return $this->File->getVisibility();
1116
	}
1117
1118
	public function publishFile() {
1119
		$this->File->publishFile();
1120
	}
1121
1122
	public function protectFile() {
1123
		$this->File->protectFile();
1124
	}
1125
1126
	public function grantFile() {
1127
		$this->File->grantFile();
1128
	}
1129
1130
	public function revokeFile() {
1131
		$this->File->revokeFile();
1132
	}
1133
1134
	public function canViewFile() {
1135
		return $this->File->canViewFile();
1136
	}
1137
}
1138