Completed
Pull Request — master (#1381)
by Damian
07:50
created

SiteTree::getCMSActions()   D

Complexity

Conditions 24
Paths 199

Size

Total Lines 151
Code Lines 87

Duplication

Lines 17
Ratio 11.26 %
Metric Value
dl 17
loc 151
rs 4.345
cc 24
eloc 87
nc 199
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 * @method ManyManyList BackLinkTracking List of site pages that link to this page.
30
 *
31
 * @mixin Hierarchy
32
 * @mixin Versioned
33
 * @mixin SiteTreeLinkTracking
34
 *
35
 * @package cms
36
 */
37
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
38
39
	/**
40
	 * Indicates what kind of children this page type can have.
41
	 * This can be an array of allowed child classes, or the string "none" -
42
	 * indicating that this page type can't have children.
43
	 * If a classname is prefixed by "*", such as "*Page", then only that
44
	 * class is allowed - no subclasses. Otherwise, the class and all its
45
	 * subclasses are allowed.
46
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
47
	 *
48
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
49
	 *
50
	 * @config
51
	 * @var array
52
	 */
53
	private static $allowed_children = array("SiteTree");
54
55
	/**
56
	 * The default child class for this page.
57
	 * Note: Value might be cached, see {@link $allowed_chilren}.
58
	 *
59
	 * @config
60
	 * @var string
61
	 */
62
	private static $default_child = "Page";
63
64
	/**
65
	 * Default value for SiteTree.ClassName enum
66
	 * {@see DBClassName::getDefault}
67
	 *
68
	 * @config
69
	 * @var string
70
	 */
71
	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...
72
73
	/**
74
	 * The default parent class for this page.
75
	 * Note: Value might be cached, see {@link $allowed_chilren}.
76
	 *
77
	 * @config
78
	 * @var string
79
	 */
80
	private static $default_parent = null;
81
82
	/**
83
	 * Controls whether a page can be in the root of the site tree.
84
	 * Note: Value might be cached, see {@link $allowed_chilren}.
85
	 *
86
	 * @config
87
	 * @var bool
88
	 */
89
	private static $can_be_root = true;
90
91
	/**
92
	 * List of permission codes a user can have to allow a user to create a page of this type.
93
	 * Note: Value might be cached, see {@link $allowed_chilren}.
94
	 *
95
	 * @config
96
	 * @var array
97
	 */
98
	private static $need_permission = null;
99
100
	/**
101
	 * If you extend a class, and don't want to be able to select the old class
102
	 * in the cms, set this to the old class name. Eg, if you extended Product
103
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
104
	 *
105
	 * @config
106
	 * @var string
107
	 */
108
	private static $hide_ancestor = null;
109
110
	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...
111
		"URLSegment" => "Varchar(255)",
112
		"Title" => "Varchar(255)",
113
		"MenuTitle" => "Varchar(100)",
114
		"Content" => "HTMLText",
115
		"MetaDescription" => "Text",
116
		"ExtraMeta" => "HTMLText('meta, link')",
117
		"ShowInMenus" => "Boolean",
118
		"ShowInSearch" => "Boolean",
119
		"Sort" => "Int",
120
		"HasBrokenFile" => "Boolean",
121
		"HasBrokenLink" => "Boolean",
122
		"ReportClass" => "Varchar",
123
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
124
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
125
	);
126
127
	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...
128
		"URLSegment" => true,
129
	);
130
131
	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...
132
		"LinkTracking" => "SiteTree",
133
		"ImageTracking" => "File",
134
		"ViewerGroups" => "Group",
135
		"EditorGroups" => "Group",
136
	);
137
138
	private static $belongs_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...
139
		"BackLinkTracking" => "SiteTree"
140
	);
141
142
	private static $many_many_extraFields = 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...
143
		"LinkTracking" => array("FieldName" => "Varchar"),
144
		"ImageTracking" => array("FieldName" => "Varchar")
145
	);
146
147
	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...
148
		"Breadcrumbs" => "HTMLText",
149
		"LastEdited" => "SS_Datetime",
150
		"Created" => "SS_Datetime",
151
		'Link' => 'Text',
152
		'RelativeLink' => 'Text',
153
		'AbsoluteLink' => 'Text',
154
		'TreeTitle' => 'HTMLText',
155
	);
156
157
	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...
158
		"ShowInMenus" => 1,
159
		"ShowInSearch" => 1,
160
		"CanViewType" => "Inherit",
161
		"CanEditType" => "Inherit"
162
	);
163
164
	private static $versioning = array(
165
		"Stage",  "Live"
166
	);
167
168
	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...
169
170
	/**
171
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
172
	 * @var boolean
173
	 * @config
174
	 */
175
	private static $can_create = true;
176
177
	/**
178
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
179
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
180
	 *
181
	 * @see CMSMain::generateTreeStylingCSS()
182
	 * @config
183
	 * @var string
184
	 */
185
	private static $icon = null;
186
	
187
	/**
188
	 * @config
189
	 * @var string Description of the class functionality, typically shown to a user
190
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
191
	 */
192
	private static $description = 'Generic content page';
193
194
	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...
195
		"Hierarchy",
196
		"Versioned('Stage', 'Live')",
197
		"SiteTreeLinkTracking"
198
	);
199
	
200
	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...
201
		'Title',
202
		'Content',
203
	);
204
205
	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...
206
		'URLSegment' => 'URL'
207
	);
208
	
209
	/**
210
	 * @config
211
	 */
212
	private static $nested_urls = true;
213
	
214
	/**
215
	 * @config
216
	*/
217
	private static $create_default_pages = true;
218
	
219
	/**
220
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
221
	 */
222
	private static $runCMSFieldsExtensions = true;
223
	
224
	/**
225
	 * Cache for canView/Edit/Publish/Delete permissions.
226
	 * Keyed by permission type (e.g. 'edit'), with an array
227
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
228
	 * See {@link batch_permission_check()} for details.
229
	 */
230
	private static $cache_permissions = array();
231
232
	/**
233
	 * @config
234
	 * @var boolean
235
	 */
236
	private static $enforce_strict_hierarchy = true;
237
238
	/**
239
	 * The value used for the meta generator tag. Leave blank to omit the tag.
240
	 *
241
	 * @config
242
	 * @var string
243
	 */
244
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
245
246
	protected $_cache_statusFlags = null;
247
	
248
	/**
249
	 * Determines if the system should avoid orphaned pages
250
	 * by deleting all children when the their parent is deleted (TRUE),
251
	 * or rather preserve this data even if its not reachable through any navigation path (FALSE).
252
	 *
253
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
254
	 * @param boolean
255
	 */
256
	static public function set_enforce_strict_hierarchy($to) {
257
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
258
		Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', $to);
259
	}
260
	
261
	/**
262
	 * @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
263
	 * @return boolean
264
	 */
265
	static public function get_enforce_strict_hierarchy() {
266
		Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
267
		return Config::inst()->get('SiteTree', 'enforce_strict_hierarchy');
268
	}
269
270
	/**
271
	 * Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
272
	 *
273
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
274
	 * @return bool
275
	 */
276
	static public function nested_urls() {
277
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
278
		return Config::inst()->get('SiteTree', 'nested_urls');
279
	}
280
	
281
	/**
282
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
283
	 */
284
	static public function enable_nested_urls() {
285
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
286
		Config::inst()->update('SiteTree', 'nested_urls', true);
287
	}
288
	
289
	/**
290
	 * @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
291
	 */
292
	static public function disable_nested_urls() {
293
		Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
294
		Config::inst()->update('SiteTree', 'nested_urls', false);
295
	}
296
	
297
	/**
298
	 * Set the (re)creation of default pages on /dev/build
299
	 *
300
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
301
	 * @param bool $option
302
	 */
303
	static public function set_create_default_pages($option = true) {
304
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
305
		Config::inst()->update('SiteTree', 'create_default_pages', $option);
306
	}
307
308
	/**
309
	 * Return true if default pages should be created on /dev/build.
310
	 *
311
	 * @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
312
	 * @return bool
313
	 */
314
	static public function get_create_default_pages() {
315
		Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
316
		return Config::inst()->get('SiteTree', 'create_default_pages');
317
	}
318
	
319
	/**
320
	 * Fetches the {@link SiteTree} object that maps to a link.
321
	 *
322
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
323
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
324
	 *
325
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
326
	 * by a extension attached to {@link SiteTree}
327
	 *
328
	 * @param string $link  The link of the page to search for
329
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
330
	 * @return SiteTree
331
	 */
332
	static public function get_by_link($link, $cache = true) {
333
		if(trim($link, '/')) {
334
			$link = trim(Director::makeRelative($link), '/');
335
		} else {
336
			$link = RootURLController::get_homepage_link();
337
		}
338
		
339
		$parts = preg_split('|/+|', $link);
340
		
341
		// Grab the initial root level page to traverse down from.
342
		$URLSegment = array_shift($parts);
343
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
344
		if(self::config()->nested_urls) {
345
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
346
		}
347
		$sitetree = DataObject::get_one('SiteTree', $conditions, $cache);
348
		
349
		/// Fall back on a unique URLSegment for b/c.
350
		if(	!$sitetree
351
			&& self::config()->nested_urls
352
			&& $page = DataObject::get_one('SiteTree', array(
353
				'"SiteTree"."URLSegment"' => $URLSegment
354
			), $cache)
355
		) {
356
			return $page;
357
		}
358
		
359
		// Attempt to grab an alternative page from extensions.
360
		if(!$sitetree) {
361
			$parentID = self::config()->nested_urls ? 0 : null;
362
			
363 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...
364
				foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
365
			}
366
			
367
			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...
368
		}
369
		
370
		// Check if we have any more URL parts to parse.
371
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
372
		
373
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
374
		foreach($parts as $segment) {
375
			$next = DataObject::get_one('SiteTree', array(
376
					'"SiteTree"."URLSegment"' => $segment,
377
					'"SiteTree"."ParentID"' => $sitetree->ID
378
				),
379
				$cache
380
			);
381
			
382
			if(!$next) {
383
				$parentID = (int) $sitetree->ID;
384
				
385 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...
386
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
387
				}
388
				
389
				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...
390
			}
391
			
392
			$sitetree->destroy();
393
			$sitetree = $next;
394
		}
395
		
396
		return $sitetree;
397
	}
398
	
399
	/**
400
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
401
	 *
402
	 * @return array
403
	 */
404
	static public function page_type_classes() {
405
		$classes = ClassInfo::getValidSubClasses();
406
407
		$baseClassIndex = array_search('SiteTree', $classes);
408
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
409
410
		$kill_ancestors = array();
411
412
		// figure out if there are any classes we don't want to appear
413
		foreach($classes as $class) {
414
			$instance = singleton($class);
415
416
			// do any of the progeny want to hide an ancestor?
417
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
418
				// note for killing later
419
				$kill_ancestors[] = $ancestor_to_hide;
420
			}
421
		}
422
423
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
424
		// requirements
425
		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...
426
			$kill_ancestors = array_unique($kill_ancestors);
427
			foreach($kill_ancestors as $mark) {
428
				// unset from $classes
429
				$idx = array_search($mark, $classes);
430
				unset($classes[$idx]);
431
			}
432
		}
433
434
		return $classes;
435
	}
436
	
437
	/**
438
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
439
	 *
440
	 * @param array      $arguments
441
	 * @param string     $content
442
	 * @param TextParser $parser
443
	 * @return string
444
	 */
445
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
446
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
447
		
448
		if (
449
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
450
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
451
		) {
452
			 return; // There were no suitable matches at all.
453
		}
454
455
		$link = Convert::raw2att($page->Link());
456
		
457
		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...
458
			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...
459
		} else {
460
			return $link;
461
		}
462
	}
463
464
	/**
465
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
466
	 *
467
	 * @param string $action Optional controller action (method).
468
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
469
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
470
	 *                       append GET parameters.
471
	 * @return string
472
	 */
473
	public function Link($action = null) {
474
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
475
	}
476
	
477
	/**
478
	 * Get the absolute URL for this page, including protocol and host.
479
	 *
480
	 * @param string $action See {@link Link()}
481
	 * @return string
482
	 */
483
	public function AbsoluteLink($action = null) {
484
		if($this->hasMethod('alternateAbsoluteLink')) {
485
			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...
486
		} else {
487
			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 487 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...
488
		}
489
	}
490
	
491
	/**
492
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
493
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
494
	 *
495
	 * @param string $action See {@link Link()}
496
	 * @return string
497
	 */
498
	public function PreviewLink($action = null) {
499
		if($this->hasMethod('alternatePreviewLink')) {
500
			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...
501
		} else {
502
			return $this->AbsoluteLink($action);
503
		}
504
	}
505
	
506
	/**
507
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
508
	 *
509
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
510
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
511
	 * and returned in its full form.
512
	 *
513
	 * @uses RootURLController::get_homepage_link()
514
	 *
515
	 * @param string $action See {@link Link()}
516
	 * @return string
517
	 */
518
	public function RelativeLink($action = null) {
519
		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...
520
			$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...
521
			// If page is removed select parent from version history (for archive page view)
522
			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...
523
				$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...
524
			}
525
			$base = $parent->RelativeLink($this->URLSegment);
526
		} 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...
527
			// Unset base for root-level homepages.
528
			// Note: Homepages with action parameters (or $action === true)
529
			// need to retain their URLSegment.
530
			$base = null;
531
		} else {
532
			$base = $this->URLSegment;
533
		}
534
		
535
		$this->extend('updateRelativeLink', $base, $action);
536
		
537
		// Legacy support: If $action === true, retain URLSegment for homepages,
538
		// but don't append any action
539
		if($action === true) $action = null;
540
541
		return Controller::join_links($base, '/', $action);
542
	}
543
544
	/**
545
	 * Get the absolute URL for this page on the Live site.
546
	 *
547
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
548
	 * @return string
549
	 */
550
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
551
		$oldStage = Versioned::current_stage();
552
		Versioned::reading_stage('Live');
553
		$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...
554
			'"SiteTree"."ID"' => $this->ID
555
		));
556
		if($live) {
557
			$link = $live->AbsoluteLink();
558
			if($includeStageEqualsLive) $link .= '?stage=Live';
559
		} else {
560
			$link = null;
561
		}
562
563
		Versioned::reading_stage($oldStage);
564
		return $link;
565
	}
566
	
567
	/**
568
	 * Generates a link to edit this page in the CMS.
569
	 *
570
	 * @return string
571
	 */
572
	public function CMSEditLink() {
573
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
574
	}
575
	
576
		
577
	/**
578
	 * Return a CSS identifier generated from this page's link.
579
	 *
580
	 * @return string The URL segment
581
	 */
582
	public function ElementName() {
583
		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...
584
	}
585
	
586
	/**
587
	 * Returns true if this is the currently active page being used to handle this request.
588
	 *
589
	 * @return bool
590
	 */
591
	public function isCurrent() {
592
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
593
	}
594
	
595
	/**
596
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
597
	 * currently being viewed).
598
	 *
599
	 * @return bool
600
	 */
601
	public function isSection() {
602
		return $this->isCurrent() || (
603
			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...
604
		);
605
	}
606
	
607
	/**
608
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
609
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
610
	 * to external users.
611
	 *
612
	 * @return bool
613
	 */
614
	public function isOrphaned() {
615
		// Always false for root pages
616
		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...
617
		
618
		// Parent must exist and not be an orphan itself
619
		$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...
620
		return !$parent || !$parent->exists() || $parent->isOrphaned();
621
	}
622
	
623
	/**
624
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
625
	 *
626
	 * @return string
627
	 */
628
	public function LinkOrCurrent() {
629
		return $this->isCurrent() ? 'current' : 'link';
630
	}
631
	
632
	/**
633
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
634
	 *
635
	 * @return string
636
	 */
637
	public function LinkOrSection() {
638
		return $this->isSection() ? 'section' : 'link';
639
	}
640
	
641
	/**
642
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
643
	 * but in the current section.
644
	 *
645
	 * @return string
646
	 */
647
	public function LinkingMode() {
648
		if($this->isCurrent()) {
649
			return 'current';
650
		} elseif($this->isSection()) {
651
			return 'section';
652
		} else {
653
			return 'link';
654
		}
655
	}
656
	
657
	/**
658
	 * Check if this page is in the given current section.
659
	 *
660
	 * @param string $sectionName Name of the section to check
661
	 * @return bool True if we are in the given section
662
	 */
663
	public function InSection($sectionName) {
664
		$page = Director::get_current_page();
665
		while($page) {
666
			if($sectionName == $page->URLSegment)
667
				return true;
668
			$page = $page->Parent;
669
		}
670
		return false;
671
	}
672
673
	/**
674
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
675
	 * such behaviour.
676
	 *
677
	 * @param bool $doWrite Whether to write the new object before returning it
678
	 * @return self The duplicated object
679
	 */
680
	 public function duplicate($doWrite = true) {
681
		
682
		$page = parent::duplicate(false);
683
		$page->Sort = 0;
684
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
685
		
686
		if($doWrite) {
687
			$page->write();
688
689
			$page = $this->duplicateManyManyRelations($this, $page);
690
		}
691
		$this->invokeWithExtensions('onAfterDuplicate', $page);
692
		
693
		return $page;
694
	}
695
696
	/**
697
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
698
	 *
699
	 * @return self The duplicated object
700
	 */
701
	public function duplicateWithChildren() {
702
		$clone = $this->duplicate();
703
		$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...
704
705
		if($children) {
706
			foreach($children as $child) {
707
				$childClone = method_exists($child, 'duplicateWithChildren')
708
					? $child->duplicateWithChildren()
709
					: $child->duplicate();
710
				$childClone->ParentID = $clone->ID;
711
				$childClone->write();
712
			}
713
		}
714
715
		return $clone;
716
	}
717
718
	/**
719
	 * Duplicate this node and its children as a child of the node with the given ID
720
	 *
721
	 * @param int $id ID of the new node's new parent
722
	 */
723
	public function duplicateAsChild($id) {
724
		$newSiteTree = $this->duplicate();
725
		$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...
726
		$newSiteTree->Sort = 0;
727
		$newSiteTree->write();
728
	}
729
	
730
	/**
731
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
732
	 *
733
	 * @param int $maxDepth The maximum depth to traverse.
734
	 * @param boolean $unlinked Whether to link page titles.
735
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
736
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
737
	 * @return HTMLText The breadcrumb trail.
738
	 */
739
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
740
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
741
		$template = new SSViewer('BreadcrumbsTemplate');
742
		return $template->process($this->customise(new ArrayData(array(
743
			"Pages" => $pages,
744
			"Unlinked" => $unlinked
745
		))));
746
	}
747
748
749
	/**
750
	 * Returns a list of breadcrumbs for the current page.
751
	 *
752
	 * @param int $maxDepth The maximum depth to traverse.
753
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
754
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
755
	 *
756
	 * @return ArrayList
757
	*/
758
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
759
		$page = $this;
760
		$pages = array();
761
		
762
		while(
763
			$page
764
 			&& (!$maxDepth || count($pages) < $maxDepth)
765
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
766
 		) {
767
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
768
				$pages[] = $page;
769
			}
770
			
771
			$page = $page->Parent;
772
		}
773
774
		return new ArrayList(array_reverse($pages));
775
	}
776
777
778
	/**
779
	 * Make this page a child of another page.
780
	 *
781
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
782
	 *
783
	 * @param SiteTree|int $item Either the parent object, or the parent ID
784
	 */
785
	public function setParent($item) {
786
		if(is_object($item)) {
787
			if (!$item->exists()) $item->write();
788
			$this->setField("ParentID", $item->ID);
789
		} else {
790
			$this->setField("ParentID", $item);
791
		}
792
	}
793
 	
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...
794
	/**
795
	 * Get the parent of this page.
796
	 *
797
	 * @return SiteTree Parent of this page
798
	 */
799
	public function getParent() {
800
		if ($parentID = $this->getField("ParentID")) {
801
			return DataObject::get_by_id("SiteTree", $parentID);
802
		}
803
	}
804
805
	/**
806
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
807
	 *
808
	 * @param int $level The maximum amount of levels to traverse.
809
	 * @param string $separator Seperating string
810
	 * @return string The resulting string
811
	 */
812
	public function NestedTitle($level = 2, $separator = " - ") {
813
		$item = $this;
814
		while($item && $level > 0) {
815
			$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...
816
			$item = $item->Parent;
817
			$level--;
818
		}
819
		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...
820
	}
821
822
	/**
823
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
824
	 * the security model for an application.
825
	 *
826
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
827
	 * - Checks for existence of a method named "can<$perm>()" on the object
828
	 * - Calls decorators and only returns for FALSE "vetoes"
829
	 * - Falls back to {@link Permission::check()}
830
	 * - Does NOT check for many-many relations named "Can<$perm>"
831
	 *
832
	 * @uses DataObjectDecorator->can()
833
	 *
834
	 * @param string $perm The permission to be checked, such as 'View'
835
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
836
	 * @return bool True if the the member is allowed to do the given action
837
	 */
838
	public function can($perm, $member = null) {
839 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...
840
			$member = Member::currentUserID();
841
		}
842
843
		if($member && Permission::checkMember($member, "ADMIN")) return true;
844
		
845
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
846
			$method = 'can' . ucfirst($perm);
847
			return $this->$method($member);
848
		}
849
		
850
		$results = $this->extend('can', $member);
851
		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...
852
853
		return ($member && Permission::checkMember($member, $perm));
854
	}
855
856
	/**
857
	 * This function should return true if the current user can add children to this page. It can be overloaded to
858
	 * customise the security model for an application.
859
	 *
860
	 * Denies permission if any of the following conditions is true:
861
	 * - alternateCanAddChildren() on a extension returns false
862
	 * - canEdit() is not granted
863
	 * - There are no classes defined in {@link $allowed_children}
864
	 *
865
	 * @uses SiteTreeExtension->canAddChildren()
866
	 * @uses canEdit()
867
	 * @uses $allowed_children
868
	 *
869
	 * @param Member|int $member
870
	 * @return bool True if the current user can add children
871
	 */
872
	public function canAddChildren($member = null) {
873
		// Disable adding children to archived pages
874
		if($this->getIsDeletedFromStage()) {
875
			return false;
876
		}
877
878 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...
879
			$member = Member::currentUserID();
880
		}
881
882
		if($member && Permission::checkMember($member, "ADMIN")) return true;
883
		
884
		// Standard mechanism for accepting permission changes from extensions
885
		$extended = $this->extendedCan('canAddChildren', $member);
886
		if($extended !== null) return $extended;
887
		
888
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
889
	}
890
891
	/**
892
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
893
	 * security model for an application.
894
	 *
895
	 * Denies permission if any of the following conditions is true:
896
	 * - canView() on any extension returns false
897
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
898
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
899
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
900
	 *
901
	 * @uses DataExtension->canView()
902
	 * @uses ViewerGroups()
903
	 *
904
	 * @param Member|int $member
905
	 * @return bool True if the current user can view this page
906
	 */
907
	public function canView($member = null) {
908 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...
909
			$member = Member::currentUserID();
910
		}
911
912
		// admin override
913
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
914
		
915
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
916
		if($this->isOrphaned()) return false;
917
918
		// Standard mechanism for accepting permission changes from extensions
919
		$extended = $this->extendedCan('canView', $member);
920
		if($extended !== null) return $extended;
921
		
922
		// check for empty spec
923
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
924
925
		// check for inherit
926
		if($this->CanViewType == 'Inherit') {
927
			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...
928
			else return $this->getSiteConfig()->canViewPages($member);
929
		}
930
		
931
		// check for any logged-in users
932
		if($this->CanViewType == 'LoggedInUsers' && $member) {
933
			return true;
934
		}
935
		
936
		// check for specific groups
937
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
938
		if(
939
			$this->CanViewType == 'OnlyTheseUsers'
940
			&& $member
941
			&& $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...
942
		) return true;
943
		
944
		return false;
945
	}
946
947
	/**
948
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
949
	 * security model for an application.
950
	 *
951
	 * Denies permission if any of the following conditions is true:
952
	 * - canDelete() returns false on any extension
953
	 * - canEdit() returns false
954
	 * - any descendant page returns false for canDelete()
955
	 *
956
	 * @uses canDelete()
957
	 * @uses SiteTreeExtension->canDelete()
958
	 * @uses canEdit()
959
	 *
960
	 * @param Member $member
961
	 * @return bool True if the current user can delete this page
962
	 */
963
	public function canDelete($member = null) {
964 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...
965
		else if(is_numeric($member)) $memberID = $member;
966
		else $memberID = Member::currentUserID();
967
		
968
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
969
			return true;
970
		}
971
		
972
		// Standard mechanism for accepting permission changes from extensions
973
		$extended = $this->extendedCan('canDelete', $memberID);
974
		if($extended !== null) return $extended;
975
				
976
		// Regular canEdit logic is handled by can_edit_multiple
977
		$results = self::can_delete_multiple(array($this->ID), $memberID);
978
		
979
		// If this page no longer exists in stage/live results won't contain the page.
980
		// Fail-over to false
981
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
982
	}
983
984
	/**
985
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
986
	 * can be overloaded to customise the security model for an application.
987
	 *
988
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
989
	 * create beneath a parent is based on the ability to edit that parent page.
990
	 *
991
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
992
	 *
993
	 * @uses $can_create
994
	 * @uses DataExtension->canCreate()
995
	 *
996
	 * @param Member $member
997
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
998
	 *                       If a parent page is known, it will be checked for validity.
999
	 *                       If omitted, it will be assumed this is to be created as a top level page.
1000
	 * @return bool True if the current user can create pages on this class.
1001
	 */
1002
	public function canCreate($member = null, $context = array()) {
1003 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...
1004
			$member = Member::currentUserID();
1005
		}
1006
1007
		// Check parent (custom canCreate option for SiteTree)
1008
		// Block children not allowed for this parent type
1009
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1010
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1011
			return false;
1012
		}
1013
1014
		// Check permission
1015
		if($member && Permission::checkMember($member, "ADMIN")) {
1016
			return true;
1017
		}
1018
1019
		// Standard mechanism for accepting permission changes from extensions
1020
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1021
		if($extended !== null) {
1022
			return $extended;
1023
		}
1024
1025
		// Fall over to inherited permissions
1026
		if($parent) {
1027
			return $parent->canAddChildren($member);
1028
		} else {
1029
			// This doesn't necessarily mean we are creating a root page, but that
1030
			// we don't know if there is a parent, so default to this permission
1031
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1032
		}
1033
	}
1034
1035
	/**
1036
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1037
	 * security model for an application.
1038
	 *
1039
	 * Denies permission if any of the following conditions is true:
1040
	 * - canEdit() on any extension returns false
1041
	 * - canView() return false
1042
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1043
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1044
	 *   CMS_Access_CMSMAIN permission code
1045
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1046
	 *
1047
	 * @uses canView()
1048
	 * @uses EditorGroups()
1049
	 * @uses DataExtension->canEdit()
1050
	 *
1051
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1052
	 *                       unit tests)
1053
	 * @return bool True if the current user can edit this page
1054
	 */
1055
	public function canEdit($member = null) {
1056 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...
1057
		else if(is_numeric($member)) $memberID = $member;
1058
		else $memberID = Member::currentUserID();
1059
		
1060
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
1061
		
1062
		// Standard mechanism for accepting permission changes from extensions
1063
		$extended = $this->extendedCan('canEdit', $memberID);
1064
		if($extended !== null) return $extended;
1065
1066
		if($this->ID) {
1067
			// Regular canEdit logic is handled by can_edit_multiple
1068
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1069
1070
			// If this page no longer exists in stage/live results won't contain the page.
1071
			// Fail-over to false
1072
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1073
			
1074
		// Default for unsaved pages
1075
		} else {
1076
			return $this->getSiteConfig()->canEditPages($member);
1077
		}
1078
	}
1079
1080
	/**
1081
	 * @deprecated
1082
	 */
1083
	public function canDeleteFromLive($member = null) {
1084
		Deprecation::notice('4.0', 'Use canUnpublish');
1085
		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...
1086
	}
1087
1088
	/**
1089
	 * Stub method to get the site config, unless the current class can provide an alternate.
1090
	 *
1091
	 * @return SiteConfig
1092
	 */
1093
	public function getSiteConfig() {
1094
		
1095
		if($this->hasMethod('alternateSiteConfig')) {
1096
			$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...
1097
			if($altConfig) return $altConfig;
1098
		}
1099
		
1100
		return SiteConfig::current_site_config();
1101
	}
1102
1103
	/**
1104
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1105
	 * can_(perm)_multiple method for efficiency.
1106
	 *
1107
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1108
	 * @param array           $ids           An array of page IDs
1109
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1110
	 *                                       to 'SiteTree::can_(permission)_multiple'
1111
	 */
1112
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1113
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1114
		
1115
		if(is_callable($batchCallback)) {
1116
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1117
		} else {
1118
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1119
				. "with callback '$batchCallback'", E_USER_WARNING);
1120
		}
1121
	}
1122
1123
	/**
1124
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1125
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1126
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1127
	 * efficiently.
1128
	 *
1129
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1130
	 * property to FALSE.
1131
	 *
1132
	 * @param array  $ids              Of {@link SiteTree} IDs
1133
	 * @param int    $memberID         Member ID
1134
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1135
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1136
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1137
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1138
	 * @param bool   $useCached
1139
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1140
	 */
1141
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1142
												  $globalPermission = null, $useCached = true) {
1143
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1144
1145
		// Sanitise the IDs
1146
		$ids = array_filter($ids, 'is_numeric');
1147
		
1148
		// This is the name used on the permission cache
1149
		// converts something like 'CanEditType' to 'edit'.
1150
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1151
1152
		// Default result: nothing editable
1153
		$result = array_fill_keys($ids, false);
1154
		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...
1155
1156
			// Look in the cache for values
1157
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1158
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1159
			
1160
				// If we can't find everything in the cache, then look up the remainder separately
1161
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1162
				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...
1163
					$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 1143 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...
1164
				}
1165
				return $cachedValues;
1166
			}
1167
		
1168
			// If a member doesn't have a certain permission then they can't edit anything
1169
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1170
				return $result;
1171
			}
1172
1173
			// Placeholder for parameterised ID list
1174
			$idPlaceholders = DB::placeholders($ids);
1175
1176
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1177
			// be enabled
1178
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1179
		
1180
			// Get the groups that the given member belongs to
1181
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1182
			$SQL_groupList = implode(", ", $groupIDs);
1183
			if (!$SQL_groupList) $SQL_groupList = '0';
1184
			
1185
			$combinedStageResult = array();
1186
1187
			foreach(array('Stage', 'Live') as $stage) {
1188
				// Start by filling the array with the pages that actually exist
1189
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1190
				
1191
				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...
1192
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1193
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1194
				} else {
1195
					$stageIds = array();
1196
				}
1197
				$result = array_fill_keys($stageIds, false);
1198
				
1199
				// Get the uninherited permissions
1200
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1201
					->where(array(
1202
						"(\"$typeField\" = 'LoggedInUsers' OR
1203
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1204
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1205
						=> $ids
1206
					))
1207
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1208
				
1209
				if($uninheritedPermissions) {
1210
					// Set all the relevant items in $result to true
1211
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1212
				}
1213
1214
				// Get permissions that are inherited
1215
				$potentiallyInherited = Versioned::get_by_stage(
1216
					"SiteTree",
1217
					$stage,
1218
					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...
1219
				);
1220
1221
				if($potentiallyInherited) {
1222
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1223
					// then see which ones the user has permission on
1224
					$groupedByParent = array();
1225
					foreach($potentiallyInherited as $item) {
1226
						if($item->ParentID) {
1227
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1228
							$groupedByParent[$item->ParentID][] = $item->ID;
1229
						} else {
1230
							// Might return different site config based on record context, e.g. when subsites module
1231
							// is used
1232
							$siteConfig = $item->getSiteConfig();
1233
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1234
						}
1235
					}
1236
1237
					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...
1238
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1239
						if($actuallyInherited) {
1240
							$parentIDs = array_keys(array_filter($actuallyInherited));
1241
							foreach($parentIDs as $parentID) {
1242
								// Set all the relevant items in $result to true
1243
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1244
							}
1245
						}
1246
					}
1247
				}
1248
				
1249
				$combinedStageResult = $combinedStageResult + $result;
1250
				
1251
			}
1252
		}
1253
1254
		if(isset($combinedStageResult)) {
1255
			// Cache the results
1256
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1257
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1258
1259
			return $combinedStageResult;
1260
		} else {
1261
			return array();
1262
		}
1263
	}
1264
1265
	/**
1266
	 * Get the 'can edit' information for a number of SiteTree pages.
1267
	 *
1268
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1269
	 * @param int   $memberID  ID of member
1270
	 * @param bool  $useCached Return values from the permission cache if they exist
1271
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1272
	 *                         edited
1273
	 */
1274
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1275
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1276
	}
1277
1278
	/**
1279
	 * Get the 'can edit' information for a number of SiteTree pages.
1280
	 *
1281
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1282
	 * @param int   $memberID  ID of member
1283
	 * @param bool  $useCached Return values from the permission cache if they exist
1284
	 * @return array
1285
	 */
1286
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1287
		$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...
1288
		$result = array_fill_keys($ids, false);
1289
		$cacheKey = "delete-$memberID";
1290
		
1291
		// Look in the cache for values
1292
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1293
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1294
			
1295
			// If we can't find everything in the cache, then look up the remainder separately
1296
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1297
			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...
1298
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1299
					+ $cachedValues;
1300
			}
1301
			return $cachedValues;
1302
		}
1303
1304
		// You can only delete pages that you can edit
1305
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1306
		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...
1307
		
1308
			// You can only delete pages whose children you can delete
1309
			$editablePlaceholders = DB::placeholders($editableIDs);
1310
			$childRecords = SiteTree::get()->where(array(
1311
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1312
			));
1313
			if($childRecords) {
1314
				$children = $childRecords->map("ID", "ParentID");
1315
1316
				// Find out the children that can be deleted
1317
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1318
				
1319
				// Get a list of all the parents that have no undeletable children
1320
				$deletableParents = array_fill_keys($editableIDs, true);
1321
				foreach($deletableChildren as $id => $canDelete) {
1322
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1323
				}
1324
1325
				// Use that to filter the list of deletable parents that have children
1326
				$deletableParents = array_keys($deletableParents);
1327
1328
				// Also get the $ids that don't have children
1329
				$parents = array_unique($children->values());
1330
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1331
1332
				// Combine the two
1333
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1334
1335
			} else {
1336
				$deletable = $editableIDs;
1337
			}
1338
		} else {
1339
			$deletable = array();
1340
		}
1341
		
1342
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1343
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1344
	}
1345
1346
	/**
1347
	 * Collate selected descendants of this page.
1348
	 *
1349
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1350
	 * $collator array.
1351
	 *
1352
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1353
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1354
	 * @return bool
1355
	 */
1356
	public function collateDescendants($condition, &$collator) {
1357
		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...
1358
			foreach($children as $item) {
1359
				if(eval("return $condition;")) $collator[] = $item;
1360
				$item->collateDescendants($condition, $collator);
1361
			}
1362
			return true;
1363
		}
1364
	}
1365
1366
	/**
1367
	 * Return the title, description, keywords and language metatags.
1368
	 *
1369
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1370
	 *
1371
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1372
	 * @return string The XHTML metatags
1373
	 */
1374
	public function MetaTags($includeTitle = true) {
1375
		$tags = "";
1376
		if($includeTitle === true || $includeTitle == 'true') {
1377
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1378
		}
1379
1380
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1381
		if (!empty($generator)) {
1382
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1383
		}
1384
1385
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1386
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1387
		if($this->MetaDescription) {
1388
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1389
		}
1390
		if($this->ExtraMeta) {
1391
			$tags .= $this->ExtraMeta . "\n";
1392
		}
1393
		
1394
		if(Permission::check('CMS_ACCESS_CMSMain')
1395
			&& in_array('CMSPreviewable', class_implements($this))
1396
			&& !$this instanceof ErrorPage
1397
			&& $this->ID > 0
1398
		) {
1399
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1400
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1401
		}
1402
1403
		$this->extend('MetaTags', $tags);
1404
1405
		return $tags;
1406
	}
1407
1408
	/**
1409
	 * Returns the object that contains the content that a user would associate with this page.
1410
	 *
1411
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1412
	 * return the page that is linked to.
1413
	 *
1414
	 * @return $this
1415
	 */
1416
	public function ContentSource() {
1417
		return $this;
1418
	}
1419
1420
	/**
1421
	 * Add default records to database.
1422
	 *
1423
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1424
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1425
	 */
1426
	public function requireDefaultRecords() {
1427
		parent::requireDefaultRecords();
1428
		
1429
		// default pages
1430
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1431
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1432
				$homepage = new Page();
1433
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1434
				$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>');
1435
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1436
				$homepage->Sort = 1;
1437
				$homepage->write();
1438
				$homepage->publish('Stage', 'Live');
1439
				$homepage->flushCache();
1440
				DB::alteration_message('Home page created', 'created');
1441
			}
1442
1443
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1444
				$aboutus = new Page();
1445
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1446
				$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>');
1447
				$aboutus->Sort = 2;
1448
				$aboutus->write();
1449
				$aboutus->publish('Stage', 'Live');
1450
				$aboutus->flushCache();
1451
				DB::alteration_message('About Us page created', 'created');
1452
1453
				$contactus = new Page();
1454
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1455
				$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>');
1456
				$contactus->Sort = 3;
1457
				$contactus->write();
1458
				$contactus->publish('Stage', 'Live');
1459
				$contactus->flushCache();
1460
				DB::alteration_message('Contact Us page created', 'created');
1461
			}
1462
		}
1463
		
1464
		// schema migration
1465
		// @todo Move to migration task once infrastructure is implemented
1466
		if($this->class == 'SiteTree') {
1467
			$conn = DB::get_schema();
1468
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1469
			if($conn->hasField('SiteTree' ,'Viewers')) {
1470
				$task = new UpgradeSiteTreePermissionSchemaTask();
1471
				$task->run(new SS_HTTPRequest('GET','/'));
1472
			}
1473
		}
1474
	}
1475
1476
	protected function onBeforeWrite() {
1477
		parent::onBeforeWrite();
1478
1479
		// If Sort hasn't been set, make this page come after it's siblings
1480
		if(!$this->Sort) {
1481
			$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...
1482
			$this->Sort = DB::prepared_query(
1483
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1484
				array($parentID)
1485
			)->value();
1486
		}
1487
1488
		// If there is no URLSegment set, generate one from Title
1489
		$defaultSegment = $this->generateURLSegment(_t(
1490
			'CMSMain.NEWPAGE',
1491
			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...
1492
		));
1493
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1494
			$this->URLSegment = $this->generateURLSegment($this->Title);
1495
		} else if($this->isChanged('URLSegment', 2)) {
1496
			// Do a strict check on change level, to avoid double encoding caused by
1497
			// bogus changes through forceChange()
1498
			$filter = URLSegmentFilter::create();
1499
			$this->URLSegment = $filter->filter($this->URLSegment);
1500
			// If after sanitising there is no URLSegment, give it a reasonable default
1501
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1502
		}
1503
		
1504
		// Ensure that this object has a non-conflicting URLSegment value.
1505
		$count = 2;
1506
		while(!$this->validURLSegment()) {
1507
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1508
			$count++;
1509
		}
1510
1511
		$this->syncLinkTracking();
1512
1513
		// Check to see if we've only altered fields that shouldn't affect versioning
1514
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1515
		$changedFields = array_keys($this->getChangedFields(true, 2));
1516
1517
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1518
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1519
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1520
1521
		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...
1522
			// This will have the affect of preserving the versioning
1523
			$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...
1524
		}
1525
	}
1526
	
1527
	public function syncLinkTracking() {
1528
		$this->extend('augmentSyncLinkTracking');
1529
	}
1530
	
1531
	public function onAfterWrite() {
1532
		// Need to flush cache to avoid outdated versionnumber references
1533
		$this->flushCache();
1534
		
1535
		$linkedPages = $this->VirtualPages();
1536
		if($linkedPages) {
1537
			// The only way after a write() call to determine if it was triggered by a writeWithoutVersion(),
1538
			// which we have to pass on to the virtual page writes as well.
1539
			$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...
1540
			$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...
1541
			foreach($linkedPages as $page) {
1542
				 $page->copyFrom($page->CopyContentFrom());
1543
				 if($withoutVersion) $page->writeWithoutVersion();
1544
				 else $page->write();
1545
			}
1546
		}
1547
		
1548
		parent::onAfterWrite();
1549
	}
1550
	
1551
	public function onBeforeDelete() {
1552
		parent::onBeforeDelete();
1553
		
1554
		// If deleting this page, delete all its children.
1555
		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...
1556
			foreach($children as $child) {
1557
				$child->delete();
1558
			}
1559
		}
1560
	}
1561
	
1562
	public function onAfterDelete() {
1563
		// Need to flush cache to avoid outdated versionnumber references
1564
		$this->flushCache();
1565
		
1566
		// Need to mark pages depending to this one as broken
1567
		$dependentPages = $this->DependentPages();
1568
		if($dependentPages) foreach($dependentPages as $page) {
1569
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1570
			$page->write();
1571
		}
1572
		
1573
		parent::onAfterDelete();
1574
	}
1575
1576
	public function flushCache($persistent = true) {
1577
		parent::flushCache($persistent);
1578
		$this->_cache_statusFlags = null;
1579
	}
1580
	
1581
	public function validate() {
1582
		$result = parent::validate();
1583
1584
		// Allowed children validation
1585
		$parent = $this->getParent();
1586
		if($parent && $parent->exists()) {
1587
			// No need to check for subclasses or instanceof, as allowedChildren() already
1588
			// deconstructs any inheritance trees already.
1589
			$allowed = $parent->allowedChildren();
1590
			$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...
1591
			if(!in_array($subject->ClassName, $allowed)) {
1592
				
1593
				$result->error(
1594
					_t(
1595
						'SiteTree.PageTypeNotAllowed',
1596
						'Page type "{type}" not allowed as child of this parent page',
1597
						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...
1598
					),
1599
					'ALLOWED_CHILDREN'
1600
				);
1601
			}
1602
		}
1603
1604
		// "Can be root" validation
1605 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...
1606
			$result->error(
1607
				_t(
1608
					'SiteTree.PageTypNotAllowedOnRoot',
1609
					'Page type "{type}" is not allowed on the root level',
1610
					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...
1611
				),
1612
				'CAN_BE_ROOT'
1613
			);
1614
		}
1615
		
1616
		return $result;
1617
	}
1618
	
1619
	/**
1620
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1621
	 * checks for:
1622
	 *  - A page with the same URLSegment that has a conflict
1623
	 *  - Conflicts with actions on the parent page
1624
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1625
	 *
1626
	 * @return bool
1627
	 */
1628
	public function validURLSegment() {
1629
		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...
1630
			if($controller = ModelAsController::controller_for($parent)) {
1631
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1632
			}
1633
		}
1634
		
1635
		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...
1636
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1637
		}
1638
		
1639
		// Filters by url, id, and parent
1640
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1641
		if($this->ID) {
1642
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1643
		}
1644
		if(self::config()->nested_urls) {
1645
			$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...
1646
		}
1647
		
1648
		$votes = array_filter(
1649
			(array)$this->extend('augmentValidURLSegment'),
1650
			function($v) {return !is_null($v);}
1651
		);
1652
		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...
1653
			return min($votes);
1654
		}
1655
1656
		// Check existence
1657
		$existingPage = DataObject::get_one('SiteTree', $filter);
1658
		if ($existingPage) return false;
1659
1660
		return !($existingPage);
1661
		}
1662
		
1663
	/**
1664
	 * Generate a URL segment based on the title provided.
1665
	 *
1666
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1667
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1668
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1669
	 * scratch, or incrementally modify the generated URL.
1670
	 *
1671
	 * @param string $title Page title
1672
	 * @return string Generated url segment
1673
	 */
1674
	public function generateURLSegment($title){
1675
		$filter = URLSegmentFilter::create();
1676
		$t = $filter->filter($title);
1677
		
1678
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1679
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1680
		
1681
		// Hook for extensions
1682
		$this->extend('updateURLSegment', $t, $title);
1683
		
1684
		return $t;
1685
	}
1686
	
1687
	/**
1688
	 * Gets the URL segment for the latest draft version of this page.
1689
	 *
1690
	 * @return string
1691
	 */
1692
	public function getStageURLSegment() {
1693
		$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...
1694
			'"SiteTree"."ID"' => $this->ID
1695
		));
1696
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1697
	}
1698
	
1699
	/**
1700
	 * Gets the URL segment for the currently published version of this page.
1701
	 *
1702
	 * @return string
1703
	 */
1704
	public function getLiveURLSegment() {
1705
		$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...
1706
			'"SiteTree"."ID"' => $this->ID
1707
		));
1708
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1709
	}
1710
	
1711
	/**
1712
	 * Rewrites any linked images on this page.
1713
	 * Non-image files should be linked via shortcodes
1714
	 * Triggers the onRenameLinkedAsset action on extensions.
1715
	 * TODO: This doesn't work for HTMLText fields on other tables.
1716
	 */
1717
	public function rewriteFileLinks() {
1718
		// Update the content without actually creating a new version
1719
		foreach(array("SiteTree_Live", "SiteTree") as $table) {
1720
			// Published site
1721
			$published = DB::prepared_query(
1722
				"SELECT * FROM  \"$table\" WHERE \"ID\" = ?",
1723
				array($this->ID)
1724
			)->record();
1725
			$origPublished = $published;
1726
1727
			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...
1728
				// Skip if non HTML or if empty
1729
				if ($fieldType !== 'HTMLText' || empty($published[$fieldName])) {
1730
					continue;
1731
				}
1732
1733
				// Regenerate content
1734
				$content = Image::regenerate_html_links($published[$fieldName]);
1735
				if($content === $published[$fieldName]) {
1736
					continue;
1737
				}
1738
1739
				$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
1740
				DB::prepared_query($query, array($content, $this->ID));
1741
1742
				// Tell static caching to update itself
1743
				if($table == 'SiteTree_Live') {
1744
					$publishedClass = $origPublished['ClassName'];
1745
					$origPublishedObj = new $publishedClass($origPublished);
1746
					$this->invokeWithExtensions('onRenameLinkedAsset', $origPublishedObj);
1747
				}
1748
			}
1749
		}
1750
	}
1751
	
1752
	/**
1753
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1754
	 *
1755
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1756
	 * @return ArrayList
1757
	 */
1758
	public function DependentPages($includeVirtuals = true) {
1759
		if(class_exists('Subsite')) {
1760
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1761
			Subsite::disable_subsite_filter(true);
1762
		}
1763
		
1764
		// Content links
1765
		$items = new ArrayList();
1766
1767
		// We merge all into a regular SS_List, because DataList doesn't support merge
1768
		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...
1769
			$linkList = new ArrayList();
1770
			foreach($contentLinks as $item) {
1771
				$item->DependentLinkType = 'Content link';
1772
				$linkList->push($item);
1773
			}
1774
			$items->merge($linkList);
1775
		}
1776
		
1777
		// Virtual pages
1778
		if($includeVirtuals) {
1779
			$virtuals = $this->VirtualPages();
1780
			if($virtuals) {
1781
				$virtualList = new ArrayList();
1782
				foreach($virtuals as $item) {
1783
					$item->DependentLinkType = 'Virtual page';
1784
					$virtualList->push($item);
1785
				}
1786
				$items->merge($virtualList);
1787
			}
1788
		}
1789
1790
		// Redirector pages
1791
		$redirectors = RedirectorPage::get()->where(array(
1792
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1793
			'"RedirectorPage"."LinkToID"' => $this->ID
1794
		));
1795
		if($redirectors) {
1796
			$redirectorList = new ArrayList();
1797
			foreach($redirectors as $item) {
1798
				$item->DependentLinkType = 'Redirector page';
1799
				$redirectorList->push($item);
1800
			}
1801
			$items->merge($redirectorList);
1802
		}
1803
1804
		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...
1805
		
1806
		return $items;
1807
	}
1808
1809
	/**
1810
	 * Return all virtual pages that link to this page.
1811
	 *
1812
	 * @return DataList
1813
	 */
1814
	public function VirtualPages() {
1815
		
1816
		// Ignore new records
1817
		if(!$this->ID) return null;
1818
		
1819
		// Check subsite virtual pages
1820
		// @todo Refactor out subsite module specific code
1821
		if(class_exists('Subsite')) {
1822
			return Subsite::get_from_all_subsites('VirtualPage', array(
1823
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1824
			));
1825
		}
1826
		
1827
		// Check existing virtualpages
1828
		if(class_exists('VirtualPage')) {
1829
			return VirtualPage::get()->where(array(
1830
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1831
			));
1832
		}
1833
		
1834
		return null;
1835
	}
1836
1837
	/**
1838
	 * Returns a FieldList with which to create the main editing form.
1839
	 *
1840
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1841
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1842
	 *
1843
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1844
	 * e.g. access control.
1845
	 *
1846
	 * @return FieldList The fields to be displayed in the CMS
1847
	 */
1848
	public function getCMSFields() {
1849
		require_once("forms/Form.php");
1850
		// Status / message
1851
		// Create a status message for multiple parents
1852
		if($this->ID && is_numeric($this->ID)) {
1853
			$linkedPages = $this->VirtualPages();
1854
1855
			$parentPageLinks = array();
1856
1857
			if($linkedPages->Count() > 0) {
1858
				foreach($linkedPages as $linkedPage) {
1859
					$parentPage = $linkedPage->Parent;
1860
					if($parentPage) {
1861
						if($parentPage->ID) {
1862
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1863
						} else {
1864
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1865
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1866
								"</a>";
1867
						}
1868
					}
1869
				}
1870
1871
				$lastParent = array_pop($parentPageLinks);
1872
				$parentList = "'$lastParent'";
1873
1874
				if(count($parentPageLinks) > 0) {
1875
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1876
						. $parentList;
1877
				}
1878
1879
				$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...
1880
					'SiteTree.APPEARSVIRTUALPAGES',
1881
					"This content also appears on the virtual pages in the {title} sections.",
1882
					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...
1883
				);
1884
			}
1885
		}
1886
1887
		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...
1888
			$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...
1889
		}
1890
1891
		$dependentNote = '';
1892
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1893
		
1894
		// Create a table for showing pages linked to this one
1895
		$dependentPages = $this->DependentPages();
1896
		$dependentPagesCount = $dependentPages->Count();
1897
		if($dependentPagesCount) {
1898
			$dependentColumns = array(
1899
				'Title' => $this->fieldLabel('Title'),
1900
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1901
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1902
			);
1903
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1904
			
1905
			$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>');
1906
			$dependentTable = GridField::create(
1907
				'DependentPages',
1908
				false,
1909
				$dependentPages
1910
			);
1911
			$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...
1912
				->setDisplayFields($dependentColumns)
1913
				->setFieldFormatting(array(
1914
					'Title' => function($value, &$item) {
1915
						return sprintf(
1916
							'<a href="admin/pages/edit/show/%d">%s</a>',
1917
							(int)$item->ID,
1918
							Convert::raw2xml($item->Title)
1919
						);
1920
					},
1921
					'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...
1922
						return sprintf(
1923
							'<a href="%s" target="_blank">%s</a>',
1924
							Convert::raw2xml($value),
1925
							Convert::raw2xml($value)
1926
						);
1927
					}
1928
				));
1929
		}
1930
		
1931
		$baseLink = Controller::join_links (
1932
			Director::absoluteBaseURL(),
1933
			(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...
1934
		);
1935
		
1936
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1937
			->setURLPrefix($baseLink)
1938
			->setDefaultURL($this->generateURLSegment(_t(
1939
				'CMSMain.NEWPAGE',
1940
				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...
1941
			)));
1942
		$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...
1943
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1944
			$helpText .= $helpText ? '<br />' : '';
1945
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1946
		}
1947
		$urlsegment->setHelpText($helpText);
1948
		
1949
		$fields = new FieldList(
1950
			$rootTab = new TabSet("Root",
1951
				$tabMain = new Tab('Main',
1952
					new TextField("Title", $this->fieldLabel('Title')),
1953
					$urlsegment,
1954
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1955
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1956
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1957
						array(
1958
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1959
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1960
						)
1961
					)->setHeadingLevel(4)
1962
				),
1963
				$tabDependent = new Tab('Dependent',
1964
					$dependentNote,
1965
					$dependentTable
1966
				)
1967
			)
1968
		);
1969
		$htmlField->addExtraClass('stacked');
1970
		
1971
		// Help text for MetaData on page content editor
1972
		$metaFieldDesc
1973
			->setRightTitle(
1974
				_t(
1975
					'SiteTree.METADESCHELP',
1976
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1977
				)
1978
			)
1979
			->addExtraClass('help');
1980
		$metaFieldExtra
1981
			->setRightTitle(
1982
				_t(
1983
					'SiteTree.METAEXTRAHELP',
1984
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1985
				)
1986
			)
1987
			->addExtraClass('help');
1988
1989
		// Conditional dependent pages tab
1990
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1991
		else $fields->removeFieldFromTab('Root', 'Dependent');
1992
		
1993
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1994
1995
		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...
1996
			$obsoleteWarning = _t(
1997
				'SiteTree.OBSOLETECLASS',
1998
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1999
				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...
2000
			);
2001
2002
			$fields->addFieldToTab(
2003
				"Root.Main",
2004
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2005
				"Title"
2006
			);
2007
		}
2008
2009
		if(file_exists(BASE_PATH . '/install.php')) {
2010
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2011
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2012
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2013
				. "</p>"), "Title");
2014
		}
2015
2016
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
2017
		$fields->setTabPathRewrites(array(
2018
			'/^Root\.Content\.Main$/' => 'Root.Main',
2019
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
2020
		));
2021
		
2022
		if(self::$runCMSFieldsExtensions) {
2023
			$this->extend('updateCMSFields', $fields);
2024
		}
2025
2026
		return $fields;
2027
	}
2028
	
2029
	
2030
	/**
2031
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2032
	 * for content-related fields.
2033
	 *
2034
	 * @return FieldList
2035
	 */
2036
	public function getSettingsFields() {
2037
		$groupsMap = array();
2038
		foreach(Group::get() as $group) {
2039
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2040
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2041
		}
2042
		asort($groupsMap);
2043
		
2044
		$fields = new FieldList(
2045
			$rootTab = new TabSet("Root",
2046
				$tabBehaviour = new Tab('Settings',
2047
					new DropdownField(
2048
						"ClassName",
2049
						$this->fieldLabel('ClassName'),
2050
						$this->getClassDropdown()
2051
					),
2052
					$parentTypeSelector = new CompositeField(
2053
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2054
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2055
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2056
						)),
2057
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
2058
					),
2059
					$visibility = new FieldGroup(
2060
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2061
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2062
					),
2063
					$viewersOptionsField = new OptionsetField(
2064
						"CanViewType",
2065
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2066
					),
2067
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2068
						->setSource($groupsMap)
2069
						->setAttribute(
2070
							'data-placeholder',
2071
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2072
						),
2073
					$editorsOptionsField = new OptionsetField(
2074
						"CanEditType",
2075
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2076
					),
2077
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2078
						->setSource($groupsMap)
2079
						->setAttribute(
2080
							'data-placeholder',
2081
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2082
						)
2083
				)
2084
			)
2085
		);
2086
		
2087
		$visibility->setTitle($this->fieldLabel('Visibility'));
2088
		
2089
2090
		// This filter ensures that the ParentID dropdown selection does not show this node,
2091
		// or its descendents, as this causes vanishing bugs
2092
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2093
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2094
		
2095
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2096
		
2097
		// Make page location fields read-only if the user doesn't have the appropriate permission
2098
		if(!Permission::check("SITETREE_REORGANISE")) {
2099
			$fields->makeFieldReadonly('ParentType');
2100
			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...
2101
				$fields->removeByName('ParentID');
2102
			} else {
2103
				$fields->makeFieldReadonly('ParentID');
2104
			}
2105
		}
2106
		
2107
		$viewersOptionsSource = array();
2108
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2109
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2110
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2111
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2112
		$viewersOptionsField->setSource($viewersOptionsSource);
2113
		
2114
		$editorsOptionsSource = array();
2115
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2116
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2117
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2118
		$editorsOptionsField->setSource($editorsOptionsSource);
2119
2120
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2121
			$fields->makeFieldReadonly($viewersOptionsField);
2122
			if($this->CanViewType == 'OnlyTheseUsers') {
2123
				$fields->makeFieldReadonly($viewerGroupsField);
2124
			} else {
2125
				$fields->removeByName('ViewerGroups');
2126
			}
2127
			
2128
			$fields->makeFieldReadonly($editorsOptionsField);
2129
			if($this->CanEditType == 'OnlyTheseUsers') {
2130
				$fields->makeFieldReadonly($editorGroupsField);
2131
			} else {
2132
				$fields->removeByName('EditorGroups');
2133
			}
2134
		}
2135
		
2136
		if(self::$runCMSFieldsExtensions) {
2137
			$this->extend('updateSettingsFields', $fields);
2138
		}
2139
		
2140
		return $fields;
2141
	}
2142
	
2143
	/**
2144
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2145
	 * @return array
2146
	 */
2147
	public function fieldLabels($includerelations = true) {
2148
		$cacheKey = $this->class . '_' . $includerelations;
2149
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2150
			$labels = parent::fieldLabels($includerelations);
2151
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2152
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2153
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2154
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2155
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2156
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2157
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2158
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2159
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2160
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2161
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2162
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2163
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2164
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2165
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2166
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2167
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2168
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2169
			$labels['LinkChangeNote'] = _t (
2170
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2171
			);
2172
			
2173
			if($includerelations){
2174
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2175
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2176
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2177
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2178
			}
2179
2180
			self::$_cache_field_labels[$cacheKey] = $labels;
2181
		}
2182
2183
		return self::$_cache_field_labels[$cacheKey];
2184
	}
2185
2186
	/**
2187
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2188
	 *
2189
	 * Frontend scripts and styles know how to handle the following FormFields:
2190
	 * - top-level FormActions appear as standalone buttons
2191
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2192
	 * - TabSet & Tabs appear as a drop ups
2193
	 * - FormActions within the Tab are restyled as links
2194
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2195
	 *
2196
	 * @return FieldList The available actions for this page.
2197
	 */
2198
	public function getCMSActions() {
2199
		$existsOnLive = $this->getExistsOnLive();
2200
2201
		// Major actions appear as buttons immediately visible as page actions.
2202
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset');
2203
2204
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2205
		$rootTabSet = new TabSet('ActionMenus');
2206
		$moreOptions = new Tab(
2207
			'MoreOptions',
2208
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2209
		);
2210
		$rootTabSet->push($moreOptions);
2211
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus');
2212
2213
		// Render page information into the "more-options" drop-up, on the top.
2214
		$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...
2215
			'"SiteTree"."ID"' => $this->ID
2216
		));
2217
		$moreOptions->push(
2218
			new LiteralField('Information',
2219
				$this->customise(array(
2220
					'Live' => $live,
2221
					'ExistsOnLive' => $existsOnLive
2222
				))->renderWith('SiteTree_Information')
2223
			)
2224
		);
2225
2226
		// "readonly"/viewing version that isn't the current version of the record
2227
		$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...
2228
			'"SiteTree"."ID"' => $this->ID
2229
		));
2230
		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...
2231
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2232
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2233
2234
			$actions = new FieldList(array($majorActions, $rootTabSet));
2235
2236
			// getCMSActions() can be extended with updateCMSActions() on a extension
2237
			$this->extend('updateCMSActions', $actions);
2238
2239
			return $actions;
2240
		}
2241
2242 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...
2243
			// "unpublish"
2244
			$moreOptions->push(
2245
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2246
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2247
					->addExtraClass('ss-ui-action-destructive')
2248
			);
2249
		}
2250
2251 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...
2252
			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...
2253
				// "rollback"
2254
				$moreOptions->push(
2255
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2256
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2257
				);
2258
			}
2259
		}
2260
2261
		if($this->canEdit()) {
2262
			if($this->getIsDeletedFromStage()) {
2263
				// The usual major actions are not available, so we provide alternatives here.
2264
				if($existsOnLive) {
2265
					// "restore"
2266
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2267
					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...
2268
						// "delete from live"
2269
						$majorActions->push(
2270
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2271
								->addExtraClass('ss-ui-action-destructive')
2272
						);
2273
					}
2274
				} else {
2275
					// Determine if we should force a restore to root (where once it was a subpage)
2276
					$restoreToRoot = $this->isParentArchived();
2277
					
2278
					// "restore"
2279
					$title = $restoreToRoot
2280
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2281
						: _t('CMSMain.RESTORE','Restore draft');
2282
					$description = $restoreToRoot
2283
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2284
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2285
					$majorActions->push(
2286
						FormAction::create('restore', $title)
2287
							->setDescription($description)
2288
							->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...
2289
							->setAttribute('data-icon', 'decline')
2290
					);
2291
				}
2292
			} else {
2293
				// Detect use of legacy actions
2294
				// {@see CMSMain::enabled_legacy_actions}
2295
				$legacy = CMSMain::config()->enabled_legacy_actions;
2296
				if(in_array('CMSBatchAction_Delete', $legacy)) {
2297
					Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
2298
					if($this->canDelete()) {
2299
						// delete
2300
						$moreOptions->push(
2301
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2302
								->addExtraClass('delete ss-ui-action-destructive')
2303
						);
2304
					}
2305
				} 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...
2306
					// "archive"
2307
					$moreOptions->push(
2308
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2309
							->setDescription(_t(
2310
								'SiteTree.BUTTONARCHIVEDESC',
2311
								'Unpublish and send to archive'
2312
							))
2313
							->addExtraClass('delete ss-ui-action-destructive')
2314
					);
2315
				}
2316
			
2317
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2318
				$majorActions->push(
2319
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2320
						->setAttribute('data-icon', 'accept')
2321
						->setAttribute('data-icon-alternate', 'addpage')
2322
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2323
				);
2324
			}
2325
		}
2326
2327
		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...
2328
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2329
			$majorActions->push(
2330
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2331
					->setAttribute('data-icon', 'accept')
2332
					->setAttribute('data-icon-alternate', 'disk')
2333
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2334
			);
2335
2336
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2337
			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...
2338
				$publish->addExtraClass('ss-ui-alternate');
2339
			}
2340
		}
2341
		
2342
		$actions = new FieldList(array($majorActions, $rootTabSet));
2343
		
2344
		// Hook for extensions to add/remove actions.
2345
		$this->extend('updateCMSActions', $actions);
2346
		
2347
		return $actions;
2348
	}
2349
	
2350
	/**
2351
	 * Publish this page.
2352
	 *
2353
	 * @uses SiteTreeExtension->onBeforePublish()
2354
	 * @uses SiteTreeExtension->onAfterPublish()
2355
	 * @return bool True if published
2356
	 */
2357
	public function doPublish() {
2358
		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...
2359
		
2360
		$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...
2361
			'"SiteTree"."ID"' => $this->ID
2362
		));
2363
		if(!$original) $original = new SiteTree();
2364
2365
		// Handle activities undertaken by extensions
2366
		$this->invokeWithExtensions('onBeforePublish', $original);
2367
		//$this->PublishedByID = Member::currentUser()->ID;
2368
		$this->write();
2369
		$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...
2370
2371
		DB::prepared_query('UPDATE "SiteTree_Live"
2372
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2373
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2374
			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...
2375
		);
2376
			
2377
		// Publish any virtual pages that might need publishing
2378
		$linkedPages = $this->VirtualPages();
2379
		if($linkedPages) foreach($linkedPages as $page) {
2380
			$page->copyFrom($page->CopyContentFrom());
2381
			$page->write();
2382
			if($page->getExistsOnLive()) $page->doPublish();
2383
		}
2384
		
2385
		// Need to update pages linking to this one as no longer broken, on the live site
2386
		$origMode = Versioned::get_reading_mode();
2387
		Versioned::reading_stage('Live');
2388
		foreach($this->DependentPages(false) as $page) {
2389
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2390
			$page->write();
2391
		}
2392
		Versioned::set_reading_mode($origMode);
2393
		
2394
		// Handle activities undertaken by extensions
2395
		$this->invokeWithExtensions('onAfterPublish', $original);
2396
		
2397
		return true;
2398
	}
2399
	
2400
	/**
2401
	 * Unpublish this page - remove it from the live site
2402
	 *
2403
	 * Overrides {@see Versioned::doUnpublish()}
2404
	 *
2405
	 * @uses SiteTreeExtension->onBeforeUnpublish()
2406
	 * @uses SiteTreeExtension->onAfterUnpublish()
2407
	 */
2408
	public function doUnpublish() {
2409
		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...
2410
		if(!$this->ID) return false;
2411
		
2412
		$this->invokeWithExtensions('onBeforeUnpublish', $this);
2413
		
2414
		$origStage = Versioned::current_stage();
2415
		Versioned::reading_stage('Live');
2416
2417
		// We should only unpublish virtualpages that exist on live
2418
		$virtualPages = $this->VirtualPages();
2419
2420
		// This way our ID won't be unset
2421
		$clone = clone $this;
2422
		$clone->delete();
2423
2424
		// Rewrite backlinks
2425
		$dependentPages = $this->DependentPages(false);
2426
		if($dependentPages) foreach($dependentPages as $page) {
2427
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2428
			$page->write();
2429
		}
2430
		Versioned::reading_stage($origStage);
2431
2432
		// Unpublish any published virtual pages
2433
		if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();
2434
2435
		// If we're on the draft site, then we can update the status.
2436
		// Otherwise, these lines will resurrect an inappropriate record
2437
		if(DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()
2438
			&& Versioned::current_stage() != 'Live') {
2439
			$this->write();
2440
		}
2441
2442
		$this->invokeWithExtensions('onAfterUnpublish', $this);
2443
2444
		return true;
2445
	}
2446
	
2447
	/**
2448
	 * Revert the draft changes: replace the draft content with the content on live
2449
	 */
2450
	public function doRevertToLive() {
2451
		$this->invokeWithExtensions('onBeforeRevertToLive', $this);
2452
2453
		$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...
2454
2455
		// Use a clone to get the updates made by $this->publish
2456
		$clone = DataObject::get_by_id("SiteTree", $this->ID);
2457
		$clone->writeWithoutVersion();
2458
2459
		// Need to update pages linking to this one as no longer broken
2460
		foreach($this->DependentPages(false) as $page) {
2461
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2462
			$page->write();
2463
		}
2464
		
2465
		$this->invokeWithExtensions('onAfterRevertToLive', $this);
2466
		return true;
2467
	}
2468
2469
	/**
2470
	 * Determine if this page references a parent which is archived, and not available in stage
2471
	 *
2472
	 * @return bool True if there is an archived parent
2473
	 */
2474
	protected function isParentArchived() {
2475
		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...
2476
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2477
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2478
				return true;
2479
			}
2480
		}
2481
		return false;
2482
	}
2483
	
2484
	/**
2485
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2486
	 *
2487
	 * @return self
2488
	 */
2489
	public function doRestoreToStage() {
2490
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2491
2492
		// Ensure that the parent page is restored, otherwise restore to root
2493
		if($this->isParentArchived()) {
2494
			$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...
2495
		}
2496
		
2497
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2498
		// create an empty record
2499
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2500
			$conn = DB::get_conn();
2501
			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...
2502
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2503
			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...
2504
		}
2505
		
2506
		$oldStage = Versioned::current_stage();
2507
		Versioned::reading_stage('Stage');
2508
		$this->forceChange();
2509
		$this->write();
2510
		
2511
		$result = DataObject::get_by_id($this->class, $this->ID);
2512
2513
		// Need to update pages linking to this one as no longer broken
2514
		foreach($result->DependentPages(false) as $page) {
2515
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2516
			$page->write();
2517
		}
2518
		
2519
		Versioned::reading_stage($oldStage);
2520
2521
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2522
		
2523
		return $result;
2524
	}
2525
2526
	/**
2527
	 * @deprecated
2528
	 */
2529
	public function doDeleteFromLive() {
2530
		Deprecation::notice("4.0", "Use doUnpublish instead");
2531
		return $this->doUnpublish();
2532
	}
2533
2534
	/**
2535
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2536
	 *
2537
	 * @return bool
2538
	 */
2539
	public function isNew() {
2540
		/**
2541
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2542
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2543
		 */
2544
		if(empty($this->ID)) return true;
2545
2546
		if(is_numeric($this->ID)) return false;
2547
2548
		return stripos($this->ID, 'new') === 0;
2549
	}
2550
2551
	/**
2552
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2553
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2554
	 * {@link SiteTree::$needs_permission}.
2555
	 *
2556
	 * @return array
2557
	 */
2558
	protected function getClassDropdown() {
2559
		$classes = self::page_type_classes();
2560
		$currentClass = null;
2561
		$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...
2562
		
2563
		$result = array();
2564
		foreach($classes as $class) {
2565
			$instance = singleton($class);
2566
2567
			// if the current page type is this the same as the class type always show the page type in the list
2568
			if ($this->ClassName != $instance->ClassName) {
2569
				if($instance instanceof HiddenClass) continue;
2570
				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...
2571
			}
2572
			
2573
			if($perms = $instance->stat('need_permission')) {
2574
				if(!$this->can($perms)) continue;
2575
			}
2576
2577
			$pageTypeName = $instance->i18n_singular_name();
2578
2579
			$currentClass = $class;
2580
			$result[$class] = $pageTypeName;
2581
2582
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2583
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2584
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2585
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2586
				$result[$class] = $result[$class] .  " ({$class})";
2587
			}
2588
		}
2589
		
2590
		// sort alphabetically, and put current on top
2591
		asort($result);
2592
		if($currentClass) {
2593
			$currentPageTypeName = $result[$currentClass];
2594
			unset($result[$currentClass]);
2595
			$result = array_reverse($result);
2596
			$result[$currentClass] = $currentPageTypeName;
2597
			$result = array_reverse($result);
2598
		}
2599
		
2600
		return $result;
2601
	}
2602
2603
	/**
2604
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2605
	 *
2606
	 * @return string[]
2607
	 */
2608
	public function allowedChildren() {
2609
		$allowedChildren = array();
2610
		$candidates = $this->stat('allowed_children');
2611
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2612
			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...
2613
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2614
				// Otherwise, the class and all its subclasses are allowed.
2615
				if(substr($candidate,0,1) == '*') {
2616
					$allowedChildren[] = substr($candidate,1);
2617
				} else {
2618
					$subclasses = ClassInfo::subclassesFor($candidate);
2619
					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...
2620
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2621
					}
2622
				}
2623
			}
2624
		}
2625
		
2626
		return $allowedChildren;
2627
	}
2628
2629
	/**
2630
	 * Returns the class name of the default class for children of this page.
2631
	 *
2632
	 * @return string
2633
	 */
2634
	public function defaultChild() {
2635
		$default = $this->stat('default_child');
2636
		$allowed = $this->allowedChildren();
2637
		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...
2638
			if(!$default || !in_array($default, $allowed))
2639
				$default = reset($allowed);
2640
			return $default;
2641
		}
2642
	}
2643
2644
	/**
2645
	 * Returns the class name of the default class for the parent of this page.
2646
	 *
2647
	 * @return string
2648
	 */
2649
	public function defaultParent() {
2650
		return $this->stat('default_parent');
2651
	}
2652
2653
	/**
2654
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2655
	 * Title field.
2656
	 *
2657
	 * @return string
2658
	 */
2659
	public function getMenuTitle(){
2660
		if($value = $this->getField("MenuTitle")) {
2661
			return $value;
2662
		} else {
2663
			return $this->getField("Title");
2664
		}
2665
	}
2666
2667
2668
	/**
2669
	 * Set the menu title for this page.
2670
	 *
2671
	 * @param string $value
2672
	 */
2673
	public function setMenuTitle($value) {
2674
		if($value == $this->getField("Title")) {
2675
			$this->setField("MenuTitle", null);
2676
		} else {
2677
			$this->setField("MenuTitle", $value);
2678
		}
2679
	}
2680
	
2681
	/**
2682
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2683
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2684
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2685
	 * the flags.
2686
	 *
2687
	 * Example (simple):
2688
	 *   "deletedonlive" => "Deleted"
2689
	 *
2690
	 * Example (with optional title attribute):
2691
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2692
	 *
2693
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2694
	 * @return array
2695
	 */
2696
	public function getStatusFlags($cached = true) {
2697
		if(!$this->_cache_statusFlags || !$cached) {
2698
			$flags = array();
2699
			if($this->getIsDeletedFromStage()) {
2700
				if($this->getExistsOnLive()) {
2701
					$flags['removedfromdraft'] = array(
2702
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2703
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2704
					);
2705
				} else {
2706
					$flags['archived'] = array(
2707
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2708
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2709
					);
2710
				}
2711
			} else if($this->getIsAddedToStage()) {
2712
				$flags['addedtodraft'] = array(
2713
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2714
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2715
				);
2716
			} else if($this->getIsModifiedOnStage()) {
2717
				$flags['modified'] = array(
2718
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2719
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2720
				);
2721
			}
2722
2723
			$this->extend('updateStatusFlags', $flags);
2724
2725
			$this->_cache_statusFlags = $flags;
2726
		}
2727
		
2728
		return $this->_cache_statusFlags;
2729
	}
2730
2731
	/**
2732
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2733
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2734
	 * publication status.
2735
	 *
2736
	 * @return string An HTML string ready to be directly used in a template
2737
	 */
2738
	public function getTreeTitle() {
2739
		// Build the list of candidate children
2740
		$children = array();
2741
		$candidates = static::page_type_classes();
2742
		foreach($this->allowedChildren() as $childClass) {
2743
			if(!in_array($childClass, $candidates)) continue;
2744
			$child = singleton($childClass);
2745
			if($child->canCreate(null, array('Parent' => $this))) {
2746
				$children[$childClass] = $child->i18n_singular_name();
2747
			}
2748
		}
2749
		$flags = $this->getStatusFlags();
2750
		$treeTitle = sprintf(
2751
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2752
			Convert::raw2att(Convert::raw2json($children)),
2753
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2754
		);
2755
		foreach($flags as $class => $data) {
2756
			if(is_string($data)) $data = array('text' => $data);
2757
			$treeTitle .= sprintf(
2758
				"<span class=\"badge %s\"%s>%s</span>",
2759
				'status-' . Convert::raw2xml($class),
2760
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2761
				Convert::raw2xml($data['text'])
2762
			);
2763
		}
2764
		
2765
		return $treeTitle;
2766
	}
2767
2768
	/**
2769
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2770
	 * we're currently inside, etc.
2771
	 *
2772
	 * @param int $level
2773
	 * @return SiteTree
2774
	 */
2775
	public function Level($level) {
2776
		$parent = $this;
2777
		$stack = array($parent);
2778
		while($parent = $parent->Parent) {
2779
			array_unshift($stack, $parent);
2780
		}
2781
2782
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2783
	}
2784
2785
	/**
2786
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2787
	 *
2788
	 * @return int
2789
	 */
2790
	public function getPageLevel() {
2791
		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...
2792
			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...
2793
		}
2794
		return 1;
2795
	}
2796
2797
	/**
2798
	 * Return the CSS classes to apply to this node in the CMS tree.
2799
	 *
2800
	 * @param string $numChildrenMethod
2801
	 * @return string
2802
	 */
2803
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2804
		$classes = sprintf('class-%s', $this->class);
2805
		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...
2806
			$classes .= " BrokenLink";
2807
		}
2808
2809
		if(!$this->canAddChildren()) {
2810
			$classes .= " nochildren";
2811
		}
2812
2813
		if(!$this->canEdit() && !$this->canAddChildren()) {
2814
			if (!$this->canView()) {
2815
				$classes .= " disabled";
2816
			} else {
2817
				$classes .= " edit-disabled";
2818
			}
2819
		}
2820
2821
		if(!$this->ShowInMenus) {
2822
			$classes .= " notinmenu";
2823
		}
2824
			
2825
		//TODO: Add integration
2826
		/*
2827
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2828
			$classes .= " untranslated ";
2829
		*/
2830
		$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...
2831
2832
		return $classes;
2833
	}
2834
2835
	/**
2836
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2837
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2838
	 *
2839
	 * @return bool
2840
	 */
2841
	public function getIsDeletedFromStage() {
2842
		if(!$this->ID) return true;
2843
		if($this->isNew()) return false;
2844
2845
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2846
2847
		// Return true for both completely deleted pages and for pages just deleted from stage
2848
		return !($stageVersion);
2849
	}
2850
	
2851
	/**
2852
	 * Return true if this page exists on the live site
2853
	 *
2854
	 * @return bool
2855
	 */
2856
	public function getExistsOnLive() {
2857
		return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2858
	}
2859
2860
	/**
2861
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2862
	 * unpublished changes to the draft site.
2863
	 *
2864
	 * @return bool
2865
	 */
2866
	public function getIsModifiedOnStage() {
2867
		// New unsaved pages could be never be published
2868
		if($this->isNew()) return false;
2869
		
2870
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2871
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2872
		
2873
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2874
		$this->extend('getIsModifiedOnStage', $isModified);
2875
		
2876
		return $isModified;
2877
	}
2878
	
2879
	/**
2880
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2881
	 * published.
2882
	 *
2883
	 * @return bool
2884
	 */
2885
	public function getIsAddedToStage() {
2886
		// New unsaved pages could be never be published
2887
		if($this->isNew()) return false;
2888
		
2889
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2890
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2891
2892
		return ($stageVersion && !$liveVersion);
2893
	}
2894
	
2895
	/**
2896
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2897
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2898
	 */
2899
	static public function disableCMSFieldsExtensions() {
2900
		self::$runCMSFieldsExtensions = false;
2901
	}
2902
	
2903
	/**
2904
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2905
	 * disableCMSFieldsExtensions().
2906
	 */
2907
	static public function enableCMSFieldsExtensions() {
2908
		self::$runCMSFieldsExtensions = true;
2909
	}
2910
2911
	public function providePermissions() {
2912
		return array(
2913
			'SITETREE_GRANT_ACCESS' => array(
2914
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2915
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2916
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2917
				'sort' => 100
2918
			),
2919
			'SITETREE_VIEW_ALL' => array(
2920
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2921
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2922
				'sort' => -100,
2923
				'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')
2924
			),
2925
			'SITETREE_EDIT_ALL' => array(
2926
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2927
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2928
				'sort' => -50,
2929
				'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')
2930
			),
2931
			'SITETREE_REORGANISE' => array(
2932
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2933
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2934
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2935
				'sort' => 100
2936
			),
2937
			'VIEW_DRAFT_CONTENT' => array(
2938
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2939
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2940
				'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.'),
2941
				'sort' => 100
2942
			)
2943
		);
2944
	}
2945
	
2946
	/**
2947
	 * Return the translated Singular name.
2948
	 *
2949
	 * @return string
2950
	 */
2951
	public function i18n_singular_name() {
2952
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2953
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2954
		return _t($class.'.SINGULARNAME', $this->singular_name());
2955
	}
2956
	
2957
	/**
2958
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2959
	 * picks it up for the wrong folder.
2960
	 *
2961
	 * @return array
2962
	 */
2963
	public function provideI18nEntities() {
2964
		$entities = parent::provideI18nEntities();
2965
		
2966
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2967
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;		
2968
2969
		$entities[$this->class . '.DESCRIPTION'] = array(
2970
			$this->stat('description'),
2971
			'Description of the page type (shown in the "add page" dialog)'
2972
		);
2973
2974
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2975
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2976
2977
		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...
2978
	}
2979
2980
	/**
2981
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2982
	 *
2983
	 * @return string
2984
	 */
2985
	public function getParentType() {
2986
		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...
2987
	}
2988
2989
	/**
2990
	 * Clear the permissions cache for SiteTree
2991
	 */
2992
	public static function reset() {
2993
		self::$cache_permissions = array();
2994
	}
2995
	
2996
	static public function on_db_reset() {
2997
		self::$cache_permissions = array();
2998
	}
2999
3000
}
3001