Completed
Push — master ( 98eea6...214a1e )
by Damian
15s
created

File::handle_shortcode()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 29
rs 8.439
cc 6
eloc 18
nc 8
nop 5
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
		// Find appropriate record, with fallback for error handlers
218
		$record = static::find_shortcode_record($arguments, $errorCode);
219
		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...
220
			$record = static::find_error_record($errorCode);
221
		}
222
		if (!$record) {
223
			return null; // There were no suitable matches at all.
224
		}
225
226
		// build the HTML tag
227
		if($content) {
228
			// build some useful meta-data (file type and size) as data attributes
229
			$attrs = ' ';
230
			if($record instanceof File) {
231
				foreach(array(
232
					'class' => 'file',
233
					'data-type' => $record->getExtension(),
234
					'data-size' => $record->getSize()
235
				) as $name => $value) {
236
					$attrs .= sprintf('%s="%s" ', $name, $value);
237
				}
238
			}
239
240
			return sprintf('<a href="%s"%s>%s</a>', $record->Link(), rtrim($attrs), $parser->parse($content));
241
		} else {
242
			return $record->Link();
243
		}
244
	}
245
246
	/**
247
	 * Find the record to use for a given shortcode.
248
	 *
249
	 * @param array $args Array of input shortcode arguments
250
	 * @param int $errorCode If the file is not found, or is inaccessible, this will be assigned to a HTTP error code.
251
	 * @return File|null The File DataObject, if it can be found.
252
	 */
253
	public static function find_shortcode_record($args, &$errorCode = null) {
254
		// Validate shortcode
255
		if(!isset($args['id']) || !is_numeric($args['id'])) {
256
			return null;
257
		}
258
259
		// Check if the file is found
260
		$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...
261
		if (!$file) {
262
			$errorCode = 404;
263
			return null;
264
		}
265
266
		// Check if the file is viewable
267
		if(!$file->canView()) {
268
			$errorCode = 403;
269
			return null;
270
		}
271
272
		// Success
273
		return $file;
274
	}
275
276
	/**
277
	 * Given a HTTP Error, find an appropriate substitute File or SiteTree data object instance.
278
	 *
279
	 * @param int $errorCode HTTP Error value
280
	 * @return File|SiteTree File or SiteTree object to use for the given error
281
	 */
282
	protected static function find_error_record($errorCode) {
283
		$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
284
		$result = array_filter($result);
285
		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...
286
			return reset($result);
287
		}
288
		return null;
289
	}
290
291
	/**
292
	 * A file only exists if the file_exists() and is in the DB as a record
293
	 *
294
	 * Use $file->isInDB() to only check for a DB record
295
	 * Use $file->File->exists() to only check if the asset exists
296
	 *
297
	 * @return bool
298
	 */
299
	public function exists() {
300
		return parent::exists() && $this->File->exists();
301
	}
302
303
	/**
304
	 * Find a File object by the given filename.
305
	 *
306
	 * @param string $filename Filename to search for, including any custom parent directories.
307
	 * @return File
308
	 */
309
	public static function find($filename) {
310
		// Split to folders and the actual filename, and traverse the structure.
311
		$parts = explode("/", $filename);
312
		$parentID = 0;
313
		$item = null;
314
		foreach($parts as $part) {
315
			$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...
316
				'Name' => $part,
317
				'ParentID' => $parentID
318
			))->first();
319
			if(!$item) break;
320
			$parentID = $item->ID;
321
		}
322
323
		return $item;
324
	}
325
326
	/**
327
	 * Just an alias function to keep a consistent API with SiteTree
328
	 *
329
	 * @return string The link to the file
330
	 */
331
	public function Link() {
332
		return $this->getURL();
333
	}
334
335
	/**
336
	 * @deprecated 4.0
337
	 */
338
	public function RelativeLink() {
339
		Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
340
		return Director::makeRelative($this->getURL());
341
	}
342
343
	/**
344
	 * Just an alias function to keep a consistent API with SiteTree
345
	 *
346
	 * @return string The absolute link to the file
347
	 */
