Completed
Pull Request — master (#1415)
by Damian
02:27
created

SiteTree::getStatusFlags()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 34
rs 6.7272
cc 7
eloc 23
nc 6
nop 1
1
<?php
2
/**
3
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
4
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
5
 * draft and published states.
6
 *
7
 * <h2>URLs</h2>
8
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
9
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
10
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
11
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
12
 * {@link URLSegmentFilter::$default_allow_multibyte}.
13
 *
14
 * @property string URLSegment
15
 * @property string Title
16
 * @property string MenuTitle
17
 * @property string Content HTML content of the page.
18
 * @property string MetaDescription
19
 * @property string ExtraMeta
20
 * @property string ShowInMenus
21
 * @property string ShowInSearch
22
 * @property string Sort Integer value denoting the sort order.
23
 * @property string ReportClass
24
 * @property string CanViewType Type of restriction for viewing this object.
25
 * @property string CanEditType Type of restriction for editing this object.
26
 *
27
 * @method ManyManyList ViewerGroups List of groups that can view this object.
28
 * @method ManyManyList EditorGroups List of groups that can edit this object.
29
 *
30
 * @mixin Hierarchy
31
 * @mixin Versioned
32
 * @mixin SiteTreeLinkTracking
33
 *
34
 * @package cms
35
 */
36
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
37
38
	/**
39
	 * Indicates what kind of children this page type can have.
40
	 * This can be an array of allowed child classes, or the string "none" -
41
	 * indicating that this page type can't have children.
42
	 * If a classname is prefixed by "*", such as "*Page", then only that
43
	 * class is allowed - no subclasses. Otherwise, the class and all its
44
	 * subclasses are allowed.
45
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
46
	 *
47
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
48
	 *
49
	 * @config
50
	 * @var array
51
	 */
52
	private static $allowed_children = array("SiteTree");
53
54
	/**
55
	 * The default child class for this page.
56
	 * Note: Value might be cached, see {@link $allowed_chilren}.
57
	 *
58
	 * @config
59
	 * @var string
60
	 */
61
	private static $default_child = "Page";
62
63
	/**
64
	 * Default value for SiteTree.ClassName enum
65
	 * {@see DBClassName::getDefault}
66
	 *
67
	 * @config
68
	 * @var string
69
	 */
70
	private static $default_classname = "Page";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
71
72
	/**
73
	 * The default parent class for this page.
74
	 * Note: Value might be cached, see {@link $allowed_chilren}.
75
	 *
76
	 * @config
77
	 * @var string
78
	 */
79
	private static $default_parent = null;
80
81
	/**
82
	 * Controls whether a page can be in the root of the site tree.
83
	 * Note: Value might be cached, see {@link $allowed_chilren}.
84
	 *
85
	 * @config
86
	 * @var bool
87
	 */
88
	private static $can_be_root = true;
89
90
	/**
91
	 * List of permission codes a user can have to allow a user to create a page of this type.
92
	 * Note: Value might be cached, see {@link $allowed_chilren}.
93
	 *
94
	 * @config
95
	 * @var array
96
	 */
97
	private static $need_permission = null;
98
99
	/**
100
	 * If you extend a class, and don't want to be able to select the old class
101
	 * in the cms, set this to the old class name. Eg, if you extended Product
102
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
103
	 *
104
	 * @config
105
	 * @var string
106
	 */
107
	private static $hide_ancestor = null;
108
109
	private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
110
		"URLSegment" => "Varchar(255)",
111
		"Title" => "Varchar(255)",
112
		"MenuTitle" => "Varchar(100)",
113
		"Content" => "HTMLText",
114
		"MetaDescription" => "Text",
115
		"ExtraMeta" => "HTMLText('meta, link')",
116
		"ShowInMenus" => "Boolean",
117
		"ShowInSearch" => "Boolean",
118
		"Sort" => "Int",
119
		"HasBrokenFile" => "Boolean",
120
		"HasBrokenLink" => "Boolean",
121
		"ReportClass" => "Varchar",
122
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
123
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
124
	);
125
126
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
127
		"URLSegment" => true,
128
	);
129
130
	private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
131
		"ViewerGroups" => "Group",
132
		"EditorGroups" => "Group",
133
	);
134
135
	private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
136
		"VirtualPages" => "VirtualPage.CopyContentFrom"
137
	);
138
139
	private static $owned_by = array(
140
		"VirtualPages"
141
	);
142
143
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
144
		"Breadcrumbs" => "HTMLText",
145
		"LastEdited" => "SS_Datetime",
146
		"Created" => "SS_Datetime",
147
		'Link' => 'Text',
148
		'RelativeLink' => 'Text',
149
		'AbsoluteLink' => 'Text',
150
		'TreeTitle' => 'HTMLText',
151
	);
152
153
	private static $defaults = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
154
		"ShowInMenus" => 1,
155
		"ShowInSearch" => 1,
156
		"CanViewType" => "Inherit",
157
		"CanEditType" => "Inherit"
158
	);
159
160
	private static $versioning = array(
161
		"Stage",  "Live"
162
	);
163
164
	private static $default_sort = "\"Sort\"";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
165
166
	/**
167
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
168
	 * @var boolean
169
	 * @config
170
	 */
171
	private static $can_create = true;
172
173
	/**
174
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
175
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
176
	 *
177
	 * @see CMSMain::generateTreeStylingCSS()
178
	 * @config
179
	 * @var string
180
	 */
181
	private static $icon = null;
182
	
183
	/**
184
	 * @config
185
	 * @var string Description of the class functionality, typically shown to a user
186
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
187
	 */
188
	private static $description = 'Generic content page';
189
190
	private static $extensions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
191
		"Hierarchy",
192
		"Versioned('Stage', 'Live')",
193
		"SiteTreeLinkTracking"
194
	);
195
	
196
	private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
197
		'Title',
198
		'Content',
199
	);
200
201
	private static $field_labels = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
202
		'URLSegment' => 'URL'
203
	);
204
	
205
	/**
206
	 * @config
207
	 */
208
	private static $nested_urls = true;
209
	
210
	/**
211
	 * @config
212
	*/
213
	private static $create_default_pages = true;
214
	
215
	/**
216
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
217
	 */
218
	private static $runCMSFieldsExtensions = true;
219
	
220
	/**
221
	 * Cache for canView/Edit/Publish/Delete permissions.
222
	 * Keyed by permission type (e.g. 'edit'), with an array
223
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
224
	 * See {@link batch_permission_check()} for details.
225
	 */
226
	private static $cache_permissions = array();
227
228
	/**
229
	 * @config
230
	 * @var boolean
231
	 */
232
	private static $enforce_strict_hierarchy = true;
233
234
	/**
235
	 * The value used for the meta generator tag. Leave blank to omit the tag.
236
	 *
237
	 * @config
238
	 * @var string
239
	 */
240
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
241
242
	protected $_cache_statusFlags = null;
243
	
244
	/**
245
	 * Determines if the system should avoid orphaned pages
246
	 * by deleting all children when the their parent is deleted (TRUE),
247
	 * or rather preserve this data even if its not reachable through any navigation path (FALSE).
248
	 *
249
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
250
	 * @param boolean
251
	 */
252
	static public function set_enforce_strict_hierarchy($to) {
253
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
254
		Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', $to);
255
	}
256
	
257
	/**
258
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
259
	 * @return boolean
260
	 */
261
	static public function get_enforce_strict_hierarchy() {
262
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
263
		return Config::inst()->get('SiteTree', 'enforce_strict_hierarchy');
264
	}
265
266
	/**
267
	 * Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
268
	 *
269
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
270
	 * @return bool
271
	 */
272
	static public function nested_urls() {
273
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
274
		return Config::inst()->get('SiteTree', 'nested_urls');
275
	}
276
	
277
	/**
278
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
279
	 */
280
	static public function enable_nested_urls() {
281
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
282
		Config::inst()->update('SiteTree', 'nested_urls', true);
283
	}
284
	
285
	/**
286
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
287
	 */
288
	static public function disable_nested_urls() {
289
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
290
		Config::inst()->update('SiteTree', 'nested_urls', false);
291
	}
292
	
293
	/**
294
	 * Set the (re)creation of default pages on /dev/build
295
	 *
296
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
297
	 * @param bool $option
298
	 */
299
	static public function set_create_default_pages($option = true) {
300
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
301
		Config::inst()->update('SiteTree', 'create_default_pages', $option);
302
	}
303
304
	/**
305
	 * Return true if default pages should be created on /dev/build.
306
	 *
307
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
308
	 * @return bool
309
	 */
310
	static public function get_create_default_pages() {
311
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
312
		return Config::inst()->get('SiteTree', 'create_default_pages');
313
	}
314
	
315
	/**
316
	 * Fetches the {@link SiteTree} object that maps to a link.
317
	 *
318
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
319
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
320
	 *
321
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
322
	 * by a extension attached to {@link SiteTree}
323
	 *
324
	 * @param string $link  The link of the page to search for
325
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
326
	 * @return SiteTree
327
	 */
328
	static public function get_by_link($link, $cache = true) {
329
		if(trim($link, '/')) {
330
			$link = trim(Director::makeRelative($link), '/');
331
		} else {
332
			$link = RootURLController::get_homepage_link();
333
		}
334
		
335
		$parts = preg_split('|/+|', $link);
336
		
337
		// Grab the initial root level page to traverse down from.
338
		$URLSegment = array_shift($parts);
339
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
340
		if(self::config()->nested_urls) {
341
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
342
		}
343
		$sitetree = DataObject::get_one('SiteTree', $conditions, $cache);
344
		
345
		/// Fall back on a unique URLSegment for b/c.
346
		if(	!$sitetree
347
			&& self::config()->nested_urls
348
			&& $page = DataObject::get_one('SiteTree', array(
349
				'"SiteTree"."URLSegment"' => $URLSegment
350
			), $cache)
351
		) {
352
			return $page;
353
		}
354
		
355
		// Attempt to grab an alternative page from extensions.
356
		if(!$sitetree) {
357
			$parentID = self::config()->nested_urls ? 0 : null;
358
			
359 View Code Duplication
			if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $URLSegment, $parentID)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360
				foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
361
			}
362
			
363
			if(!$sitetree) return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SiteTree::get_by_link of type SiteTree.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
364
		}
365
		
366
		// Check if we have any more URL parts to parse.
367
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
368
		
369
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
370
		foreach($parts as $segment) {
371
			$next = DataObject::get_one('SiteTree', array(
372
					'"SiteTree"."URLSegment"' => $segment,
373
					'"SiteTree"."ParentID"' => $sitetree->ID
374
				),
375
				$cache
376
			);
377
			
378
			if(!$next) {
379
				$parentID = (int) $sitetree->ID;
380
				
381 View Code Duplication
				if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $segment, $parentID)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
382
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
383
				}
384
				
385
				if(!$next) return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SiteTree::get_by_link of type SiteTree.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
386
			}
387
			
388
			$sitetree->destroy();
389
			$sitetree = $next;
