Completed
Pull Request — master (#1399)
by Ingo
02:39
created

SiteTree::canDeleteFromLive()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 12
rs 9.4285
cc 2
eloc 7
nc 2
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 $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...
136
		"Breadcrumbs" => "HTMLText",
137
		"LastEdited" => "SS_Datetime",
138
		"Created" => "SS_Datetime",
139
		'Link' => 'Text',
140
		'RelativeLink' => 'Text',
141
		'AbsoluteLink' => 'Text',
142
		'TreeTitle' => 'HTMLText',
143
	);
144
145
	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...
146
		"ShowInMenus" => 1,
147
		"ShowInSearch" => 1,
148
		"CanViewType" => "Inherit",
149
		"CanEditType" => "Inherit"
150
	);
151
152
	private static $versioning = array(
153
		"Stage",  "Live"
154
	);
155
156
	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...
157
158
	/**
159
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
160
	 * @var boolean
161
	 * @config
162
	 */
163
	private static $can_create = true;
164
165
	/**
166
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
167
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
168
	 *
169
	 * @see CMSMain::generateTreeStylingCSS()
170
	 * @config
171
	 * @var string
172
	 */
173
	private static $icon = null;
174
	
175
	/**
176
	 * @config
177
	 * @var string Description of the class functionality, typically shown to a user
178
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
179
	 */
180
	private static $description = 'Generic content page';
181
182
	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...
183
		"Hierarchy",
184
		"Versioned('Stage', 'Live')",
185
		"SiteTreeLinkTracking"
186
	);
187
	
188
	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...
189
		'Title',
190
		'Content',
191
	);
192
193
	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...
194
		'URLSegment' => 'URL'
195
	);
196
	
197
	/**
198
	 * @config
199
	 */
200
	private static $nested_urls = true;
201
	
202
	/**
203
	 * @config
204
	*/
205
	private static $create_default_pages = true;
206
	
207
	/**
208
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
209
	 */
210
	private static $runCMSFieldsExtensions = true;
211
	
212
	/**
213
	 * Cache for canView/Edit/Publish/Delete permissions.
214
	 * Keyed by permission type (e.g. 'edit'), with an array
215
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
216
	 * See {@link batch_permission_check()} for details.
217
	 */
218
	private static $cache_permissions = array();
219
220
	/**
221
	 * @config
222
	 * @var boolean
223
	 */
224
	private static $enforce_strict_hierarchy = true;
225
226
	/**
227
	 * The value used for the meta generator tag. Leave blank to omit the tag.
228
	 *
229
	 * @config
230
	 * @var string
231
	 */
232
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
233
234
	protected $_cache_statusFlags = null;
235
	
236
	/**
237
	 * Determines if the system should avoid orphaned pages
238
	 * by deleting all children when the their parent is deleted (TRUE),
239
	 * or rather preserve this data even if its not reachable through any navigation path (FALSE).
240
	 *
241
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
242
	 * @param boolean
243
	 */
244
	static public function set_enforce_strict_hierarchy($to) {
245
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
246
		Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', $to);
247
	}
248
	
249
	/**
250
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
251
	 * @return boolean
252
	 */
253
	static public function get_enforce_strict_hierarchy() {
254
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
255
		return Config::inst()->get('SiteTree', 'enforce_strict_hierarchy');
256
	}
257
258
	/**
259
	 * Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
260
	 *
261
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
262
	 * @return bool
263
	 */
264
	static public function nested_urls() {
265
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
266
		return Config::inst()->get('SiteTree', 'nested_urls');
267
	}
268
	
269
	/**
270
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
271
	 */
272
	static public function enable_nested_urls() {
273
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
274
		Config::inst()->update('SiteTree', 'nested_urls', true);
275
	}
276
	
277
	/**
278
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
279
	 */
280
	static public function disable_nested_urls() {
281
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
282
		Config::inst()->update('SiteTree', 'nested_urls', false);
283
	}
284
	
285
	/**
286
	 * Set the (re)creation of default pages on /dev/build
287
	 *
288
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
289
	 * @param bool $option
290
	 */
291
	static public function set_create_default_pages($option = true) {
292
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
293
		Config::inst()->update('SiteTree', 'create_default_pages', $option);
294
	}
295
296
	/**
297
	 * Return true if default pages should be created on /dev/build.
298
	 *
299
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
300
	 * @return bool
301
	 */
302
	static public function get_create_default_pages() {
303
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
304
		return Config::inst()->get('SiteTree', 'create_default_pages');
305
	}
306
	
307
	/**
308
	 * Fetches the {@link SiteTree} object that maps to a link.
309
	 *
310
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
311
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
312
	 *
313
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
314
	 * by a extension attached to {@link SiteTree}
315
	 *
316
	 * @param string $link  The link of the page to search for
317
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
318
	 * @return SiteTree
319
	 */
320
	static public function get_by_link($link, $cache = true) {
321
		if(trim($link, '/')) {
322
			$link = trim(Director::makeRelative($link), '/');
323
		} else {
324
			$link = RootURLController::get_homepage_link();
325
		}
326
		
327
		$parts = preg_split('|/+|', $link);
328
		
329
		// Grab the initial root level page to traverse down from.
330
		$URLSegment = array_shift($parts);
331
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
332
		if(self::config()->nested_urls) {
333
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
334
		}
335
		$sitetree = DataObject::get_one('SiteTree', $conditions, $cache);
336
		
337
		/// Fall back on a unique URLSegment for b/c.
338
		if(	!$sitetree
339
			&& self::config()->nested_urls
340
			&& $page = DataObject::get_one('SiteTree', array(
341
				'"SiteTree"."URLSegment"' => $URLSegment
342
			), $cache)
343
		) {
344
			return $page;
345
		}
346
		
347
		// Attempt to grab an alternative page from extensions.
348
		if(!$sitetree) {
349
			$parentID = self::config()->nested_urls ? 0 : null;
350
			
351 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...
352
				foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
353
			}
354
			
355
			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...
356
		}
357
		
358
		// Check if we have any more URL parts to parse.
359
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
360
		
361
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
362
		foreach($parts as $segment) {
363
			$next = DataObject::get_one('SiteTree', array(
364
					'"SiteTree"."URLSegment"' => $segment,
365
					'"SiteTree"."ParentID"' => $sitetree->ID
366
				),
367
				$cache
368
			);
369
			
370
			if(!$next) {
371
				$parentID = (int) $sitetree->ID;
372
				
373 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...
374
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
375
				}
376
				
377
				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...
378
			}
379
			
380
			$sitetree->destroy();
381
			$sitetree = $next;
382
		}
383
		
384
		return $sitetree;
385
	}
386
	
387
	/**
388
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
389
	 *
390
	 * @return array
391
	 */
392
	public static function page_type_classes() {
393
		$classes = ClassInfo::getValidSubClasses();
394
395
		$baseClassIndex = array_search('SiteTree', $classes);
396
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
397
398
		$kill_ancestors = array();
399
400
		// figure out if there are any classes we don't want to appear
401
		foreach($classes as $class) {
402
			$instance = singleton($class);
403
404
			// do any of the progeny want to hide an ancestor?
405
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
406
				// note for killing later
407
				$kill_ancestors[] = $ancestor_to_hide;
408
			}
409
		}
410
411
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
412
		// requirements
413
		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...
414
			$kill_ancestors = array_unique($kill_ancestors);
415
			foreach($kill_ancestors as $mark) {
416
				// unset from $classes
417
				$idx = array_search($mark, $classes, true);
418
				if ($idx !== false) {
419
					unset($classes[$idx]);
420
				}
421
			}
422
		}
423
424
		return $classes;
425
	}
426
	
427
	/**
428
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
429
	 *
430
	 * @param array      $arguments
431
	 * @param string     $content
432
	 * @param TextParser $parser
433
	 * @return string
434
	 */
435
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
436
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
437
		
438
		if (
439
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
440
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
441
		) {
442
			 return; // There were no suitable matches at all.
443
		}
444
445
		$link = Convert::raw2att($page->Link());
446
		
447
		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...
448
			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...
449
		} else {
450
			return $link;
451
		}
452
	}
453
454
	/**
455
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
456
	 *
457
	 * @param string $action Optional controller action (method).
458
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
459
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
460
	 *                       append GET parameters.
461
	 * @return string
462
	 */
463
	public function Link($action = null) {
464
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
465
	}
466
	
467
	/**
468
	 * Get the absolute URL for this page, including protocol and host.
469
	 *
470
	 * @param string $action See {@link Link()}
471
	 * @return string
472
	 */
473
	public function AbsoluteLink($action = null) {
474
		if($this->hasMethod('alternateAbsoluteLink')) {
475
			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...
476
		} else {
477
			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 477 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...
478
		}
479
	}
480
	
481
	/**
482
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
483
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
484
	 *
485
	 * @param string $action See {@link Link()}
486
	 * @return string
487
	 */
488
	public function PreviewLink($action = null) {
489
		if($this->hasMethod('alternatePreviewLink')) {
490
			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...
491
		} else {
492
			return $this->AbsoluteLink($action);
493
		}
494
	}
495
	
496
	/**
497
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
498
	 *
499
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
500
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
501
	 * and returned in its full form.
502
	 *
503
	 * @uses RootURLController::get_homepage_link()
504
	 *
505
	 * @param string $action See {@link Link()}
506
	 * @return string
507
	 */
508
	public function RelativeLink($action = null) {
509
		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...
510
			$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...
511
			// If page is removed select parent from version history (for archive page view)
512
			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...
513
				$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...
514
			}
515
			$base = $parent->RelativeLink($this->URLSegment);
516
		} 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...
517
			// Unset base for root-level homepages.
518
			// Note: Homepages with action parameters (or $action === true)
519
			// need to retain their URLSegment.
520
			$base = null;
521
		} else {
522
			$base = $this->URLSegment;
523
		}
524
		