348
	public function AbsoluteLink() {
349
		return $this->getAbsoluteURL();
350
	}
351
352
	/**
353
	 * @return string
354
	 */
355
	public function getTreeTitle() {
356
		return Convert::raw2xml($this->Title);
357
	}
358
359
	/**
360
	 * @param Member $member
361
	 * @return bool
362
	 */
363
	public function canView($member = null) {
364
		if(!$member) {
365
			$member = Member::currentUser();
366
		}
367
368
		$result = $this->extendedCan('canView', $member);
369
		if($result !== null) {
370
			return $result;
371
		}
372
373
		return true;
374
	}
375
376
	/**
377
	 * Check if this file can be modified
378
	 *
379
	 * @param Member $member
380
	 * @return boolean
381
	 */
382
	public function canEdit($member = null) {
383
		if(!$member) {
384
			$member = Member::currentUser();
385
		}
386
387
		$result = $this->extendedCan('canEdit', $member);
388
		if($result !== null) {
389
			return $result;
390
		}
391
392
		return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
393
	}
394
395
	/**
396
	 * Check if a file can be created
397
	 *
398
	 * @param Member $member
399
	 * @param array $context
400
	 * @return boolean
401
	 */
402
	public function canCreate($member = null, $context = array()) {
403
		if(!$member) {
404
			$member = Member::currentUser();
405
		}
406
407
		$result = $this->extendedCan('canCreate', $member, $context);
408
		if($result !== null) {
409
			return $result;
410
		}
411
412
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 404 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...
413
	}
414
415
	/**
416
	 * Check if this file can be deleted
417
	 *
418
	 * @param Member $member
419
	 * @return boolean
420
	 */
421
	public function canDelete($member = null) {
422
		if(!$member) {
423
			$member = Member::currentUser();
424
		}
425
426
		$result = $this->extendedCan('canDelete', $member);
427
		if($result !== null) {
428
			return $result;
429
		}
430
431
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 423 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...
432
	}
433
434
	/**
435
	 * Returns the fields to power the edit screen of files in the CMS.
436
	 * You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
437
	 * and implemeting updateCMSFields(FieldList $fields) on that extension.
438
	 *
439
	 * @return FieldList
440
	 */
441
	public function getCMSFields() {
442
		// Preview
443
		$filePreview = CompositeField::create(
444
			CompositeField::create(new LiteralField("ImageFull", $this->PreviewThumbnail()))
445
				->setName("FilePreviewImage")
446
				->addExtraClass('cms-file-info-preview'),
447
			CompositeField::create(
448
				CompositeField::create(
449
					new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'),
450
					new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()),
451
					ReadonlyField::create(
452
						'ClickableURL',
453
						_t('AssetTableField.URL','URL'),
454
						sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->Link())
455
					)
456
						->setDontEscape(true),
457
					new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
458
					new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
459
				)
460
			)
461
				->setName("FilePreviewData")
462
				->addExtraClass('cms-file-info-data')
463
		)
464
			->setName("FilePreview")
465
			->addExtraClass('cms-file-info');
466
467
		//get a tree listing with only folder, no files
468
		$fields = new FieldList(
469
			new TabSet('Root',
470
				new Tab('Main',
471
					$filePreview,
472
					new TextField("Title", _t('AssetTableField.TITLE','Title')),
473
					new TextField("Name", _t('AssetTableField.FILENAME','Filename')),
474
					DropdownField::create("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups())
475
						->setHasEmptyDefault(true),
476
					new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder')
477
				)
478
			)
479
		);
480
481
		$this->extend('updateCMSFields', $fields);
482
		return $fields;
483
	}
484
485
	/**
486
	 * Returns a category based on the file extension.
487
	 * This can be useful when grouping files by type,
488
	 * showing icons on filelinks, etc.
489
	 * Possible group values are:"audio","mov","zip","image".
490
	 *
491
	 * @param string $ext Extension to check
492
	 * @return string
493
	 */
494
	public static function get_app_category($ext) {
495
		$ext = strtolower($ext);
496
		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...
497
			if(in_array($ext, $exts)) return $category;
498
		}