390
		}
391
		
392
		return $sitetree;
393
	}
394
	
395
	/**
396
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
397
	 *
398
	 * @return array
399
	 */
400
	public static function page_type_classes() {
401
		$classes = ClassInfo::getValidSubClasses();
402
403
		$baseClassIndex = array_search('SiteTree', $classes);
404
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
405
406
		$kill_ancestors = array();
407
408
		// figure out if there are any classes we don't want to appear
409
		foreach($classes as $class) {
410
			$instance = singleton($class);
411
412
			// do any of the progeny want to hide an ancestor?
413
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
414
				// note for killing later
415
				$kill_ancestors[] = $ancestor_to_hide;
416
			}
417
		}
418
419
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
420
		// requirements
421
		if($kill_ancestors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $kill_ancestors 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...
422
			$kill_ancestors = array_unique($kill_ancestors);
423
			foreach($kill_ancestors as $mark) {
424
				// unset from $classes
425
				$idx = array_search($mark, $classes, true);
426
				if ($idx !== false) {
427
					unset($classes[$idx]);
428
				}
429
			}
430
		}
431
432
		return $classes;
433
	}
434
	
435
	/**
436
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
437
	 *
438
	 * @param array      $arguments
439
	 * @param string     $content
440
	 * @param TextParser $parser
441
	 * @return string
442
	 */
443
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
444
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
445
		
446
		if (
447
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
448
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
449
		) {
450
			 return; // There were no suitable matches at all.
451
		}
452
453
		$link = Convert::raw2att($page->Link());
454
		
455
		if($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content 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...
456
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
0 ignored issues
show
Unused Code introduced by
The call to TextParser::parse() has too many arguments starting with $content.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Bug introduced by
It seems like $parser is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
457
		} else {
458
			return $link;
459
		}
460
	}
461
462
	/**
463
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
464
	 *
465
	 * @param string $action Optional controller action (method).
466
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
467
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
468
	 *                       append GET parameters.
469
	 * @return string
470
	 */
471
	public function Link($action = null) {
472
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
473
	}
474
	
475
	/**
476
	 * Get the absolute URL for this page, including protocol and host.
477
	 *
478
	 * @param string $action See {@link Link()}
479
	 * @return string
480
	 */
481
	public function AbsoluteLink($action = null) {
482
		if($this->hasMethod('alternateAbsoluteLink')) {
483
			return $this->alternateAbsoluteLink($action);
0 ignored issues
show
Bug introduced by
The method alternateAbsoluteLink() does not exist on SiteTree. Did you maybe mean AbsoluteLink()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
484
		} else {
485
			return Director::absoluteURL($this->Link($action));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($this->Link($action)); of type string|false adds false to the return on line 485 which is incompatible with the return type documented by SiteTree::AbsoluteLink of type string. It seems like you forgot to handle an error condition.
Loading history...
486
		}
487
	}
488
	
489
	/**
490
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
491
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
492
	 *
493
	 * @param string $action See {@link Link()}
494
	 * @return string
495
	 */
496
	public function PreviewLink($action = null) {
497
		if($this->hasMethod('alternatePreviewLink')) {
498
			return $this->alternatePreviewLink($action);
0 ignored issues
show
Bug introduced by
The method alternatePreviewLink() does not exist on SiteTree. Did you maybe mean PreviewLink()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
499
		} else {
500
			return $this->AbsoluteLink($action);
501
		}
502
	}
503
	
504
	/**
505
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
506
	 *
507
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
508
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
509
	 * and returned in its full form.
510
	 *
511
	 * @uses RootURLController::get_homepage_link()
512
	 *
513
	 * @param string $action See {@link Link()}
514
	 * @return string
515
	 */
516
	public function RelativeLink($action = null) {
517
		if($this->ParentID && self::config()->nested_urls) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
518
			$parent = $this->Parent();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
519
			// If page is removed select parent from version history (for archive page view)
520
			if((!$parent || !$parent->exists()) && $this->IsDeletedFromStage) {
0 ignored issues
show
Documentation introduced by
The property IsDeletedFromStage does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
521
				$parent = Versioned::get_latest_version('SiteTree', $this->ParentID);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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
			$base = $parent->RelativeLink($this->URLSegment);
524
		} elseif(!$action && $this->URLSegment == RootURLController::get_homepage_link()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type string|null is loosely compared to false; 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...
525
			// Unset base for root-level homepages.
526
			// Note: Homepages with action parameters (or $action === true)
527
			// need to retain their URLSegment.
528
			$base = null;
529
		} else {
530
			$base = $this->URLSegment;
531
		}
532
		
533
		$this->extend('updateRelativeLink', $base, $action);
534
		
535
		// Legacy support: If $action === true, retain URLSegment for homepages,
536
		// but don't append any action
537
		if($action === true) $action = null;
538
539
		return Controller::join_links($base, '/', $action);
540
	}
541
542
	/**
543
	 * Get the absolute URL for this page on the Live site.
544
	 *
545
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
546
	 * @return string
547
	 */
548
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
549
		$oldStage = Versioned::get_stage();
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
550
		Versioned::set_stage(Versioned::LIVE_STAGE);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
551
		$live = Versioned::get_one_by_stage('SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
552
			'"SiteTree"."ID"' => $this->ID
553
		));
554
		if($live) {
555
			$link = $live->AbsoluteLink();
556
			if($includeStageEqualsLive) $link .= '?stage=Live';
557
		} else {
558
			$link = null;
559
		}
560
561
		Versioned::set_stage($oldStage);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
562
		return $link;
563
	}
564
	
565
	/**
566
	 * Generates a link to edit this page in the CMS.
567
	 *
568
	 * @return string
569
	 */
570
	public function CMSEditLink() {
571
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
572
	}
573
	
574
		
575
	/**
576
	 * Return a CSS identifier generated from this page's link.
577
	 *
578
	 * @return string The URL segment
579
	 */
580
	public function ElementName() {
581
		return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
582
	}
583
	
584
	/**
585
	 * Returns true if this is the currently active page being used to handle this request.
586
	 *
587
	 * @return bool
588
	 */
589
	public function isCurrent() {
590
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
591
	}
592
	
593
	/**
594
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
595
	 * currently being viewed).
596
	 *
597
	 * @return bool
598
	 */
599
	public function isSection() {
600
		return $this->isCurrent() || (
601
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
0 ignored issues
show
Documentation Bug introduced by
The method getAncestors does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
602
		);
603
	}
604
	
605
	/**
606
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
607
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
608
	 * to external users.
609
	 *
610
	 * @return bool
611
	 */
612
	public function isOrphaned() {
613
		// Always false for root pages
614
		if(empty($this->ParentID)) return false;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
615
		
616
		// Parent must exist and not be an orphan itself
617
		$parent = $this->Parent();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
618
		return !$parent || !$parent->exists() || $parent->isOrphaned();
619
	}
620
	
621
	/**
622
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
623
	 *
624
	 * @return string
625
	 */
626
	public function LinkOrCurrent() {
627
		return $this->isCurrent() ? 'current' : 'link';
628
	}
629
	
630
	/**
631
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
632
	 *
633
	 * @return string
634
	 */
635
	public function LinkOrSection() {
636
		return $this->isSection() ? 'section' : 'link';
637
	}
638
	
639
	/**
640
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
641
	 * but in the current section.
642
	 *
643
	 * @return string
644
	 */
645
	public function LinkingMode() {
646
		if($this->isCurrent()) {
647
			return 'current';
648
		} elseif($this->isSection()) {
649
			return 'section';
650
		} else {
651
			return 'link';
652
		}
653
	}
654
	
655
	/**
656
	 * Check if this page is in the given current section.
657
	 *
658
	 * @param string $sectionName Name of the section to check
659
	 * @return bool True if we are in the given section
660
	 */
661
	public function InSection($sectionName) {
662
		$page = Director::get_current_page();
663
		while($page) {
664
			if($sectionName == $page->URLSegment)
665
				return true;
666
			$page = $page->Parent;
667
		}
668
		return false;
669
	}
670
671
	/**
672
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
673
	 * such behaviour.
674
	 *
675
	 * @param bool $doWrite Whether to write the new object before returning it
676
	 * @return self The duplicated object
677
	 */
678
	 public function duplicate($doWrite = true) {
679
		
680
		$page = parent::duplicate(false);
681
		$page->Sort = 0;
682
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
683
		
684
		if($doWrite) {
685
			$page->write();
686
687
			$page = $this->duplicateManyManyRelations($this, $page);
688
		}
689
		$this->invokeWithExtensions('onAfterDuplicate', $page);
690
		
691
		return $page;
692
	}
693
694
	/**
695
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
696
	 *
697
	 * @return self The duplicated object
698
	 */
699
	public function duplicateWithChildren() {
700
		$clone = $this->duplicate();
701
		$children = $this->AllChildren();
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
702
703
		if($children) {
704
			foreach($children as $child) {
705
				$childClone = method_exists($child, 'duplicateWithChildren')
706
					? $child->duplicateWithChildren()
707
					: $child->duplicate();
708
				$childClone->ParentID = $clone->ID;
709
				$childClone->write();
710
			}
711
		}
712
713
		return $clone;
714
	}
715
716
	/**
717
	 * Duplicate this node and its children as a child of the node with the given ID
718
	 *
719
	 * @param int $id ID of the new node's new parent
720
	 */
721
	public function duplicateAsChild($id) {
722
		$newSiteTree = $this->duplicate();
723
		$newSiteTree->ParentID = $id;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. 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...
724
		$newSiteTree->Sort = 0;
725
		$newSiteTree->write();
726
	}
727
	
728
	/**
729
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
730
	 *
731
	 * @param int $maxDepth The maximum depth to traverse.
732
	 * @param boolean $unlinked Whether to link page titles.
733
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
734
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
735
	 * @return HTMLText The breadcrumb trail.
736
	 */
737
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
738
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
739
		$template = new SSViewer('BreadcrumbsTemplate');
740
		return $template->process($this->customise(new ArrayData(array(
741
			"Pages" => $pages,
742
			"Unlinked" => $unlinked
743
		))));
744
	}
745
746
747
	/**
748
	 * Returns a list of breadcrumbs for the current page.
749
	 *
750
	 * @param int $maxDepth The maximum depth to traverse.
751
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
752
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
753
	 *
754
	 * @return ArrayList
755
	*/
756
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
757
		$page = $this;
758
		$pages = array();
759
		
760
		while(
761
			$page
762
 			&& (!$maxDepth || count($pages) < $maxDepth)
763
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
764
 		) {
765
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
766
				$pages[] = $page;
767
			}
768
			
769
			$page = $page->Parent;
770
		}
771
772
		return new ArrayList(array_reverse($pages));
773
	}
774
775
776
	/**
777
	 * Make this page a child of another page.
778
	 *
779
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
780
	 *
781
	 * @param SiteTree|int $item Either the parent object, or the parent ID
782
	 */
