Completed
Push — master ( 2fdc96...4f1f24 )
by Damian
12:09
created

File::getSize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

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