Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

File::PreviewLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
use SilverStripe\Filesystem\Storage\DBFile;
4
use SilverStripe\Filesystem\Thumbnail;
5
use SilverStripe\Filesystem\ImageManipulation;
6
use SilverStripe\Filesystem\Storage\AssetContainer;
7
8
/**
9
 * This class handles the representation of a file on the filesystem within the framework.
10
 * Most of the methods also handle the {@link Folder} subclass.
11
 *
12
 * Note: The files are stored in the assets/ directory, but SilverStripe
13
 * looks at the db object to gather information about a file such as URL
14
 * It then uses this for all processing functions (like image manipulation).
15
 *
16
 * <b>Security</b>
17
 *
18
 * Caution: It is recommended to disable any script execution in the "assets/"
19
 * directory in the webserver configuration, to reduce the risk of exploits.
20
 * See http://doc.silverstripe.org/secure-development#filesystem
21
 *
22
 * <b>Asset storage</b>
23
 *
24
 * As asset storage is configured separately to any File DataObject records, this class
25
 * does not make any assumptions about how these records are saved. They could be on
26
 * a local filesystem, remote filesystem, or a virtual record container (such as in local memory).
27
 *
28
 * The File dataobject simply represents an externally facing view of shared resources
29
 * within this asset store.
30
 *
31
 * Internally individual files are referenced by a"Filename" parameter, which represents a File, extension,
32
 * and is optionally prefixed by a list of custom directories. This path is root-agnostic, so it does not
33
 * automatically have a direct url mapping (even to the site's base directory).
34
 *
35
 * Additionally, individual files may have several versions distinguished by sha1 hash,
36
 * of which a File DataObject can point to a single one. Files can also be distinguished by
37
 * variants, which may be resized images or format-shifted documents.
38
 *
39
 * <b>Properties</b>
40
 *
41
 * - "Title": Optional title of the file (for display purposes only).
42
 *   Defaults to "Name". Note that the Title field of Folder (subclass of File)
43
 *   is linked to Name, so Name and Title will always be the same.
44
 * -"File": Physical asset backing this DB record. This is a composite DB field with
45
 *   its own list of properties. {@see DBFile} for more information
46
 * - "Content": Typically unused, but handy for a textual representation of
47
 *   files, e.g. for fulltext indexing of PDF documents.
48
 * - "ParentID": Points to a {@link Folder} record. Should be in sync with
49
 *   "Filename". A ParentID=0 value points to the "assets/" folder, not the webroot.
50
 * -"ShowInSearch": True if this file is searchable
51
 *
52
 * @package framework
53
 * @subpackage filesystem
54
 *
55
 * @property string $Name Basename of the file
56
 * @property string $Title Title of the file
57
 * @property DBFile $File asset stored behind this File record
58
 * @property string $Content
59
 * @property string $ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folders
60
 * @property int $ParentID ID of parent File/Folder
61
 * @property int $OwnerID ID of Member who owns the file
62
 *
63
 * @method File Parent() Returns parent File
64
 * @method Member Owner() Returns Member object of file owner.
65
 *
66
 * @mixin Hierarchy
67
 * @mixin Versioned
68
 */