783
	public function setParent($item) {
784
		if(is_object($item)) {
785
			if (!$item->exists()) $item->write();
786
			$this->setField("ParentID", $item->ID);
787
		} else {
788
			$this->setField("ParentID", $item);
789
		}
790
	}
791
 	
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
792
	/**
793
	 * Get the parent of this page.
794
	 *
795
	 * @return SiteTree Parent of this page
796
	 */
797
	public function getParent() {
798
		if ($parentID = $this->getField("ParentID")) {
799
			return DataObject::get_by_id("SiteTree", $parentID);
800
		}
801
	}
802
803
	/**
804
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
805
	 *
806
	 * @param int $level The maximum amount of levels to traverse.
807
	 * @param string $separator Seperating string
808
	 * @return string The resulting string
809
	 */
810
	public function NestedTitle($level = 2, $separator = " - ") {
811
		$item = $this;
812
		while($item && $level > 0) {
813
			$parts[] = $item->Title;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
814
			$item = $item->Parent;
815
			$level--;
816
		}
817
		return implode($separator, array_reverse($parts));
0 ignored issues
show
Bug introduced by
The variable $parts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
818
	}
819
820
	/**
821
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
822
	 * the security model for an application.
823
	 *
824
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
825
	 * - Checks for existence of a method named "can<$perm>()" on the object
826
	 * - Calls decorators and only returns for FALSE "vetoes"
827
	 * - Falls back to {@link Permission::check()}
828
	 * - Does NOT check for many-many relations named "Can<$perm>"
829
	 *
830
	 * @uses DataObjectDecorator->can()
831
	 *
832
	 * @param string $perm The permission to be checked, such as 'View'
833
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
834
	 * @return bool True if the the member is allowed to do the given action
835
	 */
836
	public function can($perm, $member = null) {
837 View Code Duplication
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
838
			$member = Member::currentUserID();
839
		}
840
841
		if($member && Permission::checkMember($member, "ADMIN")) return true;
842
		
843
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
844
			$method = 'can' . ucfirst($perm);
845
			return $this->$method($member);
846
		}
847
		
848
		$results = $this->extend('can', $member);
849
		if($results && is_array($results)) if(!min($results)) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $results 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...
850
851
		return ($member && Permission::checkMember($member, $perm));
852
	}
853
854
	/**
855
	 * This function should return true if the current user can add children to this page. It can be overloaded to
856
	 * customise the security model for an application.
857
	 *
858
	 * Denies permission if any of the following conditions is true:
859
	 * - alternateCanAddChildren() on a extension returns false
860
	 * - canEdit() is not granted
861
	 * - There are no classes defined in {@link $allowed_children}
862
	 *
863
	 * @uses SiteTreeExtension->canAddChildren()
864
	 * @uses canEdit()
865
	 * @uses $allowed_children
866
	 *
867
	 * @param Member|int $member
868
	 * @return bool True if the current user can add children
869
	 */
870
	public function canAddChildren($member = null) {
871
		// Disable adding children to archived pages
872
		if($this->getIsDeletedFromStage()) {
873
			return false;
874
		}
875
876 View Code Duplication
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
877
			$member = Member::currentUserID();
878
		}
879
880
		if($member && Permission::checkMember($member, "ADMIN")) return true;
881
		
882
		// Standard mechanism for accepting permission changes from extensions
883
		$extended = $this->extendedCan('canAddChildren', $member);
884
		if($extended !== null) return $extended;
885
		
886
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
887
	}
888
889
	/**
890
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
891
	 * security model for an application.
892
	 *
893
	 * Denies permission if any of the following conditions is true:
894
	 * - canView() on any extension returns false
895
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
896
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
897
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
898
	 *
899
	 * @uses DataExtension->canView()
900
	 * @uses ViewerGroups()
901
	 *
902
	 * @param Member|int $member
903
	 * @return bool True if the current user can view this page
904
	 */
905
	public function canView($member = null) {
906 View Code Duplication
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
907
			$member = Member::currentUserID();
908
		}
909
910
		// admin override
911
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
912
		
913
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
914
		if($this->isOrphaned()) return false;
915
916
		// Standard mechanism for accepting permission changes from extensions
917
		$extended = $this->extendedCan('canView', $member);
918
		if($extended !== null) return $extended;
919
		
920
		// check for empty spec
921
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
922
923
		// check for inherit
924
		if($this->CanViewType == 'Inherit') {
925
			if($this->ParentID) return $this->Parent()->canView($member);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
926
			else return $this->getSiteConfig()->canViewPages($member);
927
		}
928
		
929
		// check for any logged-in users
930
		if($this->CanViewType == 'LoggedInUsers' && $member) {
931
			return true;
932
		}
933
		
934
		// check for specific groups
935
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
936
		if(
937
			$this->CanViewType == 'OnlyTheseUsers'
938
			&& $member
939
			&& $member->inGroups($this->ViewerGroups())
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
940
		) return true;
941
		
942
		return false;
943
	}
944
945
	/**
946
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
947
	 * security model for an application.
948
	 *
949
	 * Denies permission if any of the following conditions is true:
950
	 * - canDelete() returns false on any extension
951
	 * - canEdit() returns false
952
	 * - any descendant page returns false for canDelete()
953
	 *
954
	 * @uses canDelete()
955
	 * @uses SiteTreeExtension->canDelete()
956
	 * @uses canEdit()
957
	 *
958
	 * @param Member $member
959
	 * @return bool True if the current user can delete this page
960
	 */
961
	public function canDelete($member = null) {
962 View Code Duplication
		if($member instanceof Member) $memberID = $member->ID;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
963
		else if(is_numeric($member)) $memberID = $member;
964
		else $memberID = Member::currentUserID();
965
		
966
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
967
			return true;
968
		}
969
		
970
		// Standard mechanism for accepting permission changes from extensions
971
		$extended = $this->extendedCan('canDelete', $memberID);
972
		if($extended !== null) return $extended;
973
				
974
		// Regular canEdit logic is handled by can_edit_multiple
975
		$results = self::can_delete_multiple(array($this->ID), $memberID);
976
		
977
		// If this page no longer exists in stage/live results won't contain the page.
978
		// Fail-over to false
979
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
980
	}
981
982
	/**
983
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
984
	 * can be overloaded to customise the security model for an application.
985
	 *
986
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
987
	 * create beneath a parent is based on the ability to edit that parent page.
988
	 *
989
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
990
	 *
991
	 * @uses $can_create
992
	 * @uses DataExtension->canCreate()
993
	 *
994
	 * @param Member $member
995
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
996
	 *                       If a parent page is known, it will be checked for validity.
997
	 *                       If omitted, it will be assumed this is to be created as a top level page.
998
	 * @return bool True if the current user can create pages on this class.
999
	 */
1000
	public function canCreate($member = null, $context = array()) {
1001 View Code Duplication
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1002
			$member = Member::currentUserID();
1003
		}
1004
1005
		// Check parent (custom canCreate option for SiteTree)
1006
		// Block children not allowed for this parent type
1007
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1008
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1009
			return false;
1010
		}
1011
1012
		// Check permission
1013
		if($member && Permission::checkMember($member, "ADMIN")) {
1014
			return true;
1015
		}
1016
1017
		// Standard mechanism for accepting permission changes from extensions
1018
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1019
		if($extended !== null) {
1020
			return $extended;
1021
		}
1022
1023
		// Fall over to inherited permissions
1024
		if($parent) {
1025
			return $parent->canAddChildren($member);
1026
		} else {
1027
			// This doesn't necessarily mean we are creating a root page, but that
1028
			// we don't know if there is a parent, so default to this permission
1029
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1030
		}
1031
	}
1032
1033
	/**
1034
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1035
	 * security model for an application.
1036
	 *
1037
	 * Denies permission if any of the following conditions is true:
1038
	 * - canEdit() on any extension returns false
1039
	 * - canView() return false
1040
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1041
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1042
	 *   CMS_Access_CMSMAIN permission code
1043
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1044
	 *
1045
	 * @uses canView()
1046
	 * @uses EditorGroups()
1047
	 * @uses DataExtension->canEdit()
1048
	 *
1049
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1050
	 *                       unit tests)
1051
	 * @return bool True if the current user can edit this page
1052
	 */
1053
	public function canEdit($member = null) {
1054 View Code Duplication
		if($member instanceof Member) $memberID = $member->ID;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1055
		else if(is_numeric($member)) $memberID = $member;
1056
		else $memberID = Member::currentUserID();
1057
		
1058
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
1059
		
1060
		// Standard mechanism for accepting permission changes from extensions
1061
		$extended = $this->extendedCan('canEdit', $memberID);
1062
		if($extended !== null) return $extended;
1063
1064
		if($this->ID) {
1065
			// Regular canEdit logic is handled by can_edit_multiple
1066
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1067
1068
			// If this page no longer exists in stage/live results won't contain the page.
1069
			// Fail-over to false
1070
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1071
			
1072
		// Default for unsaved pages
1073
		} else {
1074
			return $this->getSiteConfig()->canEditPages($member);
1075
		}
1076
	}
1077
1078
	/**
1079
	 * @deprecated
1080
	 */
1081
	public function canDeleteFromLive($member = null) {
1082
		Deprecation::notice('4.0', 'Use canUnpublish');
1083
1084
		// Deprecated extension
1085
		$extended = $this->extendedCan('canDeleteFromLive', $member);
1086
		if($extended !== null) {
1087
			Deprecation::notice('4.0', 'Use canUnpublish in your extension instead');
1088
			return $extended;
1089
		}
1090
1091
		return $this->canUnpublish($member);
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1092
	}
1093
1094
	/**
1095
	 * Stub method to get the site config, unless the current class can provide an alternate.
1096
	 *
1097
	 * @return SiteConfig
1098
	 */
1099
	public function getSiteConfig() {
1100
		
1101
		if($this->hasMethod('alternateSiteConfig')) {
1102
			$altConfig = $this->alternateSiteConfig();
0 ignored issues
show
Bug introduced by
The method alternateSiteConfig() does not exist on SiteTree. Did you maybe mean config()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1103
			if($altConfig) return $altConfig;
1104
		}
1105
		
1106
		return SiteConfig::current_site_config();
1107
	}
1108
1109
	/**
1110
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1111
	 * can_(perm)_multiple method for efficiency.
1112
	 *
1113
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1114
	 * @param array           $ids           An array of page IDs
1115
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1116
	 *                                       to 'SiteTree::can_(permission)_multiple'
1117
	 */
1118
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1119
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1120
		
1121
		if(is_callable($batchCallback)) {
1122
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1123
		} else {
1124
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1125
				. "with callback '$batchCallback'", E_USER_WARNING);
1126
		}
1127
	}
1128
1129
	/**
1130
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1131
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1132
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1133
	 * efficiently.
1134
	 *
1135
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1136
	 * property to FALSE.
1137
	 *
1138
	 * @param array  $ids              Of {@link SiteTree} IDs
1139
	 * @param int    $memberID         Member ID
1140
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1141
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1142
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1143
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1144
	 * @param bool   $useCached
1145
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1146
	 */
1147
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1148
												  $globalPermission = null, $useCached = true) {
1149
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1150
1151
		// Sanitise the IDs
1152
		$ids = array_filter($ids, 'is_numeric');
1153
		
1154
		// This is the name used on the permission cache
1155
		// converts something like 'CanEditType' to 'edit'.
1156
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1157
1158
		// Default result: nothing editable
1159
		$result = array_fill_keys($ids, false);
1160
		if($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids 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...
1161
1162
			// Look in the cache for values
1163
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1164
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1165
			
1166
				// If we can't find everything in the cache, then look up the remainder separately
1167
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1168
				if($uncachedValues) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedValues 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...
1169
					$cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
0 ignored issues
show
Bug introduced by
It seems like $globalPermission defined by array('CMS_ACCESS_LeftAn..., 'CMS_ACCESS_CMSMain') on line 1149 can also be of type array<integer,string,{"0":"string","1":"string"}>; however, SiteTree::batch_permission_check() does only seem to accept string|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...
1170
				}
1171
				return $cachedValues;
1172
			}
1173
		
1174
			// If a member doesn't have a certain permission then they can't edit anything
1175
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1176
				return $result;
1177
			}
1178
1179
			// Placeholder for parameterised ID list
1180
			$idPlaceholders = DB::placeholders($ids);
1181
1182
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1183
			// be enabled
1184
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1185
		
1186
			// Get the groups that the given member belongs to
1187
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1188
			$SQL_groupList = implode(", ", $groupIDs);
1189
			if (!$SQL_groupList) $SQL_groupList = '0';
1190
			
1191
			$combinedStageResult = array();
1192
1193
			foreach(array('Stage', 'Live') as $stage) {
1194
				// Start by filling the array with the pages that actually exist
1195
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1196
				
1197
				if($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids 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...
1198
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1199
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1200
				} else {
1201
					$stageIds = array();
1202
				}
1203
				$result = array_fill_keys($stageIds, false);
1204
				
1205
				// Get the uninherited permissions
1206
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1207
					->where(array(
1208
						"(\"$typeField\" = 'LoggedInUsers' OR
1209
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1210
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1211
						=> $ids
1212
					))
1213
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1214
				
1215
				if($uninheritedPermissions) {
1216
					// Set all the relevant items in $result to true
1217
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1218
				}
1219
1220
				// Get permissions that are inherited
1221
				$potentiallyInherited = Versioned::get_by_stage(
1222
					"SiteTree",
1223
					$stage,
1224
					array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
0 ignored issues
show
Documentation introduced by
array("\"{$typeField}\" ...laceholders})" => $ids) is of type array<string|integer,array>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1225
				);
1226
1227
				if($potentiallyInherited) {
1228
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1229
					// then see which ones the user has permission on
1230
					$groupedByParent = array();
1231
					foreach($potentiallyInherited as $item) {
1232
						if($item->ParentID) {
1233
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1234
							$groupedByParent[$item->ParentID][] = $item->ID;
1235
						} else {
1236
							// Might return different site config based on record context, e.g. when subsites module
1237
							// is used
1238
							$siteConfig = $item->getSiteConfig();
1239
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1240
						}
1241
					}
1242
1243
					if($groupedByParent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupedByParent 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...
1244
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1245
						if($actuallyInherited) {
1246
							$parentIDs = array_keys(array_filter($actuallyInherited));
1247
							foreach($parentIDs as $parentID) {
1248
								// Set all the relevant items in $result to true
1249
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1250
							}
1251
						}
1252
					}
1253
				}
1254
				
1255
				$combinedStageResult = $combinedStageResult + $result;
1256
				
1257
			}
1258
		}
1259
1260
		if(isset($combinedStageResult)) {
1261
			// Cache the results
1262
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1263
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1264
1265
			return $combinedStageResult;
1266
		} else {
1267
			return array();
1268
		}
1269
	}
1270
1271
	/**
1272
	 * Get the 'can edit' information for a number of SiteTree pages.
1273
	 *
1274
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1275
	 * @param int   $memberID  ID of member
1276
	 * @param bool  $useCached Return values from the permission cache if they exist
1277
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1278
	 *                         edited
1279
	 */
1280
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1281
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1282
	}
1283
1284
	/**
1285
	 * Get the 'can edit' information for a number of SiteTree pages.
1286
	 *
1287
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1288
	 * @param int   $memberID  ID of member
1289
	 * @param bool  $useCached Return values from the permission cache if they exist
1290
	 * @return array
1291
	 */
1292
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1293
		$deletable = array();
0 ignored issues
show
Unused Code introduced by
$deletable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1294
		$result = array_fill_keys($ids, false);
1295
		$cacheKey = "delete-$memberID";
1296
		
1297
		// Look in the cache for values
1298
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1299
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1300
			
1301
			// If we can't find everything in the cache, then look up the remainder separately
1302
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1303
			if($uncachedValues) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedValues 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...
1304
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1305
					+ $cachedValues;
1306
			}
1307
			return $cachedValues;
1308
		}
1309
1310
		// You can only delete pages that you can edit
1311
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1312
		if($editableIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $editableIDs of type array<integer|string> 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...
1313
		
1314
			// You can only delete pages whose children you can delete
1315
			$editablePlaceholders = DB::placeholders($editableIDs);
1316
			$childRecords = SiteTree::get()->where(array(
1317
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1318
			));
1319
			if($childRecords) {
1320
				$children = $childRecords->map("ID", "ParentID");
1321
1322
				// Find out the children that can be deleted
1323
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1324
				
1325
				// Get a list of all the parents that have no undeletable children
1326
				$deletableParents = array_fill_keys($editableIDs, true);
1327
				foreach($deletableChildren as $id => $canDelete) {
1328
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1329
				}
1330
1331
				// Use that to filter the list of deletable parents that have children
1332
				$deletableParents = array_keys($deletableParents);
1333
1334
				// Also get the $ids that don't have children
1335
				$parents = array_unique($children->values());
1336
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1337
1338
				// Combine the two
1339
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1340
1341
			} else {
1342
				$deletable = $editableIDs;
1343
			}
1344
		} else {
1345
			$deletable = array();
1346
		}
1347
		
1348
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1349
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1350
	}
1351
1352
	/**
1353
	 * Collate selected descendants of this page.
1354
	 *
1355
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1356
	 * $collator array.
1357
	 *
1358
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1359
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1360
	 * @return bool
1361
	 */
1362
	public function collateDescendants($condition, &$collator) {
1363
		if($children = $this->Children()) {
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SiteTree. Did you maybe mean duplicateWithChildren()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1364
			foreach($children as $item) {
1365
				if(eval("return $condition;")) $collator[] = $item;
1366
				$item->collateDescendants($condition, $collator);
1367
			}
1368
			return true;
1369
		}
1370
	}
1371
1372
	/**
1373
	 * Return the title, description, keywords and language metatags.
1374
	 *
1375
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1376
	 *
1377
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1378
	 * @return string The XHTML metatags
1379
	 */
1380
	public function MetaTags($includeTitle = true) {
1381
		$tags = "";
1382
		if($includeTitle === true || $includeTitle == 'true') {
1383
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1384
		}
1385
1386
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1387
		if (!empty($generator)) {
1388
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1389
		}
1390
1391
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1392
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1393
		if($this->MetaDescription) {
1394
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1395
		}
1396
		if($this->ExtraMeta) {
1397
			$tags .= $this->ExtraMeta . "\n";
1398
		}
1399
		
1400
		if(Permission::check('CMS_ACCESS_CMSMain')
1401
			&& in_array('CMSPreviewable', class_implements($this))
1402
			&& !$this instanceof ErrorPage
1403
			&& $this->ID > 0
1404
		) {
1405
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1406
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1407
		}
1408
1409
		$this->extend('MetaTags', $tags);
1410
1411
		return $tags;
1412
	}
1413
1414
	/**
1415
	 * Returns the object that contains the content that a user would associate with this page.
1416
	 *
1417
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1418
	 * return the page that is linked to.
1419
	 *
1420
	 * @return $this
1421
	 */
1422
	public function ContentSource() {
1423
		return $this;
1424
	}
1425
1426
	/**
1427
	 * Add default records to database.
1428
	 *
1429
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1430
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1431
	 */
1432
	public function requireDefaultRecords() {
1433
		parent::requireDefaultRecords();
1434
		
1435
		// default pages
1436
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1437
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1438
				$homepage = new Page();
1439
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1440
				$homepage->Content = _t('SiteTree.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>.</p><p>You can now access the <a href="http://docs.silverstripe.org">developer documentation</a>, or begin the <a href="http://www.silverstripe.org/learn/lessons">SilverStripe lessons</a>.</p>');
1441
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1442
				$homepage->Sort = 1;
1443
				$homepage->write();
1444
				$homepage->publish('Stage', 'Live');
1445
				$homepage->flushCache();
1446
				DB::alteration_message('Home page created', 'created');
1447
			}
1448
1449
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1450
				$aboutus = new Page();
1451
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1452
				$aboutus->Content = _t('SiteTree.DEFAULTABOUTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
1453
				$aboutus->Sort = 2;
1454
				$aboutus->write();
1455
				$aboutus->publish('Stage', 'Live');
1456
				$aboutus->flushCache();
1457
				DB::alteration_message('About Us page created', 'created');
1458
1459
				$contactus = new Page();
1460
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1461
				$contactus->Content = _t('SiteTree.DEFAULTCONTACTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
1462
				$contactus->Sort = 3;
1463
				$contactus->write();
1464
				$contactus->publish('Stage', 'Live');
1465
				$contactus->flushCache();
1466
				DB::alteration_message('Contact Us page created', 'created');
1467
			}
1468
		}
1469
		
1470
		// schema migration
1471
		// @todo Move to migration task once infrastructure is implemented
1472
		if($this->class == 'SiteTree') {
1473
			$conn = DB::get_schema();
1474
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1475
			if($conn->hasField('SiteTree' ,'Viewers')) {
1476
				$task = new UpgradeSiteTreePermissionSchemaTask();
1477
				$task->run(new SS_HTTPRequest('GET','/'));
1478
			}
1479
		}
1480
	}
1481
1482
	protected function onBeforeWrite() {
1483
		parent::onBeforeWrite();
1484
1485
		// If Sort hasn't been set, make this page come after it's siblings
1486
		if(!$this->Sort) {
1487
			$parentID = ($this->ParentID) ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1488
			$this->Sort = DB::prepared_query(
1489
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1490
				array($parentID)
1491
			)->value();
1492
		}
1493
1494
		// If there is no URLSegment set, generate one from Title
1495
		$defaultSegment = $this->generateURLSegment(_t(
1496
			'CMSMain.NEWPAGE',
1497
			array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1498
		));
1499
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1500
			$this->URLSegment = $this->generateURLSegment($this->Title);
1501
		} else if($this->isChanged('URLSegment', 2)) {
1502
			// Do a strict check on change level, to avoid double encoding caused by
1503
			// bogus changes through forceChange()
1504
			$filter = URLSegmentFilter::create();
1505
			$this->URLSegment = $filter->filter($this->URLSegment);
1506
			// If after sanitising there is no URLSegment, give it a reasonable default
1507
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1508
		}
1509
		
1510
		// Ensure that this object has a non-conflicting URLSegment value.
1511
		$count = 2;
1512
		while(!$this->validURLSegment()) {
1513
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1514
			$count++;
1515
		}
1516
1517
		$this->syncLinkTracking();
1518
1519
		// Check to see if we've only altered fields that shouldn't affect versioning
1520
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1521
		$changedFields = array_keys($this->getChangedFields(true, 2));
1522
1523
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1524
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1525
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1526
1527
		if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oneChangedFields of type array<integer|string> 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...
1528
			// This will have the affect of preserving the versioning
1529
			$this->migrateVersion($this->Version);
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Documentation Bug introduced by
The method migrateVersion does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1530
		}
1531
	}
1532
1533
	/**
1534
	 * Trigger synchronisation of link tracking
1535
	 *
1536
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1537
	 */
1538
	public function syncLinkTracking() {
1539
		$this->extend('augmentSyncLinkTracking');
1540
	}
1541
	
1542
	public function onBeforeDelete() {
1543
		parent::onBeforeDelete();
1544
		
1545
		// If deleting this page, delete all its children.
1546
		if(SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1547
			foreach($children as $child) {
1548
				$child->delete();
1549
			}
1550
		}
1551
	}
1552
	
1553
	public function onAfterDelete() {
1554
		// Need to flush cache to avoid outdated versionnumber references
1555
		$this->flushCache();
1556
		
1557
		// Need to mark pages depending to this one as broken
1558
		$dependentPages = $this->DependentPages();
1559
		if($dependentPages) foreach($dependentPages as $page) {
1560
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1561
			$page->write();
1562
		}
1563
		
1564
		parent::onAfterDelete();
1565
	}
1566
1567
	public function flushCache($persistent = true) {
1568
		parent::flushCache($persistent);
1569
		$this->_cache_statusFlags = null;
1570
	}
1571
	
1572
	public function validate() {
1573
		$result = parent::validate();
1574
1575
		// Allowed children validation
1576
		$parent = $this->getParent();
1577
		if($parent && $parent->exists()) {
1578
			// No need to check for subclasses or instanceof, as allowedChildren() already
1579
			// deconstructs any inheritance trees already.
1580
			$allowed = $parent->allowedChildren();
1581
			$subject = ($this instanceof VirtualPage && $this->CopyContentFromID) ? $this->CopyContentFrom() : $this;
0 ignored issues
show
Bug introduced by
The property CopyContentFromID does not seem to exist. Did you mean Content?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Documentation Bug introduced by
The method CopyContentFrom does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1582
			if(!in_array($subject->ClassName, $allowed)) {
1583
				
1584
				$result->error(
1585
					_t(
1586
						'SiteTree.PageTypeNotAllowed',
1587
						'Page type "{type}" not allowed as child of this parent page',
1588
						array('type' => $subject->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $subject->i18n_singular_name()) is of type array<string,?,{"type":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1589
					),
1590
					'ALLOWED_CHILDREN'
1591
				);
1592
			}
1593
		}
1594
1595
		// "Can be root" validation
1596
		if(!$this->stat('can_be_root') && !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1597
			$result->error(
1598
				_t(
1599
					'SiteTree.PageTypNotAllowedOnRoot',
1600
					'Page type "{type}" is not allowed on the root level',
1601
					array('type' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $this->i18n_singular_name()) is of type array<string,string,{"type":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1602
				),
1603
				'CAN_BE_ROOT'
1604
			);
1605
		}
1606
		
1607
		return $result;
1608
	}
1609
	
1610
	/**
1611
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1612
	 * checks for:
1613
	 *  - A page with the same URLSegment that has a conflict
1614
	 *  - Conflicts with actions on the parent page
1615
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1616
	 *
1617
	 * @return bool
1618
	 */
1619
	public function validURLSegment() {
1620
		if(self::config()->nested_urls && $parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1621
			if($controller = ModelAsController::controller_for($parent)) {
1622
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1623
			}
1624
		}
1625
		
1626
		if(!self::config()->nested_urls || !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1627
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1628
		}
1629
		
1630
		// Filters by url, id, and parent
1631
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1632
		if($this->ID) {
1633
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1634
		}
1635
		if(self::config()->nested_urls) {
1636
			$filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1637
		}
1638
		
1639
		$votes = array_filter(
1640
			(array)$this->extend('augmentValidURLSegment'),
1641
			function($v) {return !is_null($v);}
1642
		);
1643
		if($votes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $votes 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...
1644
			return min($votes);
1645
		}
1646
1647
		// Check existence
1648
		$existingPage = DataObject::get_one('SiteTree', $filter);
1649
		if ($existingPage) return false;
1650
1651
		return !($existingPage);
1652
		}
1653
		
1654
	/**
1655
	 * Generate a URL segment based on the title provided.
1656
	 *
1657
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1658
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1659
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1660
	 * scratch, or incrementally modify the generated URL.
1661
	 *
1662
	 * @param string $title Page title
1663
	 * @return string Generated url segment
1664
	 */
1665
	public function generateURLSegment($title){
1666
		$filter = URLSegmentFilter::create();
1667
		$t = $filter->filter($title);
1668
		
1669
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1670
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1671
		
1672
		// Hook for extensions
1673
		$this->extend('updateURLSegment', $t, $title);
1674
		
1675
		return $t;
1676
	}
1677
	
1678
	/**
1679
	 * Gets the URL segment for the latest draft version of this page.
1680
	 *
1681
	 * @return string
1682
	 */
1683
	public function getStageURLSegment() {
1684
		$stageRecord = Versioned::get_one_by_stage('SiteTree', 'Stage', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1685
			'"SiteTree"."ID"' => $this->ID
1686
		));
1687
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1688
	}
1689
	
1690
	/**
1691
	 * Gets the URL segment for the currently published version of this page.
1692
	 *
1693
	 * @return string
1694
	 */
1695
	public function getLiveURLSegment() {
1696
		$liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1697
			'"SiteTree"."ID"' => $this->ID
1698
		));
1699
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1700
	}
1701
	
1702
	/**
1703
	 * Rewrites any linked images on this page without creating a new version record.
1704
	 * Non-image files should be linked via shortcodes
1705
	 * Triggers the onRenameLinkedAsset action on extensions.
1706
	 *
1707
	 * @todo Implement image shortcodes and remove this feature
1708
	 */
1709
	public function rewriteFileLinks() {
1710
		// Skip live stage
1711
		if(\Versioned::get_stage() === \Versioned::LIVE_STAGE) {
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1712
			return;
1713
		}
1714
1715
		// Update the content without actually creating a new version
1716
		foreach($this->db() as $fieldName => $fieldType) {
0 ignored issues
show
Bug introduced by
The expression $this->db() of type array|string|null 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...
1717
			// Skip if non HTML or if empty
1718
			if ($fieldType !== 'HTMLText') {
1719
				continue;
1720
			}
1721
			$fieldValue = $this->{$fieldName};
1722
			if(empty($fieldValue)) {
1723
				continue;
1724
			}
1725
1726
			// Regenerate content
1727
			$content = Image::regenerate_html_links($fieldValue);
1728
			if($content === $fieldValue) {
1729
				continue;
1730
			}
1731
1732
			// Write content directly without updating linked assets
1733
			$table = ClassInfo::table_for_object_field($this, $fieldName);
1734
			$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
1735
			DB::prepared_query($query, array($content, $this->ID));
1736
1737
			// Update linked assets
1738
			$this->invokeWithExtensions('onRenameLinkedAsset');
1739
		}
1740
	}
1741
	
1742
	/**
1743
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1744
	 *
1745
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1746
	 * @return ArrayList
1747
	 */
1748
	public function DependentPages($includeVirtuals = true) {
1749
		if(class_exists('Subsite')) {
1750
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1751
			Subsite::disable_subsite_filter(true);
1752
		}
1753
		
1754
		// Content links
1755
		$items = new ArrayList();
1756
1757
		// We merge all into a regular SS_List, because DataList doesn't support merge
1758
		if($contentLinks = $this->BackLinkTracking()) {
0 ignored issues
show
Documentation Bug introduced by
The method BackLinkTracking does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1759
			$linkList = new ArrayList();
1760
			foreach($contentLinks as $item) {
1761
				$item->DependentLinkType = 'Content link';
1762
				$linkList->push($item);
1763
			}
1764
			$items->merge($linkList);
1765
		}
1766
		
1767
		// Virtual pages
1768
		if($includeVirtuals) {
1769
			$virtuals = $this->VirtualPages();
1770
			if($virtuals) {
1771
				$virtualList = new ArrayList();
1772
				foreach($virtuals as $item) {
1773
					$item->DependentLinkType = 'Virtual page';
1774
					$virtualList->push($item);
1775
				}
1776
				$items->merge($virtualList);
1777
			}
1778
		}
1779
1780
		// Redirector pages
1781
		$redirectors = RedirectorPage::get()->where(array(
1782
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1783
			'"RedirectorPage"."LinkToID"' => $this->ID
1784
		));
1785
		if($redirectors) {
1786
			$redirectorList = new ArrayList();
1787
			foreach($redirectors as $item) {
1788
				$item->DependentLinkType = 'Redirector page';
1789
				$redirectorList->push($item);
1790
			}
1791
			$items->merge($redirectorList);
1792
		}
1793
1794
		if(class_exists('Subsite')) Subsite::disable_subsite_filter($origDisableSubsiteFilter);
0 ignored issues
show
Bug introduced by
The variable $origDisableSubsiteFilter does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1795
		
1796
		return $items;
1797
	}
1798
1799
	/**
1800
	 * Return all virtual pages that link to this page.
1801
	 *
1802
	 * @return DataList
1803
	 */
1804
	public function VirtualPages() {
1805
		$pages = parent::VirtualPages();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DataObject as the method VirtualPages() does only exist in the following sub-classes of DataObject: SiteTree. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1806
		
1807
		// Disable subsite filter for these pages
1808
		if($pages instanceof DataList) {
1809
			return $pages->setDataQueryParam('Subsite.filter', false);
1810
		} else {
1811
			return $pages;
1812
		}
1813
	}
1814
1815
	/**
1816
	 * Returns a FieldList with which to create the main editing form.
1817
	 *
1818
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1819
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1820
	 *
1821
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1822
	 * e.g. access control.
1823
	 *
1824
	 * @return FieldList The fields to be displayed in the CMS
1825
	 */
1826
	public function getCMSFields() {
1827
		require_once("forms/Form.php");
1828
		// Status / message
1829
		// Create a status message for multiple parents
1830
		if($this->ID && is_numeric($this->ID)) {
1831
			$linkedPages = $this->VirtualPages();
1832
1833
			$parentPageLinks = array();
1834
1835
			if($linkedPages->Count() > 0) {
1836
				foreach($linkedPages as $linkedPage) {
1837
					$parentPage = $linkedPage->Parent;
1838
					if($parentPage) {
1839
						if($parentPage->ID) {
1840
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1841
						} else {
1842
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1843
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1844
								"</a>";
1845
						}
1846
					}
1847
				}
1848
1849
				$lastParent = array_pop($parentPageLinks);
1850
				$parentList = "'$lastParent'";
1851
1852
				if(count($parentPageLinks) > 0) {
1853
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1854
						. $parentList;
1855
				}
1856
1857
				$statusMessage[] = _t(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$statusMessage was never initialized. Although not strictly required by PHP, it is generally a good practice to add $statusMessage = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1858
					'SiteTree.APPEARSVIRTUALPAGES',
1859
					"This content also appears on the virtual pages in the {title} sections.",
1860
					array('title' => $parentList)
0 ignored issues
show
Documentation introduced by
array('title' => $parentList) is of type array<string,?,{"title":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1861
				);
1862
			}
1863
		}
1864
1865
		if($this->HasBrokenLink || $this->HasBrokenFile) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenLink does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
Documentation introduced by
The property HasBrokenFile does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1866
			$statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
0 ignored issues
show
Bug introduced by
The variable $statusMessage does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1867
		}
1868
1869
		$dependentNote = '';
1870
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1871
		
1872
		// Create a table for showing pages linked to this one
1873
		$dependentPages = $this->DependentPages();
1874
		$dependentPagesCount = $dependentPages->Count();
1875
		if($dependentPagesCount) {
1876
			$dependentColumns = array(
1877
				'Title' => $this->fieldLabel('Title'),
1878
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1879
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1880
			);
1881
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1882
			
1883
			$dependentNote = new LiteralField('DependentNote', '<p>' . _t('SiteTree.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
1884
			$dependentTable = GridField::create(
1885
				'DependentPages',
1886
				false,
1887
				$dependentPages
1888
			);
1889
			$dependentTable->getConfig()->getComponentByType('GridFieldDataColumns')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1890
				->setDisplayFields($dependentColumns)
1891
				->setFieldFormatting(array(
1892
					'Title' => function($value, &$item) {
1893
						return sprintf(
1894
							'<a href="admin/pages/edit/show/%d">%s</a>',
1895
							(int)$item->ID,
1896
							Convert::raw2xml($item->Title)
1897
						);
1898
					},
1899
					'AbsoluteLink' => function($value, &$item) {
0 ignored issues
show
Unused Code introduced by
The parameter $item is not used and could be removed.

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

Loading history...
1900
						return sprintf(
1901
							'<a href="%s" target="_blank">%s</a>',
1902
							Convert::raw2xml($value),
1903
							Convert::raw2xml($value)
1904
						);
1905
					}
1906
				));
1907
		}
1908
		
1909
		$baseLink = Controller::join_links (
1910
			Director::absoluteBaseURL(),
1911
			(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1912
		);
1913
		
1914
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1915
			->setURLPrefix($baseLink)
1916
			->setDefaultURL($this->generateURLSegment(_t(
1917
				'CMSMain.NEWPAGE',
1918
				array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1919
			)));
1920
		$helpText = (self::config()->nested_urls && count($this->Children())) ? $this->fieldLabel('LinkChangeNote') : '';
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SiteTree. Did you maybe mean duplicateWithChildren()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1921
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1922
			$helpText .= $helpText ? '<br />' : '';
1923
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1924
		}
1925
		$urlsegment->setHelpText($helpText);
1926
		
1927
		$fields = new FieldList(
1928
			$rootTab = new TabSet("Root",
1929
				$tabMain = new Tab('Main',
1930
					new TextField("Title", $this->fieldLabel('Title')),
1931
					$urlsegment,
1932
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1933
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1934
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1935
						array(
1936
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1937
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1938
						)
1939
					)->setHeadingLevel(4)
1940
				),
1941
				$tabDependent = new Tab('Dependent',
1942
					$dependentNote,
1943
					$dependentTable
1944
				)
1945
			)
1946
		);
1947
		$htmlField->addExtraClass('stacked');
1948
		
1949
		// Help text for MetaData on page content editor
1950
		$metaFieldDesc
1951
			->setRightTitle(
1952
				_t(
1953
					'SiteTree.METADESCHELP',
1954
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1955
				)
1956
			)
1957
			->addExtraClass('help');
1958
		$metaFieldExtra
1959
			->setRightTitle(
1960
				_t(
1961
					'SiteTree.METAEXTRAHELP',
1962
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1963
				)
1964
			)
1965
			->addExtraClass('help');
1966
1967
		// Conditional dependent pages tab
1968
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1969
		else $fields->removeFieldFromTab('Root', 'Dependent');
1970
		
1971
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1972
1973
		if($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1974
			$obsoleteWarning = _t(
1975
				'SiteTree.OBSOLETECLASS',
1976
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1977
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Documentation introduced by
array('type' => $this->ObsoleteClassName) is of type array<string,?,{"type":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1978
			);
1979
1980
			$fields->addFieldToTab(
1981
				"Root.Main",
1982
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1983
				"Title"
1984
			);
1985
		}
1986
1987
		if(file_exists(BASE_PATH . '/install.php')) {
1988
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
1989
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
1990
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
1991
				. "</p>"), "Title");
1992
		}
1993
1994
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
1995
		$fields->setTabPathRewrites(array(
1996
			'/^Root\.Content\.Main$/' => 'Root.Main',
1997
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
1998
		));
1999
		
2000
		if(self::$runCMSFieldsExtensions) {
2001
			$this->extend('updateCMSFields', $fields);
2002
		}
2003
2004
		return $fields;
2005
	}
2006
	
2007
	
2008
	/**
2009
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2010
	 * for content-related fields.
2011
	 *
2012
	 * @return FieldList
2013
	 */
2014
	public function getSettingsFields() {
2015
		$groupsMap = array();
2016
		foreach(Group::get() as $group) {
2017
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2018
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2019
		}
2020
		asort($groupsMap);
2021
		
2022
		$fields = new FieldList(
2023
			$rootTab = new TabSet("Root",
2024
				$tabBehaviour = new Tab('Settings',
2025
					new DropdownField(
2026
						"ClassName",
2027
						$this->fieldLabel('ClassName'),
2028
						$this->getClassDropdown()
2029
					),
2030
					$parentTypeSelector = new CompositeField(
2031
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2032
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2033
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2034
						)),
2035
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
2036
					),
2037
					$visibility = new FieldGroup(
2038
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2039
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2040
					),
2041
					$viewersOptionsField = new OptionsetField(
2042
						"CanViewType",
2043
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2044
					),
2045
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2046
						->setSource($groupsMap)
2047
						->setAttribute(
2048
							'data-placeholder',
2049
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2050
						),
2051
					$editorsOptionsField = new OptionsetField(
2052
						"CanEditType",
2053
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2054
					),
2055
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2056
						->setSource($groupsMap)
2057
						->setAttribute(
2058
							'data-placeholder',
2059
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2060
						)
2061
				)
2062
			)
2063
		);
2064
		
2065
		$visibility->setTitle($this->fieldLabel('Visibility'));
2066
		
2067
2068
		// This filter ensures that the ParentID dropdown selection does not show this node,
2069
		// or its descendents, as this causes vanishing bugs
2070
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2071
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2072
		
2073
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2074
		
2075
		// Make page location fields read-only if the user doesn't have the appropriate permission
2076
		if(!Permission::check("SITETREE_REORGANISE")) {
2077
			$fields->makeFieldReadonly('ParentType');
2078
			if($this->ParentType == 'root') {
0 ignored issues
show
Documentation introduced by
The property ParentType does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
2079
				$fields->removeByName('ParentID');
2080
			} else {
2081
				$fields->makeFieldReadonly('ParentID');
2082
			}
2083
		}
2084
		
2085
		$viewersOptionsSource = array();
2086
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2087
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2088
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2089
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2090
		$viewersOptionsField->setSource($viewersOptionsSource);
2091
		
2092
		$editorsOptionsSource = array();
2093
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2094
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2095
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2096
		$editorsOptionsField->setSource($editorsOptionsSource);
2097
2098
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2099
			$fields->makeFieldReadonly($viewersOptionsField);
2100
			if($this->CanViewType == 'OnlyTheseUsers') {
2101
				$fields->makeFieldReadonly($viewerGroupsField);
2102
			} else {
2103
				$fields->removeByName('ViewerGroups');
2104
			}
2105
			
2106
			$fields->makeFieldReadonly($editorsOptionsField);
2107
			if($this->CanEditType == 'OnlyTheseUsers') {
2108
				$fields->makeFieldReadonly($editorGroupsField);
2109
			} else {
2110
				$fields->removeByName('EditorGroups');
2111
			}
2112
		}
2113
		
2114
		if(self::$runCMSFieldsExtensions) {
2115
			$this->extend('updateSettingsFields', $fields);
2116
		}
2117
		
2118
		return $fields;
2119
	}
2120
	
2121
	/**
2122
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2123
	 * @return array
2124
	 */
2125
	public function fieldLabels($includerelations = true) {
2126
		$cacheKey = $this->class . '_' . $includerelations;
2127
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2128
			$labels = parent::fieldLabels($includerelations);
2129
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2130
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2131
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2132
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2133
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2134
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2135
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2136
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2137
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2138
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2139
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2140
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2141
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2142
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2143
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2144
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2145
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2146
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2147
			$labels['LinkChangeNote'] = _t (
2148
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2149
			);
2150
			
2151
			if($includerelations){
2152
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2153
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2154
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2155
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2156
			}
2157
2158
			self::$_cache_field_labels[$cacheKey] = $labels;
2159
		}
2160
2161
		return self::$_cache_field_labels[$cacheKey];
2162
	}
2163
2164
	/**
2165
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2166
	 *
2167
	 * Frontend scripts and styles know how to handle the following FormFields:
2168
	 * - top-level FormActions appear as standalone buttons
2169
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2170
	 * - TabSet & Tabs appear as a drop ups
2171
	 * - FormActions within the Tab are restyled as links
2172
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2173
	 *
2174
	 * @return FieldList The available actions for this page.
2175
	 */
2176
	public function getCMSActions() {
2177
		$existsOnLive = $this->getExistsOnLive();
2178
2179
		// Major actions appear as buttons immediately visible as page actions.
2180
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset noborder');
2181
2182
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2183
		$rootTabSet = new TabSet('ActionMenus');
2184
		$moreOptions = new Tab(
2185
			'MoreOptions',
2186
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2187
		);
2188
		$rootTabSet->push($moreOptions);
2189
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2190
2191
		// Render page information into the "more-options" drop-up, on the top.
2192
		$live = Versioned::get_one_by_stage('SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2193
			'"SiteTree"."ID"' => $this->ID
2194
		));
2195
		$moreOptions->push(
2196
			new LiteralField('Information',
2197
				$this->customise(array(
2198
					'Live' => $live,
2199
					'ExistsOnLive' => $existsOnLive
2200
				))->renderWith('SiteTree_Information')
2201
			)
2202
		);
2203
2204
		// "readonly"/viewing version that isn't the current version of the record
2205
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), array(
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2206
			'"SiteTree"."ID"' => $this->ID
2207
		));
2208
		if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2209
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2210
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2211
2212
			$actions = new FieldList(array($majorActions, $rootTabSet));
2213
2214
			// getCMSActions() can be extended with updateCMSActions() on a extension
2215
			$this->extend('updateCMSActions', $actions);
2216
2217
			return $actions;
2218
		}
2219
2220 View Code Duplication
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method canPublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method canUnpublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2221
			// "unpublish"
2222
			$moreOptions->push(
2223
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2224
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2225
					->addExtraClass('ss-ui-action-destructive')
2226
			);
2227
		}
2228
2229 View Code Duplication
		if($this->stagesDiffer('Stage', 'Live') && !$this->getIsDeletedFromStage()) {
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2230
			if($this->isPublished() && $this->canEdit())	{
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2231
				// "rollback"
2232
				$moreOptions->push(
2233
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2234
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2235
				);
2236
			}
2237
		}
2238
2239
		if($this->canEdit()) {
2240
			if($this->getIsDeletedFromStage()) {
2241
				// The usual major actions are not available, so we provide alternatives here.
2242
				if($existsOnLive) {
2243
					// "restore"
2244
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2245
					if($this->canDelete() && $this->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2246
						// "delete from live"
2247
						$majorActions->push(
2248
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2249
								->addExtraClass('ss-ui-action-destructive')
2250
						);
2251
					}
2252
				} else {
2253
					// Determine if we should force a restore to root (where once it was a subpage)
2254
					$restoreToRoot = $this->isParentArchived();
2255
					
2256
					// "restore"
2257
					$title = $restoreToRoot
2258
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2259
						: _t('CMSMain.RESTORE','Restore draft');
2260
					$description = $restoreToRoot
2261
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2262
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2263
					$majorActions->push(
2264
						FormAction::create('restore', $title)
2265
							->setDescription($description)
2266
							->setAttribute('data-to-root', $restoreToRoot)
0 ignored issues
show
Documentation introduced by
$restoreToRoot is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2267
							->setAttribute('data-icon', 'decline')
2268
					);
2269
				}
2270
			} else {
2271
				// Detect use of legacy actions
2272
				// {@see CMSMain::enabled_legacy_actions}
2273
				$legacy = CMSMain::config()->enabled_legacy_actions;
2274
				if(in_array('CMSBatchAction_Delete', $legacy)) {
2275
					Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
2276
					if($this->canDelete()) {
2277
						// delete
2278
						$moreOptions->push(
2279
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2280
								->addExtraClass('delete ss-ui-action-destructive')
2281
						);
2282
					}
2283
				} elseif($this->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2284
					// "archive"
2285
					$moreOptions->push(
2286
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2287
							->setDescription(_t(
2288
								'SiteTree.BUTTONARCHIVEDESC',
2289
								'Unpublish and send to archive'
2290
							))
2291
							->addExtraClass('delete ss-ui-action-destructive')
2292
					);
2293
				}
2294
			
2295
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2296
				$majorActions->push(
2297
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2298
						->setAttribute('data-icon', 'accept')
2299
						->setAttribute('data-icon-alternate', 'addpage')
2300
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2301
				);
2302
			}
2303
		}
2304
2305
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
0 ignored issues
show
Documentation Bug introduced by
The method canPublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2306
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2307
			$majorActions->push(
2308
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2309
					->setAttribute('data-icon', 'accept')
2310
					->setAttribute('data-icon-alternate', 'disk')
2311
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2312
			);
2313
2314
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2315
			if($this->stagesDiffer('Stage', 'Live')) {
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2316
				$publish->addExtraClass('ss-ui-alternate');
2317
			}
2318
		}
2319
		
2320
		$actions = new FieldList(array($majorActions, $rootTabSet));
2321
		
2322
		// Hook for extensions to add/remove actions.
2323
		$this->extend('updateCMSActions', $actions);
2324
		
2325
		return $actions;
2326
	}
2327
	
2328
	public function onAfterPublish() {
2329
		// Force live sort order to match stage sort order
2330
		DB::prepared_query('UPDATE "SiteTree_Live"
2331
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2332
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2333
			array($this->ParentID)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
2334
		);
2335
	}
2336
	
2337
	/**
2338
	 * Unpublish this page - remove it from the live site
2339
	 *
2340
	 * Overrides {@see Versioned::doUnpublish()}
2341
	 *
2342
	 * @uses SiteTreeExtension->onBeforeUnpublish()
2343
	 * @uses SiteTreeExtension->onAfterUnpublish()
2344
	 */
2345
	public function doUnpublish() {
2346
		if(!$this->canUnpublish()) return false;
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2347
		if(!$this->ID) return false;
2348
		
2349
		$this->invokeWithExtensions('onBeforeUnpublish', $this);
2350
		
2351
		$origStage = Versioned::get_stage();
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2352
		Versioned::set_stage(Versioned::LIVE_STAGE);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2353
2354
		// We should only unpublish virtualpages that exist on live
2355
		$virtualPages = $this->VirtualPages();
2356
2357
		// This way our ID won't be unset
2358
		$clone = clone $this;
2359
		$clone->delete();
2360
2361
		// Rewrite backlinks
2362
		$dependentPages = $this->DependentPages(false);
2363
		if($dependentPages) foreach($dependentPages as $page) {
2364
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2365
			$page->write();
2366
		}
2367
		Versioned::set_stage($origStage);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2368
2369
		// Unpublish any published virtual pages
2370
		if ($virtualPages) foreach($virtualPages as $vp) {
2371
			$vp->doUnpublish();
2372
		}
2373
2374
		// If we're on the draft site, then we can update the status.
2375
		// Otherwise, these lines will resurrect an inappropriate record
2376
		if(DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()
2377
			&& Versioned::get_stage() != 'Live') {
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2378
			$this->write();
2379
		}
2380
2381
		$this->invokeWithExtensions('onAfterUnpublish', $this);
2382
2383
		return true;
2384
	}
2385
	
2386
	/**
2387
	 * Revert the draft changes: replace the draft content with the content on live
2388
	 */
2389
	public function doRevertToLive() {
2390
		$this->invokeWithExtensions('onBeforeRevertToLive', $this);
2391
2392
		$this->publish("Live", "Stage", false);
0 ignored issues
show
Bug introduced by
The method publish() does not exist on SiteTree. Did you maybe mean onAfterPublish()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2393
2394
		// Use a clone to get the updates made by $this->publish
2395
		$clone = DataObject::get_by_id("SiteTree", $this->ID);
2396
		$clone->writeWithoutVersion();
2397
2398
		// Need to update pages linking to this one as no longer broken
2399
		foreach($this->DependentPages(false) as $page) {
2400
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2401
			$page->write();
2402
		}
2403
		
2404
		$this->invokeWithExtensions('onAfterRevertToLive', $this);
2405
		return true;
2406
	}
2407
2408
	/**
2409
	 * Determine if this page references a parent which is archived, and not available in stage
2410
	 *
2411
	 * @return bool True if there is an archived parent
2412
	 */
2413
	protected function isParentArchived() {
2414
		if($parentID = $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. 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...
2415
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2416
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2417
				return true;
2418
			}
2419
		}
2420
		return false;
2421
	}
2422
	
2423
	/**
2424
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2425
	 *
2426
	 * @return self
2427
	 */
2428
	public function doRestoreToStage() {
2429
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2430
2431
		// Ensure that the parent page is restored, otherwise restore to root
2432
		if($this->isParentArchived()) {
2433
			$this->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. 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...
2434
		}
2435
		
2436
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2437
		// create an empty record
2438
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2439
			$conn = DB::get_conn();
2440
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SS_Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2441
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2442
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SS_Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2443
		}
2444
		
2445
		$oldStage = Versioned::get_stage();
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2446
		Versioned::set_stage(Versioned::DRAFT_STAGE);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2447
		$this->forceChange();
2448
		$this->write();
2449
		
2450
		$result = DataObject::get_by_id($this->class, $this->ID);
2451
2452
		// Need to update pages linking to this one as no longer broken
2453
		foreach($result->DependentPages(false) as $page) {
2454
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2455
			$page->write();
2456
		}
2457
		
2458
		Versioned::set_stage($oldStage);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2459
2460
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2461
		
2462
		return $result;
2463
	}
2464
2465
	/**
2466
	 * @deprecated
2467
	 */
2468
	public function doDeleteFromLive() {
2469
		Deprecation::notice("4.0", "Use doUnpublish instead");
2470
		return $this->doUnpublish();
2471
	}
2472
2473
	/**
2474
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2475
	 *
2476
	 * @return bool
2477
	 */
2478
	public function isNew() {
2479
		/**
2480
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2481
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2482
		 */
2483
		if(empty($this->ID)) return true;
2484
2485
		if(is_numeric($this->ID)) return false;
2486
2487
		return stripos($this->ID, 'new') === 0;
2488
	}
2489
2490
	/**
2491
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2492
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2493
	 * {@link SiteTree::$needs_permission}.
2494
	 *
2495
	 * @return array
2496
	 */
2497
	protected function getClassDropdown() {
2498
		$classes = self::page_type_classes();
2499
		$currentClass = null;
2500
		$result = array();
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
2501
		
2502
		$result = array();
2503
		foreach($classes as $class) {
2504
			$instance = singleton($class);
2505
2506
			// if the current page type is this the same as the class type always show the page type in the list
2507
			if ($this->ClassName != $instance->ClassName) {
2508
				if($instance instanceof HiddenClass) continue;
2509
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2510
			}
2511
			
2512
			if($perms = $instance->stat('need_permission')) {
2513
				if(!$this->can($perms)) continue;
2514
			}
2515
2516
			$pageTypeName = $instance->i18n_singular_name();
2517
2518
			$currentClass = $class;
2519
			$result[$class] = $pageTypeName;
2520
2521
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2522
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2523
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2524
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2525
				$result[$class] = $result[$class] .  " ({$class})";
2526
			}
2527
		}
2528
		
2529
		// sort alphabetically, and put current on top
2530
		asort($result);
2531
		if($currentClass) {
2532
			$currentPageTypeName = $result[$currentClass];
2533
			unset($result[$currentClass]);
2534
			$result = array_reverse($result);
2535
			$result[$currentClass] = $currentPageTypeName;
2536
			$result = array_reverse($result);
2537
		}
2538
		
2539
		return $result;
2540
	}
2541
2542
	/**
2543
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2544
	 *
2545
	 * @return string[]
2546
	 */
2547
	public function allowedChildren() {
2548
		$allowedChildren = array();
2549
		$candidates = $this->stat('allowed_children');
2550
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2551
			foreach($candidates as $candidate) {
0 ignored issues
show
Bug introduced by
The expression $candidates 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...
2552
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2553
				// Otherwise, the class and all its subclasses are allowed.
2554
				if(substr($candidate,0,1) == '*') {
2555
					$allowedChildren[] = substr($candidate,1);
2556
				} else {
2557
					$subclasses = ClassInfo::subclassesFor($candidate);
2558
					foreach($subclasses as $subclass) {
0 ignored issues
show
Bug introduced by
The expression $subclasses of type null|array 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...
2559
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2560
					}
2561
				}
2562
			}
2563
		}
2564
		
2565
		return $allowedChildren;
2566
	}
2567
2568
	/**
2569
	 * Returns the class name of the default class for children of this page.
2570
	 *
2571
	 * @return string
2572
	 */
2573
	public function defaultChild() {
2574
		$default = $this->stat('default_child');
2575
		$allowed = $this->allowedChildren();
2576
		if($allowed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowed of type string[] 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...
2577
			if(!$default || !in_array($default, $allowed))
2578
				$default = reset($allowed);
2579
			return $default;
2580
		}
2581
	}
2582
2583
	/**
2584
	 * Returns the class name of the default class for the parent of this page.
2585
	 *
2586
	 * @return string
2587
	 */
2588
	public function defaultParent() {
2589
		return $this->stat('default_parent');
2590
	}
2591
2592
	/**
2593
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2594
	 * Title field.
2595
	 *
2596
	 * @return string
2597
	 */
2598
	public function getMenuTitle(){
2599
		if($value = $this->getField("MenuTitle")) {
2600
			return $value;
2601
		} else {
2602
			return $this->getField("Title");
2603
		}
2604
	}
2605
2606
2607
	/**
2608
	 * Set the menu title for this page.
2609
	 *
2610
	 * @param string $value
2611
	 */
2612
	public function setMenuTitle($value) {
2613
		if($value == $this->getField("Title")) {
2614
			$this->setField("MenuTitle", null);
2615
		} else {
2616
			$this->setField("MenuTitle", $value);
2617
		}
2618
	}
2619
	
2620
	/**
2621
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2622
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2623
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2624
	 * the flags.
2625
	 *
2626
	 * Example (simple):
2627
	 *   "deletedonlive" => "Deleted"
2628
	 *
2629
	 * Example (with optional title attribute):
2630
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2631
	 *
2632
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2633
	 * @return array
2634
	 */
2635
	public function getStatusFlags($cached = true) {
2636
		if(!$this->_cache_statusFlags || !$cached) {
2637
			$flags = array();
2638
			if($this->getIsDeletedFromStage()) {
2639
				if($this->getExistsOnLive()) {
2640
					$flags['removedfromdraft'] = array(
2641
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2642
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2643
					);
2644
				} else {
2645
					$flags['archived'] = array(
2646
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2647
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2648
					);
2649
				}
2650
			} else if($this->getIsAddedToStage()) {
2651
				$flags['addedtodraft'] = array(
2652
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2653
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2654
				);
2655
			} else if($this->getIsModifiedOnStage()) {
2656
				$flags['modified'] = array(
2657
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2658
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2659
				);
2660
			}
2661
2662
			$this->extend('updateStatusFlags', $flags);
2663
2664
			$this->_cache_statusFlags = $flags;
2665
		}
2666
		
2667
		return $this->_cache_statusFlags;
2668
	}
2669
2670
	/**
2671
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2672
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2673
	 * publication status.
2674
	 *
2675
	 * @return string An HTML string ready to be directly used in a template
2676
	 */
2677
	public function getTreeTitle() {
2678
		// Build the list of candidate children
2679
		$children = array();
2680
		$candidates = static::page_type_classes();
2681
		foreach($this->allowedChildren() as $childClass) {
2682
			if(!in_array($childClass, $candidates)) continue;
2683
			$child = singleton($childClass);
2684
			if($child->canCreate(null, array('Parent' => $this))) {
2685
				$children[$childClass] = $child->i18n_singular_name();
2686
			}
2687
		}
2688
		$flags = $this->getStatusFlags();
2689
		$treeTitle = sprintf(
2690
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2691
			Convert::raw2att(Convert::raw2json($children)),
2692
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2693
		);
2694
		foreach($flags as $class => $data) {
2695
			if(is_string($data)) $data = array('text' => $data);
2696
			$treeTitle .= sprintf(
2697
				"<span class=\"badge %s\"%s>%s</span>",
2698
				'status-' . Convert::raw2xml($class),
2699
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2700
				Convert::raw2xml($data['text'])
2701
			);
2702
		}
2703
		
2704
		return $treeTitle;
2705
	}
2706
2707
	/**
2708
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2709
	 * we're currently inside, etc.
2710
	 *
2711
	 * @param int $level
2712
	 * @return SiteTree
2713
	 */
2714
	public function Level($level) {
2715
		$parent = $this;
2716
		$stack = array($parent);
2717
		while($parent = $parent->Parent) {
2718
			array_unshift($stack, $parent);
2719
		}
2720
2721
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2722
	}
2723
2724
	/**
2725
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2726
	 *
2727
	 * @return int
2728
	 */
2729
	public function getPageLevel() {
2730
		if($this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
2731
			return 1 + $this->Parent()->getPageLevel();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SiteTree. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2732
		}
2733
		return 1;
2734
	}
2735
2736
	/**
2737
	 * Return the CSS classes to apply to this node in the CMS tree.
2738
	 *
2739
	 * @param string $numChildrenMethod
2740
	 * @return string
2741
	 */
2742
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2743
		$classes = sprintf('class-%s', $this->class);
2744
		if($this->HasBrokenFile || $this->HasBrokenLink) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenFile does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
Documentation introduced by
The property HasBrokenLink does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
2745
			$classes .= " BrokenLink";
2746
		}
2747
2748
		if(!$this->canAddChildren()) {
2749
			$classes .= " nochildren";
2750
		}
2751
2752
		if(!$this->canEdit() && !$this->canAddChildren()) {
2753
			if (!$this->canView()) {
2754
				$classes .= " disabled";
2755
			} else {
2756
				$classes .= " edit-disabled";
2757
			}
2758
		}
2759
2760
		if(!$this->ShowInMenus) {
2761
			$classes .= " notinmenu";
2762
		}
2763
			
2764
		//TODO: Add integration
2765
		/*
2766
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2767
			$classes .= " untranslated ";
2768
		*/
2769
		$classes .= $this->markingClasses($numChildrenMethod);
0 ignored issues
show
Documentation Bug introduced by
The method markingClasses does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2770
2771
		return $classes;
2772
	}
2773
2774
	/**
2775
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2776
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2777
	 *
2778
	 * @return bool
2779
	 */
2780
	public function getIsDeletedFromStage() {
2781
		if(!$this->ID) return true;
2782
		if($this->isNew()) return false;
2783
2784
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2785
2786
		// Return true for both completely deleted pages and for pages just deleted from stage
2787
		return !($stageVersion);
2788
	}
2789
	
2790
	/**
2791
	 * Return true if this page exists on the live site
2792
	 *
2793
	 * @return bool
2794
	 */
2795
	public function getExistsOnLive() {
2796
		return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2797
	}
2798
2799
	/**
2800
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2801
	 * unpublished changes to the draft site.
2802
	 *
2803
	 * @return bool
2804
	 */
2805
	public function getIsModifiedOnStage() {
2806
		// New unsaved pages could be never be published
2807
		if($this->isNew()) return false;
2808
		
2809
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2810
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2811
		
2812
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2813
		$this->extend('getIsModifiedOnStage', $isModified);
2814
		
2815
		return $isModified;
2816
	}
2817
	
2818
	/**
2819
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2820
	 * published.
2821
	 *
2822
	 * @return bool
2823
	 */
2824
	public function getIsAddedToStage() {
2825
		// New unsaved pages could be never be published
2826
		if($this->isNew()) return false;
2827
		
2828
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2829
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2830
2831
		return ($stageVersion && !$liveVersion);
2832
	}
2833
	
2834
	/**
2835
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2836
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2837
	 */
2838
	static public function disableCMSFieldsExtensions() {
2839
		self::$runCMSFieldsExtensions = false;
2840
	}
2841
	
2842
	/**
2843
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2844
	 * disableCMSFieldsExtensions().
2845
	 */
2846
	static public function enableCMSFieldsExtensions() {
2847
		self::$runCMSFieldsExtensions = true;
2848
	}
2849
2850
	public function providePermissions() {
2851
		return array(
2852
			'SITETREE_GRANT_ACCESS' => array(
2853
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2854
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2855
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2856
				'sort' => 100
2857
			),
2858
			'SITETREE_VIEW_ALL' => array(
2859
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2860
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2861
				'sort' => -100,
2862
				'help' => _t('SiteTree.VIEW_ALL_HELP', 'Ability to view any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
2863
			),
2864
			'SITETREE_EDIT_ALL' => array(
2865
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2866
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2867
				'sort' => -50,
2868
				'help' => _t('SiteTree.EDIT_ALL_HELP', 'Ability to edit any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
2869
			),
2870
			'SITETREE_REORGANISE' => array(
2871
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2872
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2873
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2874
				'sort' => 100
2875
			),
2876
			'VIEW_DRAFT_CONTENT' => array(
2877
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2878
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2879
				'help' => _t('SiteTree.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
2880
				'sort' => 100
2881
			)
2882
		);
2883
	}
2884
	
2885
	/**
2886
	 * Return the translated Singular name.
2887
	 *
2888
	 * @return string
2889
	 */
2890
	public function i18n_singular_name() {
2891
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2892
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2893
		return _t($class.'.SINGULARNAME', $this->singular_name());
2894
	}
2895
	
2896
	/**
2897
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2898
	 * picks it up for the wrong folder.
2899
	 *
2900
	 * @return array
2901
	 */
2902
	public function provideI18nEntities() {
2903
		$entities = parent::provideI18nEntities();
2904
		
2905
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2906
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;		
2907
2908
		$entities[$this->class . '.DESCRIPTION'] = array(
2909
			$this->stat('description'),
2910
			'Description of the page type (shown in the "add page" dialog)'
2911
		);
2912
2913
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2914
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2915
2916
		return $entities;
0 ignored issues
show
Best Practice introduced by
The expression return $entities; seems to be an array, but some of its elements' types (null) are incompatible with the return type of the parent method DataObject::provideI18nEntities of type array<*,array<array|inte...double|string|boolean>>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2917
	}
2918
2919
	/**
2920
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2921
	 *
2922
	 * @return string
2923
	 */
2924
	public function getParentType() {
2925
		return $this->ParentID == 0 ? 'root' : 'subpage';
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
2926
	}
2927
2928
	/**
2929
	 * Clear the permissions cache for SiteTree
2930
	 */
2931
	public static function reset() {
2932
		self::$cache_permissions = array();
2933
	}
2934
	
2935
	static public function on_db_reset() {
2936
		self::$cache_permissions = array();
2937
	}
2938
2939
}
2940