525
		$this->extend('updateRelativeLink', $base, $action);
526
		
527
		// Legacy support: If $action === true, retain URLSegment for homepages,
528
		// but don't append any action
529
		if($action === true) $action = null;
530
531
		return Controller::join_links($base, '/', $action);
532
	}
533
534
	/**
535
	 * Get the absolute URL for this page on the Live site.
536
	 *
537
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
538
	 * @return string
539
	 */
540
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
541
		$oldStage = Versioned::current_stage();
542
		Versioned::reading_stage('Live');
543
		$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...
544
			'"SiteTree"."ID"' => $this->ID
545
		));
546
		if($live) {
547
			$link = $live->AbsoluteLink();
548
			if($includeStageEqualsLive) $link .= '?stage=Live';
549
		} else {
550
			$link = null;
551
		}
552
553
		Versioned::reading_stage($oldStage);
554
		return $link;
555
	}
556
	
557
	/**
558
	 * Generates a link to edit this page in the CMS.
559
	 *
560
	 * @return string
561
	 */
562
	public function CMSEditLink() {
563
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
564
	}
565
	
566
		
567
	/**
568
	 * Return a CSS identifier generated from this page's link.
569
	 *
570
	 * @return string The URL segment
571
	 */
572
	public function ElementName() {
573
		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...
574
	}
575
	
576
	/**
577
	 * Returns true if this is the currently active page being used to handle this request.
578
	 *
579
	 * @return bool
580
	 */
581
	public function isCurrent() {
582
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
583
	}
584
	
585
	/**
586
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
587
	 * currently being viewed).
588
	 *
589
	 * @return bool
590
	 */
591
	public function isSection() {
592
		return $this->isCurrent() || (
593
			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...
594
		);
595
	}
596
	
597
	/**
598
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
599
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
600
	 * to external users.
601
	 *
602
	 * @return bool
603
	 */
604
	public function isOrphaned() {
605
		// Always false for root pages
606
		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...
607
		
608
		// Parent must exist and not be an orphan itself
609
		$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...
610
		return !$parent || !$parent->exists() || $parent->isOrphaned();
611
	}
612
	
613
	/**
614
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
615
	 *
616
	 * @return string
617
	 */
618
	public function LinkOrCurrent() {
619
		return $this->isCurrent() ? 'current' : 'link';
620
	}
621
	
622
	/**
623
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
624
	 *
625
	 * @return string
626
	 */
627
	public function LinkOrSection() {
628
		return $this->isSection() ? 'section' : 'link';
629
	}
630
	
631
	/**
632
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
633
	 * but in the current section.
634
	 *
635
	 * @return string
636
	 */
637
	public function LinkingMode() {
638
		if($this->isCurrent()) {
639
			return 'current';
640
		} elseif($this->isSection()) {
641
			return 'section';
642
		} else {
643
			return 'link';
644
		}
645
	}
646
	
647
	/**
648
	 * Check if this page is in the given current section.
649
	 *
650
	 * @param string $sectionName Name of the section to check
651
	 * @return bool True if we are in the given section
652
	 */
653
	public function InSection($sectionName) {
654
		$page = Director::get_current_page();
655
		while($page) {
656
			if($sectionName == $page->URLSegment)
657
				return true;
658
			$page = $page->Parent;
659
		}
660
		return false;
661
	}
662
663
	/**
664
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
665
	 * such behaviour.
666
	 *
667
	 * @param bool $doWrite Whether to write the new object before returning it
668
	 * @return self The duplicated object
669
	 */
670
	 public function duplicate($doWrite = true) {
671
		
672
		$page = parent::duplicate(false);
673
		$page->Sort = 0;
674
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
675
		
676
		if($doWrite) {
677
			$page->write();
678
679
			$page = $this->duplicateManyManyRelations($this, $page);
680
		}
681
		$this->invokeWithExtensions('onAfterDuplicate', $page);
682
		
683
		return $page;
684
	}
685
686
	/**
687
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
688
	 *
689
	 * @return self The duplicated object
690
	 */
691
	public function duplicateWithChildren() {
692
		$clone = $this->duplicate();
693
		$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...
694
695
		if($children) {
696
			foreach($children as $child) {
697
				$childClone = method_exists($child, 'duplicateWithChildren')
698
					? $child->duplicateWithChildren()
699
					: $child->duplicate();
700
				$childClone->ParentID = $clone->ID;
701
				$childClone->write();
702
			}
703
		}
704
705
		return $clone;
706
	}
707
708
	/**
709
	 * Duplicate this node and its children as a child of the node with the given ID
710
	 *
711
	 * @param int $id ID of the new node's new parent
712
	 */
713
	public function duplicateAsChild($id) {
714
		$newSiteTree = $this->duplicate();
715
		$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...
716
		$newSiteTree->Sort = 0;
717
		$newSiteTree->write();
718
	}
719
	
720
	/**
721
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
722
	 *
723
	 * @param int $maxDepth The maximum depth to traverse.
724
	 * @param boolean $unlinked Whether to link page titles.
725
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
726
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
727
	 * @return HTMLText The breadcrumb trail.
728
	 */
729
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
730
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
731
		$template = new SSViewer('BreadcrumbsTemplate');
732
		return $template->process($this->customise(new ArrayData(array(
733
			"Pages" => $pages,
734
			"Unlinked" => $unlinked
735
		))));
736
	}
737
738
739
	/**
740
	 * Returns a list of breadcrumbs for the current page.
741
	 *
742
	 * @param int $maxDepth The maximum depth to traverse.
743
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
744
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
745
	 *
746
	 * @return ArrayList
747
	*/
748
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
749
		$page = $this;
750
		$pages = array();
751
		
752
		while(
753
			$page
754
 			&& (!$maxDepth || count($pages) < $maxDepth)
755
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
756
 		) {
757
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
758
				$pages[] = $page;
759
			}
760
			
761
			$page = $page->Parent;
762
		}
763
764
		return new ArrayList(array_reverse($pages));
765
	}
766
767
768
	/**
769
	 * Make this page a child of another page.
770
	 *
771
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
772
	 *
773
	 * @param SiteTree|int $item Either the parent object, or the parent ID
774
	 */
775
	public function setParent($item) {
776
		if(is_object($item)) {
777
			if (!$item->exists()) $item->write();
778
			$this->setField("ParentID", $item->ID);
779
		} else {
780
			$this->setField("ParentID", $item);
781
		}
782
	}
783
 	
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...
784
	/**
785
	 * Get the parent of this page.
786
	 *
787
	 * @return SiteTree Parent of this page
788
	 */
789
	public function getParent() {
790
		if ($parentID = $this->getField("ParentID")) {
791
			return DataObject::get_by_id("SiteTree", $parentID);
792
		}
793
	}
794
795
	/**
796
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
797
	 *
798
	 * @param int $level The maximum amount of levels to traverse.
799
	 * @param string $separator Seperating string
800
	 * @return string The resulting string
801
	 */
802
	public function NestedTitle($level = 2, $separator = " - ") {
803
		$item = $this;
804
		while($item && $level > 0) {
805
			$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...
806
			$item = $item->Parent;
807
			$level--;
808
		}
809
		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...
810
	}
811
812
	/**
813
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
814
	 * the security model for an application.
815
	 *
816
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
817
	 * - Checks for existence of a method named "can<$perm>()" on the object
818
	 * - Calls decorators and only returns for FALSE "vetoes"
819
	 * - Falls back to {@link Permission::check()}
820
	 * - Does NOT check for many-many relations named "Can<$perm>"
821
	 *
822
	 * @uses DataObjectDecorator->can()
823
	 *
824
	 * @param string $perm The permission to be checked, such as 'View'
825
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
826
	 * @return bool True if the the member is allowed to do the given action
827
	 */
828
	public function can($perm, $member = null) {
829 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...
830
			$member = Member::currentUserID();
831
		}
832
833
		if($member && Permission::checkMember($member, "ADMIN")) return true;
834
		
835
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
836
			$method = 'can' . ucfirst($perm);
837
			return $this->$method($member);
838
		}
839
		
840
		$results = $this->extend('can', $member);
841
		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...
842
843
		return ($member && Permission::checkMember($member, $perm));
844
	}
845
846
	/**
847
	 * This function should return true if the current user can add children to this page. It can be overloaded to
848
	 * customise the security model for an application.
849
	 *
850
	 * Denies permission if any of the following conditions is true:
851
	 * - alternateCanAddChildren() on a extension returns false
852
	 * - canEdit() is not granted
853
	 * - There are no classes defined in {@link $allowed_children}
854
	 *
855
	 * @uses SiteTreeExtension->canAddChildren()
856
	 * @uses canEdit()
857
	 * @uses $allowed_children
858
	 *
859
	 * @param Member|int $member
860
	 * @return bool True if the current user can add children
861
	 */
862
	public function canAddChildren($member = null) {
863
		// Disable adding children to archived pages
864
		if($this->getIsDeletedFromStage()) {
865
			return false;
866
		}
867
868 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...
869
			$member = Member::currentUserID();
870
		}
871
872
		if($member && Permission::checkMember($member, "ADMIN")) return true;
873
		
874
		// Standard mechanism for accepting permission changes from extensions
875
		$extended = $this->extendedCan('canAddChildren', $member);
876
		if($extended !== null) return $extended;
877
		
878
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
879
	}
880
881
	/**
882
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
883
	 * security model for an application.
884
	 *
885
	 * Denies permission if any of the following conditions is true:
886
	 * - canView() on any extension returns false
887
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
888
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
889
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
890
	 *
891
	 * @uses DataExtension->canView()
892
	 * @uses ViewerGroups()
893
	 *
894
	 * @param Member|int $member
895
	 * @return bool True if the current user can view this page
896
	 */
897
	public function canView($member = null) {
898 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...
899
			$member = Member::currentUserID();
900
		}
901
902
		// admin override
903
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
904
		
905
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
906
		if($this->isOrphaned()) return false;
907
908
		// Standard mechanism for accepting permission changes from extensions
909
		$extended = $this->extendedCan('canView', $member);
910
		if($extended !== null) return $extended;
911
		
912
		// check for empty spec
913
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
914
915
		// check for inherit
916
		if($this->CanViewType == 'Inherit') {
917
			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...
918
			else return $this->getSiteConfig()->canViewPages($member);
919
		}
920
		
921
		// check for any logged-in users
922
		if($this->CanViewType == 'LoggedInUsers' && $member) {
923
			return true;
924
		}
925
		
926
		// check for specific groups
927
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
928
		if(
929
			$this->CanViewType == 'OnlyTheseUsers'
930
			&& $member
931
			&& $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...
932
		) return true;
933
		
934
		return false;
935
	}
936
937
	/**
938
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
939
	 * security model for an application.
940
	 *
941
	 * Denies permission if any of the following conditions is true:
942
	 * - canDelete() returns false on any extension
943
	 * - canEdit() returns false
944
	 * - any descendant page returns false for canDelete()
945
	 *
946
	 * @uses canDelete()
947
	 * @uses SiteTreeExtension->canDelete()
948
	 * @uses canEdit()
949
	 *
950
	 * @param Member $member
951
	 * @return bool True if the current user can delete this page
952
	 */
953
	public function canDelete($member = null) {
954 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...
955
		else if(is_numeric($member)) $memberID = $member;
956
		else $memberID = Member::currentUserID();
957
		
958
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
959
			return true;
960
		}
961
		
962
		// Standard mechanism for accepting permission changes from extensions
963
		$extended = $this->extendedCan('canDelete', $memberID);
964
		if($extended !== null) return $extended;
965
				
966
		// Regular canEdit logic is handled by can_edit_multiple
967
		$results = self::can_delete_multiple(array($this->ID), $memberID);
968
		
969
		// If this page no longer exists in stage/live results won't contain the page.
970
		// Fail-over to false
971
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
972
	}
973
974
	/**
975
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
976
	 * can be overloaded to customise the security model for an application.
977
	 *
978
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
979
	 * create beneath a parent is based on the ability to edit that parent page.
980
	 *
981
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
982
	 *
983
	 * @uses $can_create
984
	 * @uses DataExtension->canCreate()
985
	 *
986
	 * @param Member $member
987
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
988
	 *                       If a parent page is known, it will be checked for validity.
989
	 *                       If omitted, it will be assumed this is to be created as a top level page.
990
	 * @return bool True if the current user can create pages on this class.
991
	 */
992
	public function canCreate($member = null, $context = array()) {
993 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...
994
			$member = Member::currentUserID();
995
		}
996
997
		// Check parent (custom canCreate option for SiteTree)
998
		// Block children not allowed for this parent type
999
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1000
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1001
			return false;
1002
		}
1003
1004
		// Check permission
1005
		if($member && Permission::checkMember($member, "ADMIN")) {
1006
			return true;
1007
		}
1008
1009
		// Standard mechanism for accepting permission changes from extensions
1010
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1011
		if($extended !== null) {
1012
			return $extended;
1013
		}
1014
1015
		// Fall over to inherited permissions
1016
		if($parent) {
1017
			return $parent->canAddChildren($member);
1018
		} else {
1019
			// This doesn't necessarily mean we are creating a root page, but that
1020
			// we don't know if there is a parent, so default to this permission
1021
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1022
		}
1023
	}
1024
1025
	/**
1026
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1027
	 * security model for an application.
1028
	 *
1029
	 * Denies permission if any of the following conditions is true:
1030
	 * - canEdit() on any extension returns false
1031
	 * - canView() return false
1032
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1033
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1034
	 *   CMS_Access_CMSMAIN permission code
1035
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1036
	 *
1037
	 * @uses canView()
1038
	 * @uses EditorGroups()
1039
	 * @uses DataExtension->canEdit()
1040
	 *
1041
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1042
	 *                       unit tests)
1043
	 * @return bool True if the current user can edit this page
1044
	 */
1045
	public function canEdit($member = null) {
1046 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...
1047
		else if(is_numeric($member)) $memberID = $member;
1048
		else $memberID = Member::currentUserID();
1049
		
1050
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
1051
		
1052
		// Standard mechanism for accepting permission changes from extensions
1053
		$extended = $this->extendedCan('canEdit', $memberID);
1054
		if($extended !== null) return $extended;
1055
1056
		if($this->ID) {
1057
			// Regular canEdit logic is handled by can_edit_multiple
1058
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1059
1060
			// If this page no longer exists in stage/live results won't contain the page.
1061
			// Fail-over to false
1062
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1063
			
1064
		// Default for unsaved pages
1065
		} else {
1066
			return $this->getSiteConfig()->canEditPages($member);
1067
		}
1068
	}
1069
1070
	/**
1071
	 * @deprecated
1072
	 */
1073
	public function canDeleteFromLive($member = null) {
1074
		Deprecation::notice('4.0', 'Use canUnpublish');
1075
1076
		// Deprecated extension
1077
		$extended = $this->extendedCan('canDeleteFromLive', $member);
1078
		if($extended !== null) {
1079
			Deprecation::notice('4.0', 'Use canUnpublish in your extension instead');
1080
			return $extended;
1081
		}
1082
1083
		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...
1084
	}
1085
1086
	/**
1087
	 * Stub method to get the site config, unless the current class can provide an alternate.
1088
	 *
1089
	 * @return SiteConfig
1090
	 */
1091
	public function getSiteConfig() {
1092
		
1093
		if($this->hasMethod('alternateSiteConfig')) {
1094
			$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...
1095
			if($altConfig) return $altConfig;
1096
		}
1097
		
1098
		return SiteConfig::current_site_config();
1099
	}
1100
1101
	/**
1102
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1103
	 * can_(perm)_multiple method for efficiency.
1104
	 *
1105
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1106
	 * @param array           $ids           An array of page IDs
1107
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1108
	 *                                       to 'SiteTree::can_(permission)_multiple'
1109
	 */
1110
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1111
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1112
		
1113
		if(is_callable($batchCallback)) {
1114
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1115
		} else {
1116
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1117
				. "with callback '$batchCallback'", E_USER_WARNING);
1118
		}
1119
	}
1120
1121
	/**
1122
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1123
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1124
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1125
	 * efficiently.
1126
	 *
1127
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1128
	 * property to FALSE.
1129
	 *
1130
	 * @param array  $ids              Of {@link SiteTree} IDs
1131
	 * @param int    $memberID         Member ID
1132
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1133
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1134
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1135
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1136
	 * @param bool   $useCached
1137
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1138
	 */
1139
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1140
												  $globalPermission = null, $useCached = true) {
1141
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1142
1143
		// Sanitise the IDs
1144
		$ids = array_filter($ids, 'is_numeric');
1145
		
1146
		// This is the name used on the permission cache
1147
		// converts something like 'CanEditType' to 'edit'.
1148
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1149
1150
		// Default result: nothing editable
1151
		$result = array_fill_keys($ids, false);
1152
		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...
1153
1154
			// Look in the cache for values
1155
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1156
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1157
			
1158
				// If we can't find everything in the cache, then look up the remainder separately
1159
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1160
				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...
1161
					$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 1141 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...
1162
				}
1163
				return $cachedValues;
1164
			}
1165
		
1166
			// If a member doesn't have a certain permission then they can't edit anything
1167
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1168
				return $result;
1169
			}
1170
1171
			// Placeholder for parameterised ID list
1172
			$idPlaceholders = DB::placeholders($ids);
1173
1174
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1175
			// be enabled
1176
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1177
		
1178
			// Get the groups that the given member belongs to
1179
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1180
			$SQL_groupList = implode(", ", $groupIDs);
1181
			if (!$SQL_groupList) $SQL_groupList = '0';
1182
			
1183
			$combinedStageResult = array();
1184
1185
			foreach(array('Stage', 'Live') as $stage) {
1186
				// Start by filling the array with the pages that actually exist
1187
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1188
				
1189
				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...
1190
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1191
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1192
				} else {
1193
					$stageIds = array();
1194
				}
1195
				$result = array_fill_keys($stageIds, false);
1196
				
1197
				// Get the uninherited permissions
1198
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1199
					->where(array(
1200
						"(\"$typeField\" = 'LoggedInUsers' OR
1201
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1202
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1203
						=> $ids
1204
					))
1205
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1206
				
1207
				if($uninheritedPermissions) {
1208
					// Set all the relevant items in $result to true
1209
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1210
				}
1211
1212
				// Get permissions that are inherited
1213
				$potentiallyInherited = Versioned::get_by_stage(
1214
					"SiteTree",
1215
					$stage,
1216
					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...
1217
				);
1218
1219
				if($potentiallyInherited) {
1220
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1221
					// then see which ones the user has permission on
1222
					$groupedByParent = array();
1223
					foreach($potentiallyInherited as $item) {
1224
						if($item->ParentID) {
1225
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1226
							$groupedByParent[$item->ParentID][] = $item->ID;
1227
						} else {
1228
							// Might return different site config based on record context, e.g. when subsites module
1229
							// is used
1230
							$siteConfig = $item->getSiteConfig();
1231
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1232
						}
1233
					}
1234
1235
					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...
1236
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1237
						if($actuallyInherited) {
1238
							$parentIDs = array_keys(array_filter($actuallyInherited));
1239
							foreach($parentIDs as $parentID) {
1240
								// Set all the relevant items in $result to true
1241
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1242
							}
1243
						}
1244
					}
1245
				}
1246
				
1247
				$combinedStageResult = $combinedStageResult + $result;
1248
				
1249
			}
1250
		}
1251
1252
		if(isset($combinedStageResult)) {
1253
			// Cache the results
1254
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1255
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1256
1257
			return $combinedStageResult;
1258
		} else {
1259
			return array();
1260
		}
1261
	}
1262
1263
	/**
1264
	 * Get the 'can edit' information for a number of SiteTree pages.
1265
	 *
1266
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1267
	 * @param int   $memberID  ID of member
1268
	 * @param bool  $useCached Return values from the permission cache if they exist
1269
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1270
	 *                         edited
1271
	 */
1272
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1273
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1274
	}
1275
1276
	/**
1277
	 * Get the 'can edit' information for a number of SiteTree pages.
1278
	 *
1279
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1280
	 * @param int   $memberID  ID of member
1281
	 * @param bool  $useCached Return values from the permission cache if they exist
1282
	 * @return array
1283
	 */
1284
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1285
		$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...
1286
		$result = array_fill_keys($ids, false);
1287
		$cacheKey = "delete-$memberID";
1288
		
1289
		// Look in the cache for values
1290
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1291
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1292
			
1293
			// If we can't find everything in the cache, then look up the remainder separately
1294
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1295
			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...
1296
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1297
					+ $cachedValues;
1298
			}
1299
			return $cachedValues;
1300
		}
1301
1302
		// You can only delete pages that you can edit
1303
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1304
		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...
1305
		
1306
			// You can only delete pages whose children you can delete
1307
			$editablePlaceholders = DB::placeholders($editableIDs);
1308
			$childRecords = SiteTree::get()->where(array(
1309
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1310
			));
1311
			if($childRecords) {
1312
				$children = $childRecords->map("ID", "ParentID");
1313
1314
				// Find out the children that can be deleted
1315
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1316
				
1317
				// Get a list of all the parents that have no undeletable children
1318
				$deletableParents = array_fill_keys($editableIDs, true);
1319
				foreach($deletableChildren as $id => $canDelete) {
1320
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1321
				}
1322
1323
				// Use that to filter the list of deletable parents that have children
1324
				$deletableParents = array_keys($deletableParents);
1325
1326
				// Also get the $ids that don't have children
1327
				$parents = array_unique($children->values());
1328
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1329
1330
				// Combine the two
1331
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1332
1333
			} else {
1334
				$deletable = $editableIDs;
1335
			}
1336
		} else {
1337
			$deletable = array();
1338
		}
1339
		
1340
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1341
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1342
	}
1343
1344
	/**
1345
	 * Collate selected descendants of this page.
1346
	 *
1347
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1348
	 * $collator array.
1349
	 *
1350
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1351
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1352
	 * @return bool
1353
	 */
1354
	public function collateDescendants($condition, &$collator) {
1355
		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...
1356
			foreach($children as $item) {
1357
				if(eval("return $condition;")) $collator[] = $item;
1358
				$item->collateDescendants($condition, $collator);
1359
			}
1360
			return true;
1361
		}
1362
	}
1363
1364
	/**
1365
	 * Return the title, description, keywords and language metatags.
1366
	 *
1367
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1368
	 *
1369
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1370
	 * @return string The XHTML metatags
1371
	 */
1372
	public function MetaTags($includeTitle = true) {
1373
		$tags = "";
1374
		if($includeTitle === true || $includeTitle == 'true') {
1375
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1376
		}
1377
1378
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1379
		if (!empty($generator)) {
1380
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1381
		}
1382
1383
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1384
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1385
		if($this->MetaDescription) {
1386
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1387
		}
1388
		if($this->ExtraMeta) {
1389
			$tags .= $this->ExtraMeta . "\n";
1390
		}
1391
		
1392
		if(Permission::check('CMS_ACCESS_CMSMain')
1393
			&& in_array('CMSPreviewable', class_implements($this))
1394
			&& !$this instanceof ErrorPage
1395
			&& $this->ID > 0
1396
		) {
1397
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1398
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1399
		}
1400
1401
		$this->extend('MetaTags', $tags);
1402
1403
		return $tags;
1404
	}
1405
1406
	/**
1407
	 * Returns the object that contains the content that a user would associate with this page.
1408
	 *
1409
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1410
	 * return the page that is linked to.
1411
	 *
1412
	 * @return $this
1413
	 */
1414
	public function ContentSource() {
1415
		return $this;
1416
	}
1417
1418
	/**
1419
	 * Add default records to database.
1420
	 *
1421
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1422
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1423
	 */
1424
	public function requireDefaultRecords() {
1425
		parent::requireDefaultRecords();
1426
		
1427
		// default pages
1428
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1429
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1430
				$homepage = new Page();
1431
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1432
				$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>');
1433
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1434
				$homepage->Sort = 1;
1435
				$homepage->write();
1436
				$homepage->publish('Stage', 'Live');
1437
				$homepage->flushCache();
1438
				DB::alteration_message('Home page created', 'created');
1439
			}
1440
1441
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1442
				$aboutus = new Page();
1443
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1444
				$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>');
1445
				$aboutus->Sort = 2;
1446
				$aboutus->write();
1447
				$aboutus->publish('Stage', 'Live');
1448
				$aboutus->flushCache();
1449
				DB::alteration_message('About Us page created', 'created');
1450
1451
				$contactus = new Page();
1452
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1453
				$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>');
1454
				$contactus->Sort = 3;
1455
				$contactus->write();
1456
				$contactus->publish('Stage', 'Live');
1457
				$contactus->flushCache();
1458
				DB::alteration_message('Contact Us page created', 'created');
1459
			}
1460
		}
1461
		
1462
		// schema migration
1463
		// @todo Move to migration task once infrastructure is implemented
1464
		if($this->class == 'SiteTree') {
1465
			$conn = DB::get_schema();
1466
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1467
			if($conn->hasField('SiteTree' ,'Viewers')) {
1468
				$task = new UpgradeSiteTreePermissionSchemaTask();
1469
				$task->run(new SS_HTTPRequest('GET','/'));
1470
			}
1471
		}
1472
	}
1473
1474
	protected function onBeforeWrite() {
1475
		parent::onBeforeWrite();
1476
1477
		// If Sort hasn't been set, make this page come after it's siblings
1478
		if(!$this->Sort) {
1479
			$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...
1480
			$this->Sort = DB::prepared_query(
1481
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1482
				array($parentID)
1483
			)->value();
1484
		}
1485
1486
		// If there is no URLSegment set, generate one from Title
1487
		$defaultSegment = $this->generateURLSegment(_t(
1488
			'CMSMain.NEWPAGE',
1489
			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...
1490
		));
1491
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1492
			$this->URLSegment = $this->generateURLSegment($this->Title);
1493
		} else if($this->isChanged('URLSegment', 2)) {
1494
			// Do a strict check on change level, to avoid double encoding caused by
1495
			// bogus changes through forceChange()
1496
			$filter = URLSegmentFilter::create();
1497
			$this->URLSegment = $filter->filter($this->URLSegment);
1498
			// If after sanitising there is no URLSegment, give it a reasonable default
1499
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1500
		}
1501
		
1502
		// Ensure that this object has a non-conflicting URLSegment value.
1503
		$count = 2;
1504
		while(!$this->validURLSegment()) {
1505
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1506
			$count++;
1507
		}
1508
1509
		$this->syncLinkTracking();
1510
1511
		// Check to see if we've only altered fields that shouldn't affect versioning
1512
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1513
		$changedFields = array_keys($this->getChangedFields(true, 2));
1514
1515
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1516
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1517
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1518
1519
		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...
1520
			// This will have the affect of preserving the versioning
1521
			$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...
1522
		}
1523
	}
1524
1525
	/**
1526
	 * Trigger synchronisation of link tracking
1527
	 *
1528
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1529
	 */
1530
	public function syncLinkTracking() {
1531
		$this->extend('augmentSyncLinkTracking');
1532
	}
1533
	
1534
	public function onAfterWrite() {
1535
		// Need to flush cache to avoid outdated versionnumber references
1536
		$this->flushCache();
1537
		
1538
		$linkedPages = $this->VirtualPages();
1539
		if($linkedPages) {
1540
			// The only way after a write() call to determine if it was triggered by a writeWithoutVersion(),
1541
			// which we have to pass on to the virtual page writes as well.
1542
			$previous = ($this->Version > 1) ? Versioned::get_version($this->class, $this->ID, $this->Version-1) : null;
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...
Unused Code introduced by
$previous 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...
1543
			$withoutVersion = $this->getExtensionInstance('Versioned')->_nextWriteWithoutVersion;
0 ignored issues
show
Bug introduced by
The property _nextWriteWithoutVersion does not seem to exist in Extension.

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...
1544
			foreach($linkedPages as $page) {
1545
				 $page->copyFrom($page->CopyContentFrom());
1546
				 if($withoutVersion) $page->writeWithoutVersion();
1547
				 else $page->write();
1548
			}
1549
		}
1550
		
1551
		parent::onAfterWrite();
1552
	}
1553
	
1554
	public function onBeforeDelete() {
1555
		parent::onBeforeDelete();
1556
		
1557
		// If deleting this page, delete all its children.
1558
		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...
1559
			foreach($children as $child) {
1560
				$child->delete();
1561
			}
1562
		}
1563
	}
1564
	
1565
	public function onAfterDelete() {
1566
		// Need to flush cache to avoid outdated versionnumber references
1567
		$this->flushCache();
1568
		
1569
		// Need to mark pages depending to this one as broken
1570
		$dependentPages = $this->DependentPages();
1571
		if($dependentPages) foreach($dependentPages as $page) {
1572
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1573
			$page->write();
1574
		}
1575
		
1576
		parent::onAfterDelete();
1577
	}
1578
1579
	public function flushCache($persistent = true) {
1580
		parent::flushCache($persistent);
1581
		$this->_cache_statusFlags = null;
1582
	}
1583
	
1584
	public function validate() {
1585
		$result = parent::validate();
1586
1587
		// Allowed children validation
1588
		$parent = $this->getParent();
1589
		if($parent && $parent->exists()) {
1590
			// No need to check for subclasses or instanceof, as allowedChildren() already
1591
			// deconstructs any inheritance trees already.
1592
			$allowed = $parent->allowedChildren();
1593
			$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...
1594
			if(!in_array($subject->ClassName, $allowed)) {
1595
				
1596
				$result->error(
1597
					_t(
1598
						'SiteTree.PageTypeNotAllowed',
1599
						'Page type "{type}" not allowed as child of this parent page',
1600
						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...
1601
					),
1602
					'ALLOWED_CHILDREN'
1603
				);
1604
			}
1605
		}
1606
1607
		// "Can be root" validation
1608 View Code Duplication
		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...
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...
1609
			$result->error(
1610
				_t(
1611
					'SiteTree.PageTypNotAllowedOnRoot',
1612
					'Page type "{type}" is not allowed on the root level',
1613
					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...
1614
				),
1615
				'CAN_BE_ROOT'
1616
			);
1617
		}
1618
		
1619
		return $result;
1620
	}
1621
	
1622
	/**
1623
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1624
	 * checks for:
1625
	 *  - A page with the same URLSegment that has a conflict
1626
	 *  - Conflicts with actions on the parent page
1627
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1628
	 *
1629
	 * @return bool
1630
	 */
1631
	public function validURLSegment() {
1632
		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...
1633
			if($controller = ModelAsController::controller_for($parent)) {
1634
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1635
			}
1636
		}
1637
		
1638
		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...
1639
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1640
		}
1641
		
1642
		// Filters by url, id, and parent
1643
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1644
		if($this->ID) {
1645
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1646
		}
1647
		if(self::config()->nested_urls) {
1648
			$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...
1649
		}
1650
		
1651
		$votes = array_filter(
1652
			(array)$this->extend('augmentValidURLSegment'),
1653
			function($v) {return !is_null($v);}
1654
		);
1655
		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...
1656
			return min($votes);
1657
		}
1658
1659
		// Check existence
1660
		$existingPage = DataObject::get_one('SiteTree', $filter);
1661
		if ($existingPage) return false;
1662
1663
		return !($existingPage);
1664
		}
1665
		
1666
	/**
1667
	 * Generate a URL segment based on the title provided.
1668
	 *
1669
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1670
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1671
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1672
	 * scratch, or incrementally modify the generated URL.
1673
	 *
1674
	 * @param string $title Page title
1675
	 * @return string Generated url segment
1676
	 */
1677
	public function generateURLSegment($title){
1678
		$filter = URLSegmentFilter::create();
1679
		$t = $filter->filter($title);
1680
		
1681
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1682
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1683
		
1684
		// Hook for extensions
1685
		$this->extend('updateURLSegment', $t, $title);
1686
		
1687
		return $t;
1688
	}
1689
	
1690
	/**
1691
	 * Gets the URL segment for the latest draft version of this page.
1692
	 *
1693
	 * @return string
1694
	 */
1695
	public function getStageURLSegment() {
1696
		$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...
1697
			'"SiteTree"."ID"' => $this->ID
1698
		));
1699
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1700
	}
1701
	
1702
	/**
1703
	 * Gets the URL segment for the currently published version of this page.
1704
	 *
1705
	 * @return string
1706
	 */
1707
	public function getLiveURLSegment() {
1708
		$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...
1709
			'"SiteTree"."ID"' => $this->ID
1710
		));
1711
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1712
	}
1713
	
1714
	/**
1715
	 * Rewrites any linked images on this page without creating a new version record.
1716
	 * Non-image files should be linked via shortcodes
1717
	 * Triggers the onRenameLinkedAsset action on extensions.
1718
	 *
1719
	 * @todo Implement image shortcodes and remove this feature
1720
	 */
1721
	public function rewriteFileLinks() {
1722
		// Skip live stage
1723
		if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
1724
			return;
1725
		}
1726
1727
		// Update the content without actually creating a new version
1728
		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...
1729
			// Skip if non HTML or if empty
1730
			if ($fieldType !== 'HTMLText') {
1731
				continue;
1732
			}
1733
			$fieldValue = $this->{$fieldName};
1734
			if(empty($fieldValue)) {
1735
				continue;
1736
			}
1737
1738
			// Regenerate content
1739
			$content = Image::regenerate_html_links($fieldValue);
1740
			if($content === $fieldValue) {
1741
				continue;
1742
			}
1743
1744
			// Write content directly without updating linked assets
1745
			$table = ClassInfo::table_for_object_field($this, $fieldName);
1746
			$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
1747
			DB::prepared_query($query, array($content, $this->ID));
1748
1749
			// Update linked assets
1750
			$this->invokeWithExtensions('onRenameLinkedAsset');
1751
		}
1752
	}
1753
	
1754
	/**
1755
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1756
	 *
1757
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1758
	 * @return ArrayList
1759
	 */
1760
	public function DependentPages($includeVirtuals = true) {
1761
		if(class_exists('Subsite')) {
1762
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1763
			Subsite::disable_subsite_filter(true);
1764
		}
1765
		
1766
		// Content links
1767
		$items = new ArrayList();
1768
1769
		// We merge all into a regular SS_List, because DataList doesn't support merge
1770
		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...
1771
			$linkList = new ArrayList();
1772
			foreach($contentLinks as $item) {
1773
				$item->DependentLinkType = 'Content link';
1774
				$linkList->push($item);
1775
			}
1776
			$items->merge($linkList);
1777
		}
1778
		
1779
		// Virtual pages
1780
		if($includeVirtuals) {
1781
			$virtuals = $this->VirtualPages();
1782
			if($virtuals) {
1783
				$virtualList = new ArrayList();
1784
				foreach($virtuals as $item) {
1785
					$item->DependentLinkType = 'Virtual page';
1786
					$virtualList->push($item);
1787
				}
1788
				$items->merge($virtualList);
1789
			}
1790
		}
1791
1792
		// Redirector pages
1793
		$redirectors = RedirectorPage::get()->where(array(
1794
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1795
			'"RedirectorPage"."LinkToID"' => $this->ID
1796
		));
1797
		if($redirectors) {
1798
			$redirectorList = new ArrayList();
1799
			foreach($redirectors as $item) {
1800
				$item->DependentLinkType = 'Redirector page';
1801
				$redirectorList->push($item);
1802
			}
1803
			$items->merge($redirectorList);
1804
		}
1805
1806
		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...
1807
		
1808
		return $items;
1809
	}
1810
1811
	/**
1812
	 * Return all virtual pages that link to this page.
1813
	 *
1814
	 * @return DataList
1815
	 */
1816
	public function VirtualPages() {
1817
		
1818
		// Ignore new records
1819
		if(!$this->ID) return null;
1820
		
1821
		// Check subsite virtual pages
1822
		// @todo Refactor out subsite module specific code
1823
		if(class_exists('Subsite')) {
1824
			return Subsite::get_from_all_subsites('VirtualPage', array(
1825
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1826
			));
1827
		}
1828
		
1829
		// Check existing virtualpages
1830
		if(class_exists('VirtualPage')) {
1831
			return VirtualPage::get()->where(array(
1832
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1833
			));
1834
		}
1835
		
1836
		return null;
1837
	}
1838
1839
	/**
1840
	 * Returns a FieldList with which to create the main editing form.
1841
	 *
1842
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1843
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1844
	 *
1845
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1846
	 * e.g. access control.
1847
	 *
1848
	 * @return FieldList The fields to be displayed in the CMS
1849
	 */
1850
	public function getCMSFields() {
1851
		require_once("forms/Form.php");
1852
		// Status / message
1853
		// Create a status message for multiple parents
1854
		if($this->ID && is_numeric($this->ID)) {
1855
			$linkedPages = $this->VirtualPages();
1856
1857
			$parentPageLinks = array();
1858
1859
			if($linkedPages->Count() > 0) {
1860
				foreach($linkedPages as $linkedPage) {
1861
					$parentPage = $linkedPage->Parent;
1862
					if($parentPage) {
1863
						if($parentPage->ID) {
1864
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1865
						} else {
1866
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1867
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1868
								"</a>";
1869
						}
1870
					}
1871
				}
1872
1873
				$lastParent = array_pop($parentPageLinks);
1874
				$parentList = "'$lastParent'";
1875
1876
				if(count($parentPageLinks) > 0) {
1877
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1878
						. $parentList;
1879
				}
1880
1881
				$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...
1882
					'SiteTree.APPEARSVIRTUALPAGES',
1883
					"This content also appears on the virtual pages in the {title} sections.",
1884
					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...
1885
				);
1886
			}
1887
		}
1888
1889
		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...
1890
			$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...
1891
		}
1892
1893
		$dependentNote = '';
1894
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1895
		
1896
		// Create a table for showing pages linked to this one
1897
		$dependentPages = $this->DependentPages();
1898
		$dependentPagesCount = $dependentPages->Count();
1899
		if($dependentPagesCount) {
1900
			$dependentColumns = array(
1901
				'Title' => $this->fieldLabel('Title'),
1902
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1903
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1904
			);
1905
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1906
			
1907
			$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>');
1908
			$dependentTable = GridField::create(
1909
				'DependentPages',
1910
				false,
1911
				$dependentPages
1912
			);
1913
			$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...
1914
				->setDisplayFields($dependentColumns)
1915
				->setFieldFormatting(array(
1916
					'Title' => function($value, &$item) {
1917
						return sprintf(
1918
							'<a href="admin/pages/edit/show/%d">%s</a>',
1919
							(int)$item->ID,
1920
							Convert::raw2xml($item->Title)
1921
						);
1922
					},
1923
					'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...
1924
						return sprintf(
1925
							'<a href="%s" target="_blank">%s</a>',
1926
							Convert::raw2xml($value),
1927
							Convert::raw2xml($value)
1928
						);
1929
					}
1930
				));
1931
		}
1932
		
1933
		$baseLink = Controller::join_links (
1934
			Director::absoluteBaseURL(),
1935
			(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...
1936
		);
1937
		
1938
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1939
			->setURLPrefix($baseLink)
1940
			->setDefaultURL($this->generateURLSegment(_t(
1941
				'CMSMain.NEWPAGE',
1942
				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...
1943
			)));
1944
		$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...
1945
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1946
			$helpText .= $helpText ? '<br />' : '';
1947
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1948
		}
1949
		$urlsegment->setHelpText($helpText);
1950
		
1951
		$fields = new FieldList(
1952
			$rootTab = new TabSet("Root",
1953
				$tabMain = new Tab('Main',
1954
					new TextField("Title", $this->fieldLabel('Title')),
1955
					$urlsegment,
1956
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1957
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1958
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1959
						array(
1960
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1961
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1962
						)
1963
					)->setHeadingLevel(4)
1964
				),
1965
				$tabDependent = new Tab('Dependent',
1966
					$dependentNote,
1967
					$dependentTable
1968
				)
1969
			)
1970
		);
1971
		$htmlField->addExtraClass('stacked');
1972
		
1973
		// Help text for MetaData on page content editor
1974
		$metaFieldDesc
1975
			->setRightTitle(
1976
				_t(
1977
					'SiteTree.METADESCHELP',
1978
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1979
				)
1980
			)
1981
			->addExtraClass('help');
1982
		$metaFieldExtra
1983
			->setRightTitle(
1984
				_t(
1985
					'SiteTree.METAEXTRAHELP',
1986
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1987
				)
1988
			)
1989
			->addExtraClass('help');
1990
1991
		// Conditional dependent pages tab
1992
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1993
		else $fields->removeFieldFromTab('Root', 'Dependent');
1994
		
1995
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1996
1997
		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...
1998
			$obsoleteWarning = _t(
1999
				'SiteTree.OBSOLETECLASS',
2000
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2001
				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...
2002
			);
2003
2004
			$fields->addFieldToTab(
2005
				"Root.Main",
2006
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2007
				"Title"
2008
			);
2009
		}
2010
2011
		if(file_exists(BASE_PATH . '/install.php')) {
2012
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2013
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2014
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2015
				. "</p>"), "Title");
2016
		}
2017
2018
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
2019
		$fields->setTabPathRewrites(array(
2020
			'/^Root\.Content\.Main$/' => 'Root.Main',
2021
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
2022
		));
2023
		
2024
		if(self::$runCMSFieldsExtensions) {
2025
			$this->extend('updateCMSFields', $fields);
2026
		}
2027
2028
		return $fields;
2029
	}
2030
	
2031
	
2032
	/**
2033
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2034
	 * for content-related fields.
2035
	 *
2036
	 * @return FieldList
2037
	 */
2038
	public function getSettingsFields() {
2039
		$groupsMap = array();
2040
		foreach(Group::get() as $group) {
2041
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2042
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2043
		}
2044
		asort($groupsMap);
2045
		
2046
		$fields = new FieldList(
2047
			$rootTab = new TabSet("Root",
2048
				$tabBehaviour = new Tab('Settings',
2049
					new DropdownField(
2050
						"ClassName",
2051
						$this->fieldLabel('ClassName'),
2052
						$this->getClassDropdown()
2053
					),
2054
					$parentTypeSelector = new CompositeField(
2055
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2056
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2057
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2058
						)),
2059
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
2060
					),
2061
					$visibility = new FieldGroup(
2062
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2063
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2064
					),
2065
					$viewersOptionsField = new OptionsetField(
2066
						"CanViewType",
2067
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2068
					),
2069
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2070
						->setSource($groupsMap)
2071
						->setAttribute(
2072
							'data-placeholder',
2073
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2074
						),
2075
					$editorsOptionsField = new OptionsetField(
2076
						"CanEditType",
2077
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2078
					),
2079
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2080
						->setSource($groupsMap)
2081
						->setAttribute(
2082
							'data-placeholder',
2083
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2084
						)
2085
				)
2086
			)
2087
		);
2088
		
2089
		$visibility->setTitle($this->fieldLabel('Visibility'));
2090
		
2091
2092
		// This filter ensures that the ParentID dropdown selection does not show this node,
2093
		// or its descendents, as this causes vanishing bugs
2094
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2095
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2096
		
2097
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2098
		
2099
		// Make page location fields read-only if the user doesn't have the appropriate permission
2100
		if(!Permission::check("SITETREE_REORGANISE")) {
2101
			$fields->makeFieldReadonly('ParentType');
2102
			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...
2103
				$fields->removeByName('ParentID');
2104
			} else {
2105
				$fields->makeFieldReadonly('ParentID');
2106
			}
2107
		}
2108
		
2109
		$viewersOptionsSource = array();
2110
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2111
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2112
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2113
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2114
		$viewersOptionsField->setSource($viewersOptionsSource);
2115
		
2116
		$editorsOptionsSource = array();
2117
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2118
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2119
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2120
		$editorsOptionsField->setSource($editorsOptionsSource);
2121
2122
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2123
			$fields->makeFieldReadonly($viewersOptionsField);
2124
			if($this->CanViewType == 'OnlyTheseUsers') {
2125
				$fields->makeFieldReadonly($viewerGroupsField);
2126
			} else {
2127
				$fields->removeByName('ViewerGroups');
2128
			}
2129
			
2130
			$fields->makeFieldReadonly($editorsOptionsField);
2131
			if($this->CanEditType == 'OnlyTheseUsers') {
2132
				$fields->makeFieldReadonly($editorGroupsField);
2133
			} else {
2134
				$fields->removeByName('EditorGroups');
2135
			}
2136
		}
2137
		
2138
		if(self::$runCMSFieldsExtensions) {
2139
			$this->extend('updateSettingsFields', $fields);
2140
		}
2141
		
2142
		return $fields;
2143
	}
2144
	
2145
	/**
2146
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2147
	 * @return array
2148
	 */
2149
	public function fieldLabels($includerelations = true) {
2150
		$cacheKey = $this->class . '_' . $includerelations;
2151
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2152
			$labels = parent::fieldLabels($includerelations);
2153
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2154
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2155
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2156
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2157
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2158
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2159
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2160
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2161
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2162
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2163
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2164
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2165
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2166
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2167
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2168
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2169
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2170
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2171
			$labels['LinkChangeNote'] = _t (
2172
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2173
			);
2174
			
2175
			if($includerelations){
2176
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2177
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2178
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2179
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2180
			}
2181
2182
			self::$_cache_field_labels[$cacheKey] = $labels;
2183
		}
2184
2185
		return self::$_cache_field_labels[$cacheKey];
2186
	}
2187
2188
	/**
2189
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2190
	 *
2191
	 * Frontend scripts and styles know how to handle the following FormFields:
2192
	 * - top-level FormActions appear as standalone buttons
2193
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2194
	 * - TabSet & Tabs appear as a drop ups
2195
	 * - FormActions within the Tab are restyled as links
2196
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2197
	 *
2198
	 * @return FieldList The available actions for this page.
2199
	 */
2200
	public function getCMSActions() {
2201
		$existsOnLive = $this->getExistsOnLive();
2202
2203
		// Major actions appear as buttons immediately visible as page actions.
2204
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset noborder');
2205
2206
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2207
		$rootTabSet = new TabSet('ActionMenus');
2208
		$moreOptions = new Tab(
2209
			'MoreOptions',
2210
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2211
		);
2212
		$rootTabSet->push($moreOptions);
2213
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2214
2215
		// Render page information into the "more-options" drop-up, on the top.
2216
		$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...
2217
			'"SiteTree"."ID"' => $this->ID
2218
		));
2219
		$moreOptions->push(
2220
			new LiteralField('Information',
2221
				$this->customise(array(
2222
					'Live' => $live,
2223
					'ExistsOnLive' => $existsOnLive
2224
				))->renderWith('SiteTree_Information')
2225
			)
2226
		);
2227
2228
		// "readonly"/viewing version that isn't the current version of the record
2229
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::current_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...
2230
			'"SiteTree"."ID"' => $this->ID
2231
		));
2232
		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...
2233
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2234
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2235
2236
			$actions = new FieldList(array($majorActions, $rootTabSet));
2237
2238
			// getCMSActions() can be extended with updateCMSActions() on a extension
2239
			$this->extend('updateCMSActions', $actions);
2240
2241
			return $actions;
2242
		}
2243
2244 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...
2245
			// "unpublish"
2246
			$moreOptions->push(
2247
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2248
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2249
					->addExtraClass('ss-ui-action-destructive')
2250
			);
2251
		}
2252
2253 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...
2254
			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...
2255
				// "rollback"
2256
				$moreOptions->push(
2257
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2258
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2259
				);
2260
			}
2261
		}
2262
2263
		if($this->canEdit()) {
2264
			if($this->getIsDeletedFromStage()) {
2265
				// The usual major actions are not available, so we provide alternatives here.
2266
				if($existsOnLive) {
2267
					// "restore"
2268
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2269
					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...
2270
						// "delete from live"
2271
						$majorActions->push(
2272
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2273
								->addExtraClass('ss-ui-action-destructive')
2274
						);
2275
					}
2276
				} else {
2277
					// Determine if we should force a restore to root (where once it was a subpage)
2278
					$restoreToRoot = $this->isParentArchived();
2279
					
2280
					// "restore"
2281
					$title = $restoreToRoot
2282
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2283
						: _t('CMSMain.RESTORE','Restore draft');
2284
					$description = $restoreToRoot
2285
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2286
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2287
					$majorActions->push(
2288
						FormAction::create('restore', $title)
2289
							->setDescription($description)
2290
							->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...
2291
							->setAttribute('data-icon', 'decline')
2292
					);
2293
				}
2294
			} else {
2295
				// Detect use of legacy actions
2296
				// {@see CMSMain::enabled_legacy_actions}
2297
				$legacy = CMSMain::config()->enabled_legacy_actions;
2298
				if(in_array('CMSBatchAction_Delete', $legacy)) {
2299
					Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
2300
					if($this->canDelete()) {
2301
						// delete
2302
						$moreOptions->push(
2303
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2304
								->addExtraClass('delete ss-ui-action-destructive')
2305
						);
2306
					}
2307
				} 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...
2308
					// "archive"
2309
					$moreOptions->push(
2310
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2311
							->setDescription(_t(
2312
								'SiteTree.BUTTONARCHIVEDESC',
2313
								'Unpublish and send to archive'
2314
							))
2315
							->addExtraClass('delete ss-ui-action-destructive')
2316
					);
2317
				}
2318
			
2319
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2320
				$majorActions->push(
2321
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2322
						->setAttribute('data-icon', 'accept')
2323
						->setAttribute('data-icon-alternate', 'addpage')
2324
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2325
				);
2326
			}
2327
		}
2328
2329
		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...
2330
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2331
			$majorActions->push(
2332
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2333
					->setAttribute('data-icon', 'accept')
2334
					->setAttribute('data-icon-alternate', 'disk')
2335
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2336
			);
2337
2338
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2339
			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...
2340
				$publish->addExtraClass('ss-ui-alternate');
2341
			}
2342
		}
2343
		
2344
		$actions = new FieldList(array($majorActions, $rootTabSet));
2345
		
2346
		// Hook for extensions to add/remove actions.
2347
		$this->extend('updateCMSActions', $actions);
2348
		
2349
		return $actions;
2350
	}
2351
	
2352
	/**
2353
	 * Publish this page.
2354
	 *
2355
	 * @uses SiteTreeExtension->onBeforePublish()
2356
	 * @uses SiteTreeExtension->onAfterPublish()
2357
	 * @return bool True if published
2358
	 */
2359
	public function doPublish() {
2360
		if (!$this->canPublish()) return false;
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...
2361
		
2362
		$original = 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...
2363
			'"SiteTree"."ID"' => $this->ID
2364
		));
2365
		if(!$original) $original = new SiteTree();
2366
2367
		// Handle activities undertaken by extensions
2368
		$this->invokeWithExtensions('onBeforePublish', $original);
2369
		//$this->PublishedByID = Member::currentUser()->ID;
2370
		$this->write();
2371
		$this->publish("Stage", "Live");
0 ignored issues
show
Bug introduced by
The method publish() does not exist on SiteTree. Did you maybe mean doPublish()?

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...
2372
2373
		DB::prepared_query('UPDATE "SiteTree_Live"
2374
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2375
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2376
			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...
2377
		);
2378
			
2379
		// Publish any virtual pages that might need publishing
2380
		$linkedPages = $this->VirtualPages();
2381
		if($linkedPages) foreach($linkedPages as $page) {
2382
			$page->copyFrom($page->CopyContentFrom());
2383
			$page->write();
2384
			if($page->getExistsOnLive()) $page->doPublish();
2385
		}
2386
		
2387
		// Need to update pages linking to this one as no longer broken, on the live site
2388
		$origMode = Versioned::get_reading_mode();
2389
		Versioned::reading_stage('Live');
2390
		foreach($this->DependentPages(false) as $page) {
2391
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2392
			$page->write();
2393
		}
2394
		Versioned::set_reading_mode($origMode);
2395
		
2396
		// Handle activities undertaken by extensions
2397
		$this->invokeWithExtensions('onAfterPublish', $original);
2398
		
2399
		return true;
2400
	}
2401
	
2402
	/**
2403
	 * Unpublish this page - remove it from the live site
2404
	 *
2405
	 * Overrides {@see Versioned::doUnpublish()}
2406
	 *
2407
	 * @uses SiteTreeExtension->onBeforeUnpublish()
2408
	 * @uses SiteTreeExtension->onAfterUnpublish()
2409
	 */
2410
	public function doUnpublish() {
2411
		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...
2412
		if(!$this->ID) return false;
2413
		
2414
		$this->invokeWithExtensions('onBeforeUnpublish', $this);
2415
		
2416
		$origStage = Versioned::current_stage();
2417
		Versioned::reading_stage('Live');
2418
2419
		// We should only unpublish virtualpages that exist on live
2420
		$virtualPages = $this->VirtualPages();
2421
2422
		// This way our ID won't be unset
2423
		$clone = clone $this;
2424
		$clone->delete();
2425
2426
		// Rewrite backlinks
2427
		$dependentPages = $this->DependentPages(false);
2428
		if($dependentPages) foreach($dependentPages as $page) {
2429
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2430
			$page->write();
2431
		}
2432
		Versioned::reading_stage($origStage);
2433
2434
		// Unpublish any published virtual pages
2435
		if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();
2436
2437
		// If we're on the draft site, then we can update the status.
2438
		// Otherwise, these lines will resurrect an inappropriate record
2439
		if(DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()
2440
			&& Versioned::current_stage() != 'Live') {
2441
			$this->write();
2442
		}
2443
2444
		$this->invokeWithExtensions('onAfterUnpublish', $this);
2445
2446
		return true;
2447
	}
2448
	
2449
	/**
2450
	 * Revert the draft changes: replace the draft content with the content on live
2451
	 */
2452
	public function doRevertToLive() {
2453
		$this->invokeWithExtensions('onBeforeRevertToLive', $this);
2454
2455
		$this->publish("Live", "Stage", false);
0 ignored issues
show
Bug introduced by
The method publish() does not exist on SiteTree. Did you maybe mean doPublish()?

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...
2456
2457
		// Use a clone to get the updates made by $this->publish
2458
		$clone = DataObject::get_by_id("SiteTree", $this->ID);
2459
		$clone->writeWithoutVersion();
2460
2461
		// Need to update pages linking to this one as no longer broken
2462
		foreach($this->DependentPages(false) as $page) {
2463
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2464
			$page->write();
2465
		}
2466
		
2467
		$this->invokeWithExtensions('onAfterRevertToLive', $this);
2468
		return true;
2469
	}
2470
2471
	/**
2472
	 * Determine if this page references a parent which is archived, and not available in stage
2473
	 *
2474
	 * @return bool True if there is an archived parent
2475
	 */
2476
	protected function isParentArchived() {
2477
		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...
2478
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2479
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2480
				return true;
2481
			}
2482
		}
2483
		return false;
2484
	}
2485
	
2486
	/**
2487
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2488
	 *
2489
	 * @return self
2490
	 */
2491
	public function doRestoreToStage() {
2492
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2493
2494
		// Ensure that the parent page is restored, otherwise restore to root
2495
		if($this->isParentArchived()) {
2496
			$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...
2497
		}
2498
		
2499
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2500
		// create an empty record
2501
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2502
			$conn = DB::get_conn();
2503
			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...
2504
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2505
			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...
2506
		}
2507
		
2508
		$oldStage = Versioned::current_stage();
2509
		Versioned::reading_stage('Stage');
2510
		$this->forceChange();
2511
		$this->write();
2512
		
2513
		$result = DataObject::get_by_id($this->class, $this->ID);
2514
2515
		// Need to update pages linking to this one as no longer broken
2516
		foreach($result->DependentPages(false) as $page) {
2517
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2518
			$page->write();
2519
		}
2520
		
2521
		Versioned::reading_stage($oldStage);
2522
2523
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2524
		
2525
		return $result;
2526
	}
2527
2528
	/**
2529
	 * @deprecated
2530
	 */
2531
	public function doDeleteFromLive() {
2532
		Deprecation::notice("4.0", "Use doUnpublish instead");
2533
		return $this->doUnpublish();
2534
	}
2535
2536
	/**
2537
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2538
	 *
2539
	 * @return bool
2540
	 */
2541
	public function isNew() {
2542
		/**
2543
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2544
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2545
		 */
2546
		if(empty($this->ID)) return true;
2547
2548
		if(is_numeric($this->ID)) return false;
2549
2550
		return stripos($this->ID, 'new') === 0;
2551
	}
2552
2553
	/**
2554
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2555
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2556
	 * {@link SiteTree::$needs_permission}.
2557
	 *
2558
	 * @return array
2559
	 */
2560
	protected function getClassDropdown() {
2561
		$classes = self::page_type_classes();
2562
		$currentClass = null;
2563
		$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...
2564
		
2565
		$result = array();
2566
		foreach($classes as $class) {
2567
			$instance = singleton($class);
2568
2569
			// if the current page type is this the same as the class type always show the page type in the list
2570
			if ($this->ClassName != $instance->ClassName) {
2571
				if($instance instanceof HiddenClass) continue;
2572
				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...
2573
			}
2574
			
2575
			if($perms = $instance->stat('need_permission')) {
2576
				if(!$this->can($perms)) continue;
2577
			}
2578
2579
			$pageTypeName = $instance->i18n_singular_name();
2580
2581
			$currentClass = $class;
2582
			$result[$class] = $pageTypeName;
2583
2584
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2585
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2586
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2587
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2588
				$result[$class] = $result[$class] .  " ({$class})";
2589
			}
2590
		}
2591
		
2592
		// sort alphabetically, and put current on top
2593
		asort($result);
2594
		if($currentClass) {
2595
			$currentPageTypeName = $result[$currentClass];
2596
			unset($result[$currentClass]);
2597
			$result = array_reverse($result);
2598
			$result[$currentClass] = $currentPageTypeName;
2599
			$result = array_reverse($result);
2600
		}
2601
		
2602
		return $result;
2603
	}
2604
2605
	/**
2606
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2607
	 *
2608
	 * @return string[]
2609
	 */
2610
	public function allowedChildren() {
2611
		$allowedChildren = array();
2612
		$candidates = $this->stat('allowed_children');
2613
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2614
			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...
2615
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2616
				// Otherwise, the class and all its subclasses are allowed.
2617
				if(substr($candidate,0,1) == '*') {
2618
					$allowedChildren[] = substr($candidate,1);
2619
				} else {
2620
					$subclasses = ClassInfo::subclassesFor($candidate);
2621
					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...
2622
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2623
					}
2624
				}
2625
			}
2626
		}
2627
		
2628
		return $allowedChildren;
2629
	}
2630
2631
	/**
2632
	 * Returns the class name of the default class for children of this page.
2633
	 *
2634
	 * @return string
2635
	 */
2636
	public function defaultChild() {
2637
		$default = $this->stat('default_child');
2638
		$allowed = $this->allowedChildren();
2639
		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...
2640
			if(!$default || !in_array($default, $allowed))
2641
				$default = reset($allowed);
2642
			return $default;
2643
		}
2644
	}
2645
2646
	/**
2647
	 * Returns the class name of the default class for the parent of this page.
2648
	 *
2649
	 * @return string
2650
	 */
2651
	public function defaultParent() {
2652
		return $this->stat('default_parent');
2653
	}
2654
2655
	/**
2656
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2657
	 * Title field.
2658
	 *
2659
	 * @return string
2660
	 */
2661
	public function getMenuTitle(){
2662
		if($value = $this->getField("MenuTitle")) {
2663
			return $value;
2664
		} else {
2665
			return $this->getField("Title");
2666
		}
2667
	}
2668
2669
2670
	/**
2671
	 * Set the menu title for this page.
2672
	 *
2673
	 * @param string $value
2674
	 */
2675
	public function setMenuTitle($value) {
2676
		if($value == $this->getField("Title")) {
2677
			$this->setField("MenuTitle", null);
2678
		} else {
2679
			$this->setField("MenuTitle", $value);
2680
		}
2681
	}
2682
	
2683
	/**
2684
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2685
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2686
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2687
	 * the flags.
2688
	 *
2689
	 * Example (simple):
2690
	 *   "deletedonlive" => "Deleted"
2691
	 *
2692
	 * Example (with optional title attribute):
2693
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2694
	 *
2695
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2696
	 * @return array
2697
	 */
2698
	public function getStatusFlags($cached = true) {
2699
		if(!$this->_cache_statusFlags || !$cached) {
2700
			$flags = array();
2701
			if($this->getIsDeletedFromStage()) {
2702
				if($this->getExistsOnLive()) {
2703
					$flags['removedfromdraft'] = array(
2704
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2705
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2706
					);
2707
				} else {
2708
					$flags['archived'] = array(
2709
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2710
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2711
					);
2712
				}
2713
			} else if($this->getIsAddedToStage()) {
2714
				$flags['addedtodraft'] = array(
2715
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2716
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2717
				);
2718
			} else if($this->getIsModifiedOnStage()) {
2719
				$flags['modified'] = array(
2720
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2721
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2722
				);
2723
			}
2724
2725
			$this->extend('updateStatusFlags', $flags);
2726
2727
			$this->_cache_statusFlags = $flags;
2728
		}
2729
		
2730
		return $this->_cache_statusFlags;
2731
	}
2732
2733
	/**
2734
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2735
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2736
	 * publication status.
2737
	 *
2738
	 * @return string An HTML string ready to be directly used in a template
2739
	 */
2740
	public function getTreeTitle() {
2741
		// Build the list of candidate children
2742
		$children = array();
2743
		$candidates = static::page_type_classes();
2744
		foreach($this->allowedChildren() as $childClass) {
2745
			if(!in_array($childClass, $candidates)) continue;
2746
			$child = singleton($childClass);
2747
			if($child->canCreate(null, array('Parent' => $this))) {
2748
				$children[$childClass] = $child->i18n_singular_name();
2749
			}
2750
		}
2751
		$flags = $this->getStatusFlags();
2752
		$treeTitle = sprintf(
2753
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2754
			Convert::raw2att(Convert::raw2json($children)),
2755
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2756
		);
2757
		foreach($flags as $class => $data) {
2758
			if(is_string($data)) $data = array('text' => $data);
2759
			$treeTitle .= sprintf(
2760
				"<span class=\"badge %s\"%s>%s</span>",
2761
				'status-' . Convert::raw2xml($class),
2762
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2763
				Convert::raw2xml($data['text'])
2764
			);
2765
		}
2766
		
2767
		return $treeTitle;
2768
	}
2769
2770
	/**
2771
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2772
	 * we're currently inside, etc.
2773
	 *
2774
	 * @param int $level
2775
	 * @return SiteTree
2776
	 */
2777
	public function Level($level) {
2778
		$parent = $this;
2779
		$stack = array($parent);
2780
		while($parent = $parent->Parent) {
2781
			array_unshift($stack, $parent);
2782
		}
2783
2784
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2785
	}
2786
2787
	/**
2788
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2789
	 *
2790
	 * @return int
2791
	 */
2792
	public function getPageLevel() {
2793
		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...
2794
			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...
2795
		}
2796
		return 1;
2797
	}
2798
2799
	/**
2800
	 * Return the CSS classes to apply to this node in the CMS tree.
2801
	 *
2802
	 * @param string $numChildrenMethod
2803
	 * @return string
2804
	 */
2805
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2806
		$classes = sprintf('class-%s', $this->class);
2807
		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...
2808
			$classes .= " BrokenLink";
2809
		}
2810
2811
		if(!$this->canAddChildren()) {
2812
			$classes .= " nochildren";
2813
		}
2814
2815
		if(!$this->canEdit() && !$this->canAddChildren()) {
2816
			if (!$this->canView()) {
2817
				$classes .= " disabled";
2818
			} else {
2819
				$classes .= " edit-disabled";
2820
			}
2821
		}
2822
2823
		if(!$this->ShowInMenus) {
2824
			$classes .= " notinmenu";
2825
		}
2826
			
2827
		//TODO: Add integration
2828
		/*
2829
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2830
			$classes .= " untranslated ";
2831
		*/
2832
		$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...
2833
2834
		return $classes;
2835
	}
2836
2837
	/**
2838
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2839
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2840
	 *
2841
	 * @return bool
2842
	 */
2843
	public function getIsDeletedFromStage() {
2844
		if(!$this->ID) return true;
2845
		if($this->isNew()) return false;
2846
2847
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2848
2849
		// Return true for both completely deleted pages and for pages just deleted from stage
2850
		return !($stageVersion);
2851
	}
2852
	
2853
	/**
2854
	 * Return true if this page exists on the live site
2855
	 *
2856
	 * @return bool
2857
	 */
2858
	public function getExistsOnLive() {
2859
		return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2860
	}
2861
2862
	/**
2863
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2864
	 * unpublished changes to the draft site.
2865
	 *
2866
	 * @return bool
2867
	 */
2868
	public function getIsModifiedOnStage() {
2869
		// New unsaved pages could be never be published
2870
		if($this->isNew()) return false;
2871
		
2872
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2873
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2874
		
2875
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2876
		$this->extend('getIsModifiedOnStage', $isModified);
2877
		
2878
		return $isModified;
2879
	}
2880
	
2881
	/**
2882
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2883
	 * published.
2884
	 *
2885
	 * @return bool
2886
	 */
2887
	public function getIsAddedToStage() {
2888
		// New unsaved pages could be never be published
2889
		if($this->isNew()) return false;
2890
		
2891
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2892
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2893
2894
		return ($stageVersion && !$liveVersion);
2895
	}
2896
	
2897
	/**
2898
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2899
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2900
	 */
2901
	static public function disableCMSFieldsExtensions() {
2902
		self::$runCMSFieldsExtensions = false;
2903
	}
2904
	
2905
	/**
2906
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2907
	 * disableCMSFieldsExtensions().
2908
	 */
2909
	static public function enableCMSFieldsExtensions() {
2910
		self::$runCMSFieldsExtensions = true;
2911
	}
2912
2913
	public function providePermissions() {
2914
		return array(
2915
			'SITETREE_GRANT_ACCESS' => array(
2916
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2917
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2918
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2919
				'sort' => 100
2920
			),
2921
			'SITETREE_VIEW_ALL' => array(
2922
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2923
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2924
				'sort' => -100,
2925
				'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')
2926
			),
2927
			'SITETREE_EDIT_ALL' => array(
2928
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2929
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2930
				'sort' => -50,
2931
				'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')
2932
			),
2933
			'SITETREE_REORGANISE' => array(
2934
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2935
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2936
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2937
				'sort' => 100
2938
			),
2939
			'VIEW_DRAFT_CONTENT' => array(
2940
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2941
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2942
				'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.'),
2943
				'sort' => 100
2944
			)
2945
		);
2946
	}
2947
	
2948
	/**
2949
	 * Return the translated Singular name.
2950
	 *
2951
	 * @return string
2952
	 */
2953
	public function i18n_singular_name() {
2954
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2955
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2956
		return _t($class.'.SINGULARNAME', $this->singular_name());
2957
	}
2958
	
2959
	/**
2960
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2961
	 * picks it up for the wrong folder.
2962
	 *
2963
	 * @return array
2964
	 */
2965
	public function provideI18nEntities() {
2966
		$entities = parent::provideI18nEntities();
2967
		
2968
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2969
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;		
2970
2971
		$entities[$this->class . '.DESCRIPTION'] = array(
2972
			$this->stat('description'),
2973
			'Description of the page type (shown in the "add page" dialog)'
2974
		);
2975
2976
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2977
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2978
2979
		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...
2980
	}
2981
2982
	/**
2983
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2984
	 *
2985
	 * @return string
2986
	 */
2987
	public function getParentType() {
2988
		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...
2989
	}
2990
2991
	/**
2992
	 * Clear the permissions cache for SiteTree
2993
	 */
2994
	public static function reset() {
2995
		self::$cache_permissions = array();
2996
	}
2997
	
2998
	static public function on_db_reset() {
2999
		self::$cache_permissions = array();
3000
	}
3001
3002
}
3003