69
class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumbnail, CMSPreviewable {
70
71
	use ImageManipulation;
72
73
	private static $default_sort = "\"Name\"";
74
75
	private static $singular_name = "File";
76
77
	private static $plural_name = "Files";
78
79
	/**
80
	 * Permissions necessary to view files outside of the live stage (e.g. archive / draft stage).
81
	 *
82
	 * @config
83
	 * @var array
84
	 */
85
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_AssetAdmin', 'VIEW_DRAFT_CONTENT');
86
87
	private static $db = array(
88
		"Name" => "Varchar(255)",
89
		"Title" => "Varchar(255)",
90
		"File" =>"DBFile",
91
		// Only applies to files, doesn't inherit for folder
92
		'ShowInSearch' => 'Boolean(1)',
93
	);
94
95
	private static $has_one = array(
96
		"Parent" => "File",
97
		"Owner" => "Member"
98
	);
99
100
	private static $defaults = array(
101
		"ShowInSearch" => 1,
102
	);
103
104
	private static $extensions = array(
105
		"Hierarchy",
106
		"Versioned"
107
	);
108
109
	private static $casting = array (
110
		'TreeTitle' => 'HTMLText'
111
	);
112
113
	/**
114
	 * @config
115
	 * @var array List of allowed file extensions, enforced through {@link validate()}.
116
	 *
117
	 * Note: if you modify this, you should also change a configuration file in the assets directory.
118
	 * Otherwise, the files will be able to be uploaded but they won't be able to be served by the
119
	 * webserver.
120
	 *
121
	 *  - If you are running Apache you will need to change assets/.htaccess
122
	 *  - If you are running IIS you will need to change assets/web.config
123
	 *
124
	 * Instructions for the change you need to make are included in a comment in the config file.
125
	 */
126
	private static $allowed_extensions = array(
127
		'', 'ace', 'arc', 'arj', 'asf', 'au', 'avi', 'bmp', 'bz2', 'cab', 'cda', 'css', 'csv', 'dmg', 'doc',
128
		'docx', 'dotx', 'dotm', 'flv', 'gif', 'gpx', 'gz', 'hqx', 'ico', 'jar', 'jpeg', 'jpg', 'js', 'kml',
129
		'm4a', 'm4v', 'mid', 'midi', 'mkv', 'mov', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'ogg', 'ogv', 'pages',
130
		'pcx', 'pdf', 'png', 'pps', 'ppt', 'pptx', 'potx', 'potm', 'ra', 'ram', 'rm', 'rtf', 'sit', 'sitx',
131
		'tar', 'tgz', 'tif', 'tiff', 'txt', 'wav', 'webm', 'wma', 'wmv', 'xls', 'xlsx', 'xltx', 'xltm', 'zip',
132
		'zipx',
133
	);
134
135
	/**
136
	 * @config
137
	 * @var array Category identifiers mapped to commonly used extensions.
138
	 */
139
	private static $app_categories = array(
140
		'archive' => array(
141
			'ace', 'arc', 'arj', 'bz', 'bz2', 'cab', 'dmg', 'gz', 'hqx', 'jar', 'rar', 'sit', 'sitx', 'tar', 'tgz',
142
			'zip', 'zipx',
143
		),
144
		'audio' => array(
145
			'aif', 'aifc', 'aiff', 'apl', 'au', 'avr', 'cda', 'm4a', 'mid', 'midi', 'mp3', 'ogg', 'ra',
146
			'ram', 'rm', 'snd', 'wav', 'wma',
147
		),
148
		'document' => array(
149
			'css', 'csv', 'doc', 'docx', 'dotm', 'dotx', 'htm', 'html', 'gpx', 'js', 'kml', 'pages', 'pdf',
150
			'potm', 'potx', 'pps', 'ppt', 'pptx', 'rtf', 'txt', 'xhtml', 'xls', 'xlsx', 'xltm', 'xltx', 'xml',
151
		),
152
		'image' => array(
153
			'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff',
154
		),
155
		'image/supported' => array(
156
			'gif', 'jpeg', 'jpg', 'png'
157
		),
158
		'flash' => array(
159
			'fla', 'swf'
160
		),
161
		'video' => array(
162
			'asf', 'avi', 'flv', 'ifo', 'm1v', 'm2v', 'm4v', 'mkv', 'mov', 'mp2', 'mp4', 'mpa', 'mpe', 'mpeg',
163
			'mpg', 'ogv', 'qt', 'vob', 'webm', 'wmv',
164
		),
165
	);
166
167
	/**
168
	 * Map of file extensions to class type
169
	 *
170
	 * @config
171
	 * @var
172
	 */
173
	private static $class_for_file_extension = array(
174
		'*' => 'File',
175
		'jpg' => 'Image',
176
		'jpeg' => 'Image',
177
		'png' => 'Image',
178
		'gif' => 'Image',
179
	);
180
181
	/**
182
	 * @config
183
	 * @var If this is true, then restrictions set in {@link $allowed_max_file_size} and
184
	 * {@link $allowed_extensions} will be applied to users with admin privileges as
185
	 * well.
186
	 */
187
	private static $apply_restrictions_to_admin = true;
188
189
	/**
190
	 * If enabled, legacy file dataobjects will be automatically imported into the APL
191
	 *
192
	 * @config
193
	 * @var bool
194
	 */
195
	private static $migrate_legacy_file = false;
196
197
	/**
198
	 * @config
199
	 * @var boolean
200
	 */
201
	private static $update_filesystem = true;
202
203
	public static function get_shortcodes() {
204
		return 'file_link';
205
	}
206
207
	/**
208
	 * Replace "[file_link id=n]" shortcode with an anchor tag or link to the file.
209
	 *
210
	 * @param array $arguments Arguments passed to the parser
211
	 * @param string $content Raw shortcode
212
	 * @param ShortcodeParser $parser Parser
213
	 * @param string $shortcode Name of shortcode used to register this handler
214
	 * @param array $extra Extra arguments
215
	 * @return string Result of the handled shortcode
216
	 */
217
	public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
218
		// Find appropriate record, with fallback for error handlers
219
		$record = static::find_shortcode_record($arguments, $errorCode);
220
		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...
221
			$record = static::find_error_record($errorCode);
222
		}
223
		if (!$record) {
224
			return null; // There were no suitable matches at all.
225
		}
226
227
		// build the HTML tag
228
		if($content) {
229
			// build some useful meta-data (file type and size) as data attributes
230
			$attrs = ' ';
231
			if($record instanceof File) {
232
				foreach(array(
233
					'class' => 'file',
234
					'data-type' => $record->getExtension(),
235
					'data-size' => $record->getSize()
236
				) as $name => $value) {
237
					$attrs .= sprintf('%s="%s" ', $name, $value);
238
				}
239
			}
240
241
			return sprintf('<a href="%s"%s>%s</a>', $record->Link(), rtrim($attrs), $parser->parse($content));
242
		} else {
243
			return $record->Link();
244
		}
245
	}
246
247
	/**
248
	 * Find the record to use for a given shortcode.
249
	 *
250
	 * @param array $args Array of input shortcode arguments
251
	 * @param int $errorCode If the file is not found, or is inaccessible, this will be assigned to a HTTP error code.
252
	 * @return File|null The File DataObject, if it can be found.
253
	 */
254
	public static function find_shortcode_record($args, &$errorCode = null) {
255
		// Validate shortcode
256
		if(!isset($args['id']) || !is_numeric($args['id'])) {
257
			return null;
258
		}
259
260
		// Check if the file is found
261
		$file = File::get()->byID($args['id']);
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...
262
		if (!$file) {
263
			$errorCode = 404;
264
			return null;
265
		}
266
267
		// Check if the file is viewable
268
		if(!$file->canView()) {
269
			$errorCode = 403;
270
			return null;
271
		}
272
273
		// Success
274
		return $file;
275
	}
276
277
	/**
278
	 * Given a HTTP Error, find an appropriate substitute File or SiteTree data object instance.
279
	 *
280
	 * @param int $errorCode HTTP Error value
281
	 * @return File|SiteTree File or SiteTree object to use for the given error
282
	 */
283
	protected static function find_error_record($errorCode) {
284
		$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
285
		$result = array_filter($result);
286
		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...
287
			return reset($result);
288
		}
289
		return null;
290
	}
291
292
	/**
293
	 * A file only exists if the file_exists() and is in the DB as a record
294
	 *
295
	 * Use $file->isInDB() to only check for a DB record
296
	 * Use $file->File->exists() to only check if the asset exists
297
	 *
298
	 * @return bool
299
	 */
300
	public function exists() {
301
		return parent::exists() && $this->File->exists();
302
	}
303
304
	/**
305
	 * Find a File object by the given filename.
306
	 *
307
	 * @param string $filename Filename to search for, including any custom parent directories.
308
	 * @return File
309
	 */
310
	public static function find($filename) {
311
		// Split to folders and the actual filename, and traverse the structure.
312
		$parts = explode("/", $filename);
313
		$parentID = 0;
314
		$item = null;
315
		foreach($parts as $part) {
316
			$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...
317
				'Name' => $part,
318
				'ParentID' => $parentID
319
			))->first();
320
			if(!$item) break;
321
			$parentID = $item->ID;
322
		}
323
324
		return $item;
325
	}
326
327
	/**
328
	 * Just an alias function to keep a consistent API with SiteTree
329
	 *
330
	 * @return string The link to the file
331
	 */
332
	public function Link() {
333
		return $this->getURL();
334
	}
335
336
	/**
337
	 * @deprecated 4.0
338
	 */
339
	public function RelativeLink() {
340
		Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
341
		return Director::makeRelative($this->getURL());
342
	}
343
344
	/**
345
	 * Just an alias function to keep a consistent API with SiteTree
346
	 *
347
	 * @return string The absolute link to the file
348
	 */
349
	public function AbsoluteLink() {
350
		return $this->getAbsoluteURL();
351
	}
352
353
	/**
354
	 * @return string
355
	 */
356
	public function getTreeTitle() {
357
		return Convert::raw2xml($this->Title);
358
	}
359
360
	/**
361
	 * @param Member $member
362
	 * @return bool
363
	 */
364
	public function canView($member = null) {
365
		if(!$member) {
366
			$member = Member::currentUser();
367
		}
368
369
		$result = $this->extendedCan('canView', $member);
370
		if($result !== null) {
371
			return $result;
372
	}
373
374
		return true;
375
	}
376
377
	/**
378
	 * Check if this file can be modified
379
	 *
380
	 * @param Member $member
381
	 * @return boolean
382
	 */
383
	public function canEdit($member = null) {
384
		if(!$member) {
385
			$member = Member::currentUser();
386
		}
387
388
		$result = $this->extendedCan('canEdit', $member);
389
		if($result !== null) {
390
			return $result;
391
		}
392
393
		return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
394
	}
395
396
	/**
397
	 * Check if a file can be created
398
	 *
399
	 * @param Member $member
400
	 * @param array $context
401
	 * @return boolean
402
	 */
403
	public function canCreate($member = null, $context = array()) {
404
		if(!$member) {
405
			$member = Member::currentUser();
406
		}
407
408
		$result = $this->extendedCan('canCreate', $member, $context);
409
		if($result !== null) {
410
			return $result;
411
		}
412
413
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 405 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...
414
	}
415
416
	/**
417
	 * Check if this file can be deleted
418
	 *
419
	 * @param Member $member
420
	 * @return boolean
421
	 */
422
	public function canDelete($member = null) {
423
		if(!$member) {
424
			$member = Member::currentUser();
425
		}
426
427
		$result = $this->extendedCan('canDelete', $member);
428
		if($result !== null) {
429
			return $result;
430
		}
431
432
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 424 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...
433
	}
434
435
	/**
436
	 * Returns the fields to power the edit screen of files in the CMS.
437
	 * You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
438
	 * and implemeting updateCMSFields(FieldList $fields) on that extension.
439
	 *
440
	 * @return FieldList
441
	 */
442
	public function getCMSFields() {
443
		// Preview
444
		$filePreview = CompositeField::create(
445
			CompositeField::create(new LiteralField("ImageFull", $this->PreviewThumbnail()))
446
				->setName("FilePreviewImage")
447
				->addExtraClass('cms-file-info-preview'),
448
			CompositeField::create(
449
				CompositeField::create(
450
					new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'),
451
					new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()),
452
					ReadonlyField::create(
453
						'ClickableURL',
454
						_t('AssetTableField.URL','URL'),
455
						sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->Link())
456
					)
457
						->setDontEscape(true),
458
					new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
459
					new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
460
				)
461
			)
462
				->setName("FilePreviewData")
463
				->addExtraClass('cms-file-info-data')
464
		)
465
			->setName("FilePreview")
466
			->addExtraClass('cms-file-info');
467
468
		//get a tree listing with only folder, no files
469
		$fields = new FieldList(
470
			new TabSet('Root',
471
				new Tab('Main',
472
					$filePreview,
473
					new TextField("Title", _t('AssetTableField.TITLE','Title')),
474
					new TextField("Name", _t('AssetTableField.FILENAME','Filename')),
475
					DropdownField::create("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups())
476
						->setHasEmptyDefault(true),
477
					new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder')
478
				)
479
			)
480
		);
481
482
		$this->extend('updateCMSFields', $fields);
483
		return $fields;
484
	}
485
486
	/**
487
	 * Returns a category based on the file extension.
488
	 * This can be useful when grouping files by type,
489
	 * showing icons on filelinks, etc.
490
	 * Possible group values are: "audio", "mov", "zip", "image".
491
	 *
492
	 * @param string $ext Extension to check
493
	 * @return string
494
	 */
495
	public static function get_app_category($ext) {
496
		$ext = strtolower($ext);
497
		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...
498
			if(in_array($ext, $exts)) return $category;
499
		}
500
		return false;
501
	}
502
503
	/**
504
	 * For a category or list of categories, get the list of file extensions
505
	 *
506
	 * @param array|string $categories List of categories, or single category
507
	 * @return array
508
	 */
509
	public static function get_category_extensions($categories) {
510
		if(empty($categories)) {
511
			return array();
512
	}
513
514
		// Fix arguments into a single array
515
		if(!is_array($categories)) {
516
			$categories = array($categories);
517
		} elseif(count($categories) === 1 && is_array(reset($categories))) {
518
			$categories = reset($categories);
519
		}
520
521
		// Check configured categories
522
		$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...
523
524
		// Merge all categories into list of extensions
525
		$extensions = array();
526
		foreach(array_filter($categories) as $category) {
527
			if(isset($appCategories[$category])) {
528
				$extensions = array_merge($extensions, $appCategories[$category]);
529
			} else {
530
				throw new InvalidArgumentException("Unknown file category: $category");
531
			}
532
		}
533
		$extensions = array_unique($extensions);
534
		sort($extensions);
535
		return $extensions;
536
	}
537
538
	/**
539
	 * Returns a category based on the file extension.
540
	 *
541
	 * @return string
542
	 */
543
	public function appCategory() {
544
		return self::get_app_category($this->getExtension());
545
		}
546
547
548
	/**
549
	 * Should be called after the file was uploaded
550
	 */
551
	public function onAfterUpload() {
552
		$this->extend('onAfterUpload');
553
	}
554
555
	/**
556
	 * Make sure the file has a name
557
	 */
558
	protected function onBeforeWrite() {
559
		// Set default owner
560
		if(!$this->isInDB() && !$this->OwnerID) {
561
			$this->OwnerID = Member::currentUserID();
562
		}
563
564
		// Set default name
565
		if(!$this->getField('Name')) {
566
			$this->Name ="new-" . strtolower($this->class);
567
	}
568
569
		// Propegate changes to the AssetStore and update the DBFile field
570
		$this->updateFilesystem();
571
572
		parent::onBeforeWrite();
573
	}
574
575
	/**
576
	 * This will check if the parent record and/or name do not match the name on the underlying
577
	 * DBFile record, and if so, copy this file to the new location, and update the record to
578
	 * point to this new file.
579
	 *
580
	 * This method will update the File {@see DBFile} field value on success, so it must be called
581
	 * before writing to the database
582
	 *
583
	 * @return bool True if changed
584
	 */
585
	public function updateFilesystem() {
586
		if(!$this->config()->update_filesystem) {
587
			return false;
588
		}
589
590
		// Check the file exists
591
		if(!$this->File->exists()) {
592
			return false;
593
				}
594
595
		// Avoid moving files on live; Rely on this being done on stage prior to publish.
596
		if(Versioned::get_stage() !== Versioned::DRAFT) {
597
			return false;
598
			}
599
600
		// Check path updated record will point to
601
		// If no changes necessary, skip
602
		$pathBefore = $this->File->getFilename();
603
		$pathAfter = $this->generateFilename();
604
		if($pathAfter === $pathBefore) {
605
			return false;
606
		}
607
608
		// Copy record to new location via stream
609
		$stream = $this->File->getStream();
610
		$this->File->setFromStream($stream, $pathAfter);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->File->getStream() on line 609 can also be of type null; however, SilverStripe\Filesystem\...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...
611
		return true;
612
	}
613
614
	/**
615
	 * Collate selected descendants of this page.
616
	 * $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
617
	 * to the $collator array.
618
	 *
619
	 * @param string $condition The PHP condition to be evaluated.  The page will be called $item
620
	 * @param array $collator An array, passed by reference, to collect all of the matching descendants.
621
	 * @return true|null
622
	 */
623
	public function collateDescendants($condition, &$collator) {
624
		if($children = $this->Children()) {
625
			foreach($children as $item) {
626
				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...
627
				$item->collateDescendants($condition, $collator);
628
			}
629
			return true;
630
		}
631
	}
632
633
	/**
634
	 * Setter function for Name. Automatically sets a default title,
635
	 * and removes characters that might be invalid on the filesystem.
636
	 * Also adds a suffix to the name if the filename already exists
637
	 * on the filesystem, and is associated to a different {@link File} database record
638
	 * in the same folder. This means "myfile.jpg" might become "myfile-1.jpg".
639
	 *
640
	 * Does not change the filesystem itself, please use {@link write()} for this.
641
	 *
642
	 * @param string $name
643
	 * @return $this
644
	 */
645
	public function setName($name) {
646
		$oldName = $this->Name;
647
648
		// It can't be blank, default to Title
649
		if(!$name) {
650
			$name = $this->Title;
651
		}
652
653
		// Fix illegal characters
654
		$filter = FileNameFilter::create();
655
		$name = $filter->filter($name);
656
657
		// We might have just turned it blank, so check again.
658
		if(!$name) {
659
			$name = 'new-folder';
660
		}
661
662
		// If it's changed, check for duplicates
663
		if($oldName && $oldName != $name) {
664
			$base = pathinfo($name, PATHINFO_FILENAME);
665
			$ext = self::get_file_extension($name);
666
			$suffix = 1;
667
668
			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...
669
					'Name' => $name,
670
					'ParentID' => (int) $this->ParentID
671
				))->exclude(array(
672
					'ID' => $this->ID
673
				))->first()
674
			) {
675
				$suffix++;
676
				$name = "$base-$suffix.$ext";
677
			}
678
		}
679
680
		// Update actual field value
681
		$this->setField('Name', $name);
682
683
		// Update title
684
		if(!$this->Title) {
685
			$this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name));
686
		}
687
688
		return $this;
689
	}
690
691
	/**
692
	 * Gets the URL of this file
693
	 *
694
	 * @return string
695
	 */
696
	public function getAbsoluteURL() {
697
		$url = $this->getURL();
698
		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...
699
			return Director::absoluteURL($url);
700
		}
701
	}
702
703
	/**
704
	 * Gets the URL of this file
705
	 *
706
	 * @uses Director::baseURL()
707
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
708
	 * @return string
709
	 */
710
	public function getURL($grant = true) {
711
		if($this->File->exists()) {
712
			return $this->File->getURL($grant);
713
		}
714
	}
715
716
	/**
717
	 * Get URL, but without resampling.
718
	 *
719
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
720
	 * @return string
721
	 */
722
	public function getSourceURL($grant = true) {
723
		if($this->File->exists()) {
724
			return $this->File->getSourceURL($grant);
725
		}
726
	}
727
728
	/**
729
	 * @todo Coupling with cms module, remove this method.
730
	 *
731
	 * @return string
732
	 */
733
	public function DeleteLink() {
734
		return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID;
735
	}
736
737
	/**
738
	 * Get expected value of Filename tuple value. Will be used to trigger
739
	 * a file move on draft stage.
740
	 *
741
	 * @return string
742
	 */
743
	public function generateFilename() {
744
		// Check if this file is nested within a folder
745
		$parent = $this->Parent();
746
		if($parent && $parent->exists()) {
747
			return $this->join_paths($parent->getFilename(), $this->Name);
748
		}
749
		return $this->Name;
750
	}
751
752
	/**
753
	 * Ensure that parent folders are published before this one is published
754
	 *
755
	 * @todo Solve this via triggered publishing / ownership in the future
756
	 */
757
	public function onBeforePublish() {
758
		// Relies on Parent() returning the stage record
759
		$parent = $this->Parent();
760
		if($parent && $parent->exists()) {
761
			$parent->publishRecursive();
762
		}
763
	}
764
765
	/**
766
	 * Update the ParentID and Name for the given filename.
767
	 *
768
	 * On save, the underlying DBFile record will move the underlying file to this location.
769
	 * Thus it will not update the underlying Filename value until this is done.
770
	 *
771
	 * @param string $filename
772
	 * @return $this
773
	 */
774
	public function setFilename($filename) {
775
		// Check existing folder path
776
		$folder = '';
777
		$parent = $this->Parent();
778
		if($parent && $parent->exists()) {
779
			$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...
780
	}
781
782
		// Detect change in foldername
783
		$newFolder = ltrim(dirname(trim($filename, '/')), '.');
784
		if($folder !== $newFolder) {
785
			if(!$newFolder) {
786
				$this->ParentID = 0;
787
		} else {
788
				$parent = Folder::find_or_make($newFolder);
789
				$this->ParentID = $parent->ID;
790
		}
791
	}
792
793
		// Update base name
794
		$this->Name = basename($filename);
795
		return $this;
796
	}
797
798
	/**
799
	 * Returns the file extension
800
	 *
801
	 * @return string
802
	 */
803
	public function getExtension() {
804
		return self::get_file_extension($this->Name);
805
	}
806
807
	/**
808
	 * Gets the extension of a filepath or filename,
809
	 * by stripping away everything before the last "dot".
810
	 * Caution: Only returns the last extension in "double-barrelled"
811
	 * extensions (e.g. "gz" for "tar.gz").
812
	 *
813
	 * Examples:
814
	 * - "myfile" returns ""
815
	 * - "myfile.txt" returns "txt"
816
	 * - "myfile.tar.gz" returns "gz"
817
	 *
818
	 * @param string $filename
819
	 * @return string
820
	 */
821
	public static function get_file_extension($filename) {
822
		return pathinfo($filename, PATHINFO_EXTENSION);
823
	}
824
825
	/**
826
	 * Given an extension, determine the icon that should be used
827
	 *
828
	 * @param string $extension
829
	 * @return string Icon filename relative to base url
830
	 */
831
	public static function get_icon_for_extension($extension) {
832
		$extension = strtolower($extension);
833
834
		// Check if exact extension has an icon
835
		if(!file_exists(FRAMEWORK_PATH ."/client/dist/images/app_icons/{$extension}_32.png")) {
836
			$extension = static::get_app_category($extension);
837
838
			// Fallback to category specific icon
839
			if(!file_exists(FRAMEWORK_PATH ."/client/dist/images/app_icons/{$extension}_32.png")) {
840
				$extension ="generic";
841
			}
842
		}
843
844
		return FRAMEWORK_DIR ."/client/dist/images/app_icons/{$extension}_32.png";
845
	}
846
847
	/**
848
	 * Return the type of file for the given extension
849
	 * on the current file name.
850
	 *
851
	 * @return string
852
	 */
853
	public function getFileType() {
854
		return self::get_file_type($this->getFilename());
855
	}
856
857
	/**
858
	 * Get descriptive type of file based on filename
859
	 *
860
	 * @param string $filename
861
	 * @return string Description of file
862
	 */
863
	public static function get_file_type($filename) {
864
		$types = array(
865
			'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
866
			'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
867
			'jpeg' => _t('File.JpgType', 'JPEG image - good for photos'),
868
			'png' => _t('File.PngType', 'PNG image - good general-purpose format'),
869
			'ico' => _t('File.IcoType', 'Icon image'),
870
			'tiff' => _t('File.TiffType', 'Tagged image format'),
871
			'doc' => _t('File.DocType', 'Word document'),
872
			'xls' => _t('File.XlsType', 'Excel spreadsheet'),
873
			'zip' => _t('File.ZipType', 'ZIP compressed file'),
874
			'gz' => _t('File.GzType', 'GZIP compressed file'),
875
			'dmg' => _t('File.DmgType', 'Apple disk image'),
876
			'pdf' => _t('File.PdfType', 'Adobe Acrobat PDF file'),
877
			'mp3' => _t('File.Mp3Type', 'MP3 audio file'),
878
			'wav' => _t('File.WavType', 'WAV audo file'),
879
			'avi' => _t('File.AviType', 'AVI video file'),
880
			'mpg' => _t('File.MpgType', 'MPEG video file'),
881
			'mpeg' => _t('File.MpgType', 'MPEG video file'),
882
			'js' => _t('File.JsType', 'Javascript file'),
883
			'css' => _t('File.CssType', 'CSS file'),
884
			'html' => _t('File.HtmlType', 'HTML file'),
885
			'htm' => _t('File.HtmlType', 'HTML file')
886
		);
887
888
		// Get extension
889
		$extension = strtolower(self::get_file_extension($filename));
890
		return isset($types[$extension]) ? $types[$extension] : 'unknown';
891
	}
892
893
	/**
894
	 * Returns the size of the file type in an appropriate format.
895
	 *
896
	 * @return string|false String value, or false if doesn't exist
897
	 */
898
	public function getSize() {
899
		$size = $this->getAbsoluteSize();
900
		if($size) {
901
			return static::format_size($size);
902
	}
903
		return false;
904
	}
905
906
	/**
907
	 * Formats a file size (eg: (int)42 becomes string '42 bytes')
908
	 *
909
	 * @todo unit tests
910
	 *
911
	 * @param int $size
912
	 * @return string
913
	 */
914
	public static function format_size($size) {
915
		if($size < 1024) {
916
			return $size . ' bytes';
917
		}
918
		if($size < 1024*10) {
919
			return (round($size/1024*10)/10). ' KB';
920
		}
921
		if($size < 1024*1024) {
922
			return round($size/1024) . ' KB';
923
		}
924
		if($size < 1024*1024*10) {
925
			return (round(($size/1024)/1024*10)/10) . ' MB';
926
		}
927
		if($size < 1024*1024*1024) {
928
			return round(($size/1024)/1024) . ' MB';
929
		}
930
		return round($size/(1024*1024*1024)*10)/10 . ' GB';
931
	}
932
933
	/**
934
	 * Convert a php.ini value (eg: 512M) to bytes
935
	 *
936
	 * @todo unit tests
937
	 *
938
	 * @param string $iniValue
939
	 * @return int
940
	 */
941
	public static function ini2bytes($iniValue) {
942
		switch(strtolower(substr(trim($iniValue), -1))) {
943
			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...
944
				$iniValue *= 1024;
945
			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...
946
				$iniValue *= 1024;
947
			case 'k':
948
				$iniValue *= 1024;
949
		}
950
		return $iniValue;
951
	}
952
953
	/**
954
	 * Return file size in bytes.
955
	 *
956
	 * @return int
957
	 */
958
	public function getAbsoluteSize(){
959
		return $this->File->getAbsoluteSize();
960
		}
961
962
	public function validate() {
963
		$result = new ValidationResult();
964
		$this->File->validate($result, $this->Name);
965
		$this->extend('validate', $result);
966
		return $result;
967
	}
968
969
	/**
970
	 * Maps a {@link File} subclass to a specific extension.
971
	 * By default, files with common image extensions will be created
972
	 * as {@link Image} instead of {@link File} when using
973
	 * {@link Folder::constructChild}, {@link Folder::addUploadToFolder}),
974
	 * and the {@link Upload} class (either directly or through {@link FileField}).
975
	 * For manually instanciated files please use this mapping getter.
976
	 *
977
	 * Caution: Changes to mapping doesn't apply to existing file records in the database.
978
	 * Also doesn't hook into {@link Object::getCustomClass()}.
979
	 *
980
	 * @param String File extension, without dot prefix. Use an asterisk ('*')
981
	 * to specify a generic fallback if no mapping is found for an extension.
982
	 * @return String Classname for a subclass of {@link File}
983
	 */
984
	public static function get_class_for_file_extension($ext) {
985
		$map = array_change_key_case(self::config()->class_for_file_extension, CASE_LOWER);
986
		return (array_key_exists(strtolower($ext), $map)) ? $map[strtolower($ext)] : $map['*'];
987
	}
988
989
	/**
990
	 * See {@link get_class_for_file_extension()}.
991
	 *
992
	 * @param String|array
993
	 * @param String
994
	 */
995
	public static function set_class_for_file_extension($exts, $class) {
996
		if(!is_array($exts)) $exts = array($exts);
997
		foreach($exts as $ext) {
998
			if(!is_subclass_of($class, 'File')) {
999
				throw new InvalidArgumentException(
1000
					sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
1001
				);
1002
			}
1003
			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...
1004
		}
1005
	}
1006
1007
	public function getMetaData() {
1008
		if($this->File->exists()) {
1009
			return $this->File->getMetaData();
1010
		}
1011
	}
1012
1013
	public function getMimeType() {
1014
		if($this->File->exists()) {
1015
			return $this->File->getMimeType();
1016
		}
1017
	}
1018
1019
	public function getStream() {
1020
		if($this->File->exists()) {
1021
			return $this->File->getStream();
1022
		}
1023
	}
1024
1025
	public function getString() {
1026
		if($this->File->exists()) {
1027
			return $this->File->getString();
1028
		}
1029
	}
1030
1031
	public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array()) {
1032
		$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $config);
1033
1034
		// Update File record to name of the uploaded asset
1035
		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...
1036
			$this->setFilename($result['Filename']);
1037
		}
1038
		return $result;
1039
	}
1040
1041
	public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array()) {
1042
		$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $config);
1043
1044
		// Update File record to name of the uploaded asset
1045
		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...
1046
			$this->setFilename($result['Filename']);
1047
		}
1048
		return $result;
1049
	}
1050
1051
	public function setFromString($data, $filename, $hash = null, $variant = null, $config = array()) {
1052
		$result = $this->File->setFromString($data, $filename, $hash, $variant, $config);
1053
1054
		// Update File record to name of the uploaded asset
1055
		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...
1056
			$this->setFilename($result['Filename']);
1057
		}
1058
		return $result;
1059
	}
1060
1061
	public function getIsImage() {
1062
		return false;
1063
	}
1064
1065
	public function getFilename() {
1066
		return $this->File->Filename;
1067
	}
1068
1069
	public function getHash() {
1070
		return $this->File->Hash;
1071
	}
1072
1073
	public function getVariant() {
1074
		return $this->File->Variant;
1075
	}
1076
1077
	/**
1078
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1079
	 *
1080
	 * @return string
1081
	 */
1082
	public function forTemplate() {
1083
		return $this->getTag() ?: '';
1084
	}
1085
1086
	/**
1087
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1088
	 *
1089
	 * @return string
1090
	 */
1091
	public function getTag() {
1092
		$template = $this->File->getFrontendTemplate();
1093
		if(empty($template)) {
1094
			return '';
1095
		}
1096
		return (string)$this->renderWith($template);
1097
	}
1098
1099
	public function requireDefaultRecords() {
1100
		parent::requireDefaultRecords();
1101
1102
		// Check if old file records should be migrated
1103
		if(!$this->config()->migrate_legacy_file) {
1104
			return;
1105
		}
1106
1107
		$migrated = FileMigrationHelper::singleton()->run();
1108
		if($migrated) {
1109
			DB::alteration_message("{$migrated} File DataObjects upgraded","changed");
1110
		}
1111
	}
1112
1113
	/**
1114
	 * Joins one or more segments together to build a Filename identifier.
1115
	 *
1116
	 * Note that the result will not have a leading slash, and should not be used
1117
	 * with local file paths.
1118
	 *
1119
	 * @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...
1120
	 * @return string
1121
	 */
1122
	public static function join_paths() {
1123
		$args = func_get_args();
1124
		if(count($args) === 1 && is_array($args[0])) {
1125
			$args = $args[0];
1126
		}
1127
1128
		$parts = array();
1129
		foreach($args as $arg) {
1130
			$part = trim($arg, ' \\/');
1131
			if($part) {
1132
				$parts[] = $part;
1133
			}
1134
		}
1135
1136
		return implode('/', $parts);
1137
	}
1138
1139
	public function deleteFile() {
1140
		return $this->File->deleteFile();
1141
	}
1142
1143
	public function getVisibility() {
1144
		return $this->File->getVisibility();
1145
	}
1146
1147
	public function publishFile() {
1148
		$this->File->publishFile();
1149
	}
1150
1151
	public function protectFile() {
1152
		$this->File->protectFile();
1153
	}
1154
1155
	public function grantFile() {
1156
		$this->File->grantFile();
1157
	}
1158
1159
	public function revokeFile() {
1160
		$this->File->revokeFile();
1161
	}
1162
1163
	public function canViewFile() {
1164
		return $this->File->canViewFile();
1165
	}
1166
1167
	public function CMSEditLink() {
1168
		$link = null;
1169
		$this->extend('updateCMSEditLink', $link);
1170
		return $link;
1171
	}
1172
1173
	public function PreviewLink($action = null) {
1174
		// No preview for non-images by default
1175
		$link = null;
1176
		$this->extend('updatePreviewLink', $link, $action);
1177
		return $link;
1178
	}
1179
}
1180