499
		return false;
500
	}
501
502
	/**
503
	 * For a category or list of categories, get the list of file extensions
504
	 *
505
	 * @param array|string $categories List of categories, or single category
506
	 * @return array
507
	 */
508
	public static function get_category_extensions($categories) {
509
		if(empty($categories)) {
510
			return array();
511
		}
512
513
		// Fix arguments into a single array
514
		if(!is_array($categories)) {
515
			$categories = array($categories);
516
		} elseif(count($categories) === 1 && is_array(reset($categories))) {
517
			$categories = reset($categories);
518
		}
519
520
		// Check configured categories
521
		$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...
522
523
		// Merge all categories into list of extensions
524
		$extensions = array();
525
		foreach(array_filter($categories) as $category) {
526
			if(isset($appCategories[$category])) {
527
				$extensions = array_merge($extensions, $appCategories[$category]);
528
			} else {
529
				throw new InvalidArgumentException("Unknown file category: $category");
530
			}
531
		}
532
		$extensions = array_unique($extensions);
533
		sort($extensions);
534
		return $extensions;
535
	}
536
537
	/**
538
	 * Returns a category based on the file extension.
539
	 *
540
	 * @return string
541
	 */
542
	public function appCategory() {
543
		return self::get_app_category($this->getExtension());
544
	}
545
546
547
	/**
548
	 * Should be called after the file was uploaded
549
	 */
550
	public function onAfterUpload() {
551
		$this->extend('onAfterUpload');
552
	}
553
554
	/**
555
	 * Make sure the file has a name
556
	 */
557
	protected function onBeforeWrite() {
558
		// Set default owner
559
		if(!$this->isInDB() && !$this->OwnerID) {
560
			$this->OwnerID = Member::currentUserID();
561
		}
562
563
		// Set default name
564
		if(!$this->getField('Name')) {
565
			$this->Name ="new-" . strtolower($this->class);
566
		}
567
568
		// Propegate changes to the AssetStore and update the DBFile field
569
		$this->updateFilesystem();
570
571
		parent::onBeforeWrite();
572
	}
573
574
	/**
575
	 * This will check if the parent record and/or name do not match the name on the underlying
576
	 * DBFile record, and if so, copy this file to the new location, and update the record to
577
	 * point to this new file.
578
	 *
579
	 * This method will update the File {@see DBFile} field value on success, so it must be called
580
	 * before writing to the database
581
	 *
582
	 * @return bool True if changed
583
	 */
584
	public function updateFilesystem() {
585
		if(!$this->config()->update_filesystem) {
586
			return false;
587
		}
588
589
		// Check the file exists
590
		if(!$this->File->exists()) {
591
			return false;
592
		}
593
594
		// Avoid moving files on live; Rely on this being done on stage prior to publish.
595
		if(Versioned::get_stage() !== Versioned::DRAFT) {
596
			return false;
597
		}
598
599
		// Check path updated record will point to
600
		// If no changes necessary, skip
601
		$pathBefore = $this->File->getFilename();
602
		$pathAfter = $this->generateFilename();
603
		if($pathAfter === $pathBefore) {
604
			return false;
605
		}
606
607
		// Copy record to new location via stream
608
		$stream = $this->File->getStream();
609
		$this->File->setFromStream($stream, $pathAfter);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->File->getStream() on line 608 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...
610
		return true;
611
	}
612
613
	/**
614
	 * Collate selected descendants of this page.
615
	 * $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
616
	 * to the $collator array.
617
	 *
618
	 * @param string $condition The PHP condition to be evaluated.  The page will be called $item
619
	 * @param array $collator An array, passed by reference, to collect all of the matching descendants.
620
	 * @return true|null
621
	 */
622
	public function collateDescendants($condition, &$collator) {
623
		if($children = $this->Children()) {
624
			foreach($children as $item) {
625
				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...
626
				$item->collateDescendants($condition, $collator);
627
			}
628
			return true;
629
		}
630
	}
631
632
	/**
633
	 * Setter function for Name. Automatically sets a default title,
634
	 * and removes characters that might be invalid on the filesystem.
635
	 * Also adds a suffix to the name if the filename already exists
636
	 * on the filesystem, and is associated to a different {@link File} database record
637
	 * in the same folder. This means"myfile.jpg" might become"myfile-1.jpg".
638
	 *
639
	 * Does not change the filesystem itself, please use {@link write()} for this.
640
	 *
641
	 * @param string $name
642
	 * @return $this
643
	 */
644
	public function setName($name) {
645
		$oldName = $this->Name;
646
647
		// It can't be blank, default to Title
648
		if(!$name) {
649
			$name = $this->Title;
650
		}
651
652
		// Fix illegal characters
653
		$filter = FileNameFilter::create();
654
		$name = $filter->filter($name);
655
656
		// We might have just turned it blank, so check again.
657
		if(!$name) {
658
			$name = 'new-folder';
659
		}
660
661
		// If it's changed, check for duplicates
662
		if($oldName && $oldName != $name) {
663
			$base = pathinfo($name, PATHINFO_FILENAME);
664
			$ext = self::get_file_extension($name);
665
			$suffix = 1;
666
667
			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...
668
					'Name' => $name,
669
					'ParentID' => (int) $this->ParentID
670
				))->exclude(array(
671
					'ID' => $this->ID
672
				))->first()
673
			) {
674
				$suffix++;
675
				$name ="$base-$suffix.$ext";
676
			}
677
		}
678
679
		// Update actual field value
680
		$this->setField('Name', $name);
681
682
		// Update title
683
		if(!$this->Title) {
684
			$this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name));
685
		}
686
687
		return $this;
688
	}
689
690
	/**
691
	 * Gets the URL of this file
692
	 *
693
	 * @return string
694
	 */
695
	public function getAbsoluteURL() {
696
		$url = $this->getURL();
697
		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...
698
			return Director::absoluteURL($url);
699
		}
700
	}
701
702
	/**
703
	 * Gets the URL of this file
704
	 *
705
	 * @uses Director::baseURL()
706
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
707
	 * @return string
708
	 */
709
	public function getURL($grant = true) {
710
		if($this->File->exists()) {
711
			return $this->File->getURL($grant);
712
		}
713
	}
714
715
	/**
716
	 * Get URL, but without resampling.
717
	 *
718
	 * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
719
	 * @return string
720
	 */
721
	public function getSourceURL($grant = true) {
722
		if($this->File->exists()) {
723
			return $this->File->getSourceURL($grant);
724
		}
725
	}
726
727
	/**
728
	 * @todo Coupling with cms module, remove this method.
729
	 *
730
	 * @return string
731
	 */
732
	public function DeleteLink() {
733
		return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID;
734
	}
735
736
	/**
737
	 * Get expected value of Filename tuple value. Will be used to trigger
738
	 * a file move on draft stage.
739
	 *
740
	 * @return string
741
	 */
742
	public function generateFilename() {
743
		// Check if this file is nested within a folder
744
		$parent = $this->Parent();
745
		if($parent && $parent->exists()) {
746
			return $this->join_paths($parent->getFilename(), $this->Name);
747
		}
748
		return $this->Name;
749
	}
750
751
	/**
752
	 * Ensure that parent folders are published before this one is published
753
	 *
754
	 * @todo Solve this via triggered publishing / ownership in the future
755
	 */
756
	public function onBeforePublish() {
757
		// Relies on Parent() returning the stage record
758
		$parent = $this->Parent();
759
		if($parent && $parent->exists()) {
760
			$parent->doPublish();
761
		}
762
	}
763
764
	/**
765
	 * Update the ParentID and Name for the given filename.
766
	 *
767
	 * On save, the underlying DBFile record will move the underlying file to this location.
768
	 * Thus it will not update the underlying Filename value until this is done.
769
	 *
770
	 * @param string $filename
771
	 * @return $this
772
	 */
773
	public function setFilename($filename) {
774
		// Check existing folder path
775
		$folder = '';
776
		$parent = $this->Parent();
777
		if($parent && $parent->exists()) {
778
			$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...
779
		}
780
781
		// Detect change in foldername
782
		$newFolder = ltrim(dirname(trim($filename, '/')), '.');
783
		if($folder !== $newFolder) {
784
			if(!$newFolder) {
785
				$this->ParentID = 0;
786
			} else {
787
				$parent = Folder::find_or_make($newFolder);
788
				$this->ParentID = $parent->ID;
789
			}
790
		}
791
792
		// Update base name
793
		$this->Name = basename($filename);
794
		return $this;
795
	}
796
797
	/**
798
	 * Returns the file extension
799
	 *
800
	 * @return string
801
	 */
802
	public function getExtension() {
803
		return self::get_file_extension($this->Name);
804
	}
805
806
	/**
807
	 * Gets the extension of a filepath or filename,
808
	 * by stripping away everything before the last"dot".
809
	 * Caution: Only returns the last extension in"double-barrelled"
810
	 * extensions (e.g."gz" for"tar.gz").
811
	 *
812
	 * Examples:
813
	 * -"myfile" returns""
814
	 * -"myfile.txt" returns"txt"
815
	 * -"myfile.tar.gz" returns"gz"
816
	 *
817
	 * @param string $filename
818
	 * @return string
819
	 */
820
	public static function get_file_extension($filename) {
821
		return pathinfo($filename, PATHINFO_EXTENSION);
822
	}
823
824
	/**
825
	 * Given an extension, determine the icon that should be used
826
	 *
827
	 * @param string $extension
828
	 * @return string Icon filename relative to base url
829
	 */
830
	public static function get_icon_for_extension($extension) {
831
		$extension = strtolower($extension);
832
833
		// Check if exact extension has an icon
834
		if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
835
			$extension = static::get_app_category($extension);
836
837
			// Fallback to category specific icon
838
			if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
839
				$extension ="generic";
840
			}
841
		}
842
843
		return FRAMEWORK_DIR ."/images/app_icons/{$extension}_32.gif";
844
	}
845
846
	/**
847
	 * Return the type of file for the given extension
848
	 * on the current file name.
849
	 *
850
	 * @return string
851
	 */
852
	public function getFileType() {
853
		return self::get_file_type($this->getFilename());
854
	}
855
856
	/**
857
	 * Get descriptive type of file based on filename
858
	 *
859
	 * @param string $filename
860
	 * @return string Description of file
861
	 */
862
	public static function get_file_type($filename) {
863
		$types = array(
864
			'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
865
			'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
866
			'jpeg' => _t('File.JpgType', 'JPEG image - good for photos'),
867
			'png' => _t('File.PngType', 'PNG image - good general-purpose format'),
868
			'ico' => _t('File.IcoType', 'Icon image'),
869
			'tiff' => _t('File.TiffType', 'Tagged image format'),
870
			'doc' => _t('File.DocType', 'Word document'),
871
			'xls' => _t('File.XlsType', 'Excel spreadsheet'),
872
			'zip' => _t('File.ZipType', 'ZIP compressed file'),
873
			'gz' => _t('File.GzType', 'GZIP compressed file'),
874
			'dmg' => _t('File.DmgType', 'Apple disk image'),
875
			'pdf' => _t('File.PdfType', 'Adobe Acrobat PDF file'),
876
			'mp3' => _t('File.Mp3Type', 'MP3 audio file'),
877
			'wav' => _t('File.WavType', 'WAV audo file'),
878
			'avi' => _t('File.AviType', 'AVI video file'),
879
			'mpg' => _t('File.MpgType', 'MPEG video file'),
880
			'mpeg' => _t('File.MpgType', 'MPEG video file'),
881
			'js' => _t('File.JsType', 'Javascript file'),
882
			'css' => _t('File.CssType', 'CSS file'),
883
			'html' => _t('File.HtmlType', 'HTML file'),
884
			'htm' => _t('File.HtmlType', 'HTML file')
885
		);
886
887
		// Get extension
888
		$extension = strtolower(self::get_file_extension($filename));
889
		return isset($types[$extension]) ? $types[$extension] : 'unknown';
890
	}
891
892
	/**
893
	 * Returns the size of the file type in an appropriate format.
894
	 *
895
	 * @return string|false String value, or false if doesn't exist
896
	 */
897
	public function getSize() {
898
		$size = $this->getAbsoluteSize();
899
		if($size) {
900
			return static::format_size($size);
901
		}
902
		return false;
903
	}
904
905
	/**
906
	 * Formats a file size (eg: (int)42 becomes string '42 bytes')
907
	 *
908
	 * @todo unit tests
909
	 *
910
	 * @param int $size
911
	 * @return string
912
	 */
913
	public static function format_size($size) {
914
		if($size < 1024) {
915
			return $size . ' bytes';
916
		}
917
		if($size < 1024*10) {
918
			return (round($size/1024*10)/10). ' KB';
919
		}
920
		if($size < 1024*1024) {
921
			return round($size/1024) . ' KB';
922
		}
923
		if($size < 1024*1024*10) {
924
			return (round(($size/1024)/1024*10)/10) . ' MB';
925
		}
926
		if($size < 1024*1024*1024) {
927
			return round(($size/1024)/1024) . ' MB';
928
		}
929
		return round($size/(1024*1024*1024)*10)/10 . ' GB';
930
	}
931
932
	/**
933
	 * Convert a php.ini value (eg: 512M) to bytes
934
	 *
935
	 * @todo unit tests
936
	 *
937
	 * @param string $iniValue
938
	 * @return int
939
	 */
940
	public static function ini2bytes($iniValue) {
941
		switch(strtolower(substr(trim($iniValue), -1))) {
942
			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...
943
				$iniValue *= 1024;
944
			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...
945
				$iniValue *= 1024;
946
			case 'k':
947
				$iniValue *= 1024;
948
		}
949
		return $iniValue;
950
	}
951
952
	/**
953
	 * Return file size in bytes.
954
	 *
955
	 * @return int
956
	 */
957
	public function getAbsoluteSize(){
958
		return $this->File->getAbsoluteSize();
959
	}
960
961
	public function validate() {
962
		$result = new ValidationResult();
963
		$this->File->validate($result, $this->Name);
964
		$this->extend('validate', $result);
965
		return $result;
966
	}
967
968
	/**
969
	 * Maps a {@link File} subclass to a specific extension.
970
	 * By default, files with common image extensions will be created
971
	 * as {@link Image} instead of {@link File} when using
972
	 * {@link Folder::constructChild}, {@link Folder::addUploadToFolder}),
973
	 * and the {@link Upload} class (either directly or through {@link FileField}).
974
	 * For manually instanciated files please use this mapping getter.
975
	 *
976
	 * Caution: Changes to mapping doesn't apply to existing file records in the database.
977
	 * Also doesn't hook into {@link Object::getCustomClass()}.
978
	 *
979
	 * @param String File extension, without dot prefix. Use an asterisk ('*')
980
	 * to specify a generic fallback if no mapping is found for an extension.
981
	 * @return String Classname for a subclass of {@link File}
982
	 */
983
	public static function get_class_for_file_extension($ext) {
984
		$map = array_change_key_case(self::config()->class_for_file_extension, CASE_LOWER);
985
		return (array_key_exists(strtolower($ext), $map)) ? $map[strtolower($ext)] : $map['*'];
986
	}
987
988
	/**
989
	 * See {@link get_class_for_file_extension()}.
990
	 *
991
	 * @param String|array
992
	 * @param String
993
	 */
994
	public static function set_class_for_file_extension($exts, $class) {
995
		if(!is_array($exts)) $exts = array($exts);
996
		foreach($exts as $ext) {
997
			if(!is_subclass_of($class, 'File')) {
998
				throw new InvalidArgumentException(
999
					sprintf('Class"%s" (for extension"%s") is not a valid subclass of File', $class, $ext)
1000
				);
1001
			}
1002
			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...
1003
		}
1004
	}
1005
1006
	public function getMetaData() {
1007
		if($this->File->exists()) {
1008
			return $this->File->getMetaData();
1009
		}
1010
	}
1011
1012
	public function getMimeType() {
1013
		if($this->File->exists()) {
1014
			return $this->File->getMimeType();
1015
		}
1016
	}
1017
1018
	public function getStream() {
1019
		if($this->File->exists()) {
1020
			return $this->File->getStream();
1021
		}
1022
	}
1023
1024
	public function getString() {
1025
		if($this->File->exists()) {
1026
			return $this->File->getString();
1027
		}
1028
	}
1029
1030
	public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array()) {
1031
		$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $config);
1032
1033
		// Update File record to name of the uploaded asset
1034
		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...
1035
			$this->setFilename($result['Filename']);
1036
		}
1037
		return $result;
1038
	}
1039
1040
	public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array()) {
1041
		$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $config);
1042
1043
		// Update File record to name of the uploaded asset
1044
		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...
1045
			$this->setFilename($result['Filename']);
1046
		}
1047
		return $result;
1048
	}
1049
1050
	public function setFromString($data, $filename, $hash = null, $variant = null, $config = array()) {
1051
		$result = $this->File->setFromString($data, $filename, $hash, $variant, $config);
1052
1053
		// Update File record to name of the uploaded asset
1054
		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...
1055
			$this->setFilename($result['Filename']);
1056
		}
1057
		return $result;
1058
	}
1059
1060
	public function getIsImage() {
1061
		return false;
1062
	}
1063
1064
	public function getFilename() {
1065
		return $this->File->Filename;
1066
	}
1067
1068
	public function getHash() {
1069
		return $this->File->Hash;
1070
	}
1071
1072
	public function getVariant() {
1073
		return $this->File->Variant;
1074
	}
1075
1076
	/**
1077
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1078
	 *
1079
	 * @return string
1080
	 */
1081
	public function forTemplate() {
1082
		return $this->getTag() ?: '';
1083
	}
1084
1085
	/**
1086
	 * Return a html5 tag of the appropriate for this file (normally img or a)
1087
	 *
1088
	 * @return string
1089
	 */
1090
	public function getTag() {
1091
		$template = $this->File->getFrontendTemplate();
1092
		if(empty($template)) {
1093
			return '';
1094
		}
1095
		return (string)$this->renderWith($template);
1096
	}
1097
1098
	public function requireDefaultRecords() {
1099
		parent::requireDefaultRecords();
1100
1101
		// Check if old file records should be migrated
1102
		if(!$this->config()->migrate_legacy_file) {
1103
			return;
1104
		}
1105
1106
		$migrated = FileMigrationHelper::singleton()->run();
1107
		if($migrated) {
1108
			DB::alteration_message("{$migrated} File DataObjects upgraded","changed");
1109
		}
1110
	}
1111
1112
	/**
1113
	 * Joins one or more segments together to build a Filename identifier.
1114
	 *
1115
	 * Note that the result will not have a leading slash, and should not be used
1116
	 * with local file paths.
1117
	 *
1118
	 * @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...
1119
	 * @return string
1120
	 */
1121
	public static function join_paths() {
1122
		$args = func_get_args();
1123
		if(count($args) === 1 && is_array($args[0])) {
1124
			$args = $args[0];
1125
		}
1126
1127
		$parts = array();
1128
		foreach($args as $arg) {
1129
			$part = trim($arg, ' \\/');
1130
			if($part) {
1131
				$parts[] = $part;
1132
			}
1133
		}
1134
1135
		return implode('/', $parts);
1136
	}
1137
1138
	public function deleteFile() {
1139
		return $this->File->deleteFile();
1140
	}
1141
1142
	public function getVisibility() {
1143
		return $this->File->getVisibility();
1144
	}
1145
1146
	public function publishFile() {
1147
		$this->File->publishFile();
1148
	}
1149
1150
	public function protectFile() {
1151
		$this->File->protectFile();
1152
	}
1153
1154
	public function grantFile() {
1155
		$this->File->grantFile();
1156
	}
1157
1158
	public function revokeFile() {
1159
		$this->File->revokeFile();
1160
	}
1161
1162
	public function canViewFile() {
1163
		return $this->File->canViewFile();
1164
	}
1165
}
1166