Completed
Push — master ( 849cd8...301702 )
by Hamish
18s
created

SiteTree::doPublish()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 42
rs 6.7272
cc 7
eloc 22
nc 9
nop 0
1
<?php
2
/**
3
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
4
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
5
 * draft and published states.
6
 *
7
 * <h2>URLs</h2>
8
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
9
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
10
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
11
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
12
 * {@link URLSegmentFilter::$default_allow_multibyte}.
13
 *
14
 * @property string URLSegment
15
 * @property string Title
16
 * @property string MenuTitle
17
 * @property string Content HTML content of the page.
18
 * @property string MetaDescription
19
 * @property string ExtraMeta
20
 * @property string ShowInMenus
21
 * @property string ShowInSearch
22
 * @property string Sort Integer value denoting the sort order.
23
 * @property string ReportClass
24
 * @property string CanViewType Type of restriction for viewing this object.
25
 * @property string CanEditType Type of restriction for editing this object.
26
 *
27
 * @method ManyManyList ViewerGroups List of groups that can view this object.
28
 * @method ManyManyList EditorGroups List of groups that can edit this object.
29
 *
30
 * @mixin Hierarchy
31
 * @mixin Versioned
32
 * @mixin SiteTreeLinkTracking
33
 *
34
 * @package cms
35
 */
36
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
37
38
	/**
39
	 * Indicates what kind of children this page type can have.
40
	 * This can be an array of allowed child classes, or the string "none" -
41
	 * indicating that this page type can't have children.
42
	 * If a classname is prefixed by "*", such as "*Page", then only that
43
	 * class is allowed - no subclasses. Otherwise, the class and all its
44
	 * subclasses are allowed.
45
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
46
	 *
47
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
48
	 *
49
	 * @config
50
	 * @var array
51
	 */
52
	private static $allowed_children = array("SiteTree");
53
54
	/**
55
	 * The default child class for this page.
56
	 * Note: Value might be cached, see {@link $allowed_chilren}.
57
	 *
58
	 * @config
59
	 * @var string
60
	 */
61
	private static $default_child = "Page";
62
63
	/**
64
	 * Default value for SiteTree.ClassName enum
65
	 * {@see DBClassName::getDefault}
66
	 *
67
	 * @config
68
	 * @var string
69
	 */
70
	private static $default_classname = "Page";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
71
72
	/**
73
	 * The default parent class for this page.
74
	 * Note: Value might be cached, see {@link $allowed_chilren}.
75
	 *
76
	 * @config
77
	 * @var string
78
	 */
79
	private static $default_parent = null;
80
81
	/**
82
	 * Controls whether a page can be in the root of the site tree.
83
	 * Note: Value might be cached, see {@link $allowed_chilren}.
84
	 *
85
	 * @config
86
	 * @var bool
87
	 */
88
	private static $can_be_root = true;
89
90
	/**
91
	 * List of permission codes a user can have to allow a user to create a page of this type.
92
	 * Note: Value might be cached, see {@link $allowed_chilren}.
93
	 *
94
	 * @config
95
	 * @var array
96
	 */
97
	private static $need_permission = null;
98
99
	/**
100
	 * If you extend a class, and don't want to be able to select the old class
101
	 * in the cms, set this to the old class name. Eg, if you extended Product
102
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
103
	 *
104
	 * @config
105
	 * @var string
106
	 */
107
	private static $hide_ancestor = null;
108
109
	private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
110
		"URLSegment" => "Varchar(255)",
111
		"Title" => "Varchar(255)",
112
		"MenuTitle" => "Varchar(100)",
113
		"Content" => "HTMLText",
114
		"MetaDescription" => "Text",
115
		"ExtraMeta" => "HTMLText('meta, link')",
116
		"ShowInMenus" => "Boolean",
117
		"ShowInSearch" => "Boolean",
118
		"Sort" => "Int",
119
		"HasBrokenFile" => "Boolean",
120
		"HasBrokenLink" => "Boolean",
121
		"ReportClass" => "Varchar",
122
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
123
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
124
	);
125
126
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
127
		"URLSegment" => true,
128
	);
129
130
	private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
131
		"ViewerGroups" => "Group",
132
		"EditorGroups" => "Group",
133
	);
134
135
	private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
136
		"VirtualPages" => "VirtualPage.CopyContentFrom"
137
	);
138
139
	private static $owned_by = array(
140
		"VirtualPages"
141
	);
142
143
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
144
		"Breadcrumbs" => "HTMLText",
145
		"LastEdited" => "SS_Datetime",
146
		"Created" => "SS_Datetime",
147
		'Link' => 'Text',
148
		'RelativeLink' => 'Text',
149
		'AbsoluteLink' => 'Text',
150
		'TreeTitle' => 'HTMLText',
151
	);
152
153
	private static $defaults = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
154
		"ShowInMenus" => 1,
155
		"ShowInSearch" => 1,
156
		"CanViewType" => "Inherit",
157
		"CanEditType" => "Inherit"
158
	);
159
160
	private static $versioning = array(
161
		"Stage",  "Live"
162
	);
163
164
	private static $default_sort = "\"Sort\"";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
165
166
	/**
167
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
168
	 * @var boolean
169
	 * @config
170
	 */
171
	private static $can_create = true;
172
173
	/**
174
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
175
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
176
	 *
177
	 * @see CMSMain::generateTreeStylingCSS()
178
	 * @config
179
	 * @var string
180
	 */
181
	private static $icon = null;
182
183
	/**
184
	 * @config
185
	 * @var string Description of the class functionality, typically shown to a user
186
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
187
	 */
188
	private static $description = 'Generic content page';
189
190
	private static $extensions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
191
		"Hierarchy",
192
		"Versioned",
193
		"SiteTreeLinkTracking"
194
	);
195
196
	private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
197
		'Title',
198
		'Content',
199
	);
200
201
	private static $field_labels = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
202
		'URLSegment' => 'URL'
203
	);
204
205
	/**
206
	 * @config
207
	 */
208
	private static $nested_urls = true;
209
210
	/**
211
	 * @config
212
	*/
213
	private static $create_default_pages = true;
214
215
	/**
216
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
217
	 */
218
	private static $runCMSFieldsExtensions = true;
219
220
	/**
221
	 * Cache for canView/Edit/Publish/Delete permissions.
222
	 * Keyed by permission type (e.g. 'edit'), with an array
223
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
224
	 * See {@link batch_permission_check()} for details.
225
	 */
226
	private static $cache_permissions = array();
227
228
	/**
229
	 * @config
230
	 * @var boolean
231
	 */
232
	private static $enforce_strict_hierarchy = true;
233
234
	/**
235
	 * The value used for the meta generator tag. Leave blank to omit the tag.
236
	 *
237
	 * @config
238
	 * @var string
239
	 */
240
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
241
242
	protected $_cache_statusFlags = null;
243
244
	/**
245
	 * Fetches the {@link SiteTree} object that maps to a link.
246
	 *
247
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
248
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
249
	 *
250
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
251
	 * by a extension attached to {@link SiteTree}
252
	 *
253
	 * @param string $link  The link of the page to search for
254
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
255
	 * @return SiteTree
256
	 */
257
	static public function get_by_link($link, $cache = true) {
258
		if(trim($link, '/')) {
259
			$link = trim(Director::makeRelative($link), '/');
260
		} else {
261
			$link = RootURLController::get_homepage_link();
262
		}
263
264
		$parts = preg_split('|/+|', $link);
265
266
		// Grab the initial root level page to traverse down from.
267
		$URLSegment = array_shift($parts);
268
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
269
		if(self::config()->nested_urls) {
270
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
271
		}
272
		$sitetree = DataObject::get_one('SiteTree', $conditions, $cache);
273
274
		/// Fall back on a unique URLSegment for b/c.
275
		if(	!$sitetree
276
			&& self::config()->nested_urls
277
			&& $page = DataObject::get_one('SiteTree', array(
278
				'"SiteTree"."URLSegment"' => $URLSegment
279
			), $cache)
280
		) {
281
			return $page;
282
		}
283
284
		// Attempt to grab an alternative page from extensions.
285
		if(!$sitetree) {
286
			$parentID = self::config()->nested_urls ? 0 : null;
287
288 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...
289
				foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
290
			}
291
292
			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...
293
		}
294
295
		// Check if we have any more URL parts to parse.
296
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
297
298
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
299
		foreach($parts as $segment) {
300
			$next = DataObject::get_one('SiteTree', array(
301
					'"SiteTree"."URLSegment"' => $segment,
302
					'"SiteTree"."ParentID"' => $sitetree->ID
303
				),
304
				$cache
305
			);
306
307
			if(!$next) {
308
				$parentID = (int) $sitetree->ID;
309
310 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...
311
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
312
				}
313
314
				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...
315
			}
316
317
			$sitetree->destroy();
318
			$sitetree = $next;
319
		}
320
321
		return $sitetree;
322
	}
323
324
	/**
325
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
326
	 *
327
	 * @return array
328
	 */
329
	public static function page_type_classes() {
330
		$classes = ClassInfo::getValidSubClasses();
331
332
		$baseClassIndex = array_search('SiteTree', $classes);
333
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
334
335
		$kill_ancestors = array();
336
337
		// figure out if there are any classes we don't want to appear
338
		foreach($classes as $class) {
339
			$instance = singleton($class);
340
341
			// do any of the progeny want to hide an ancestor?
342
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
343
				// note for killing later
344
				$kill_ancestors[] = $ancestor_to_hide;
345
			}
346
		}
347
348
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
349
		// requirements
350
		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...
351
			$kill_ancestors = array_unique($kill_ancestors);
352
			foreach($kill_ancestors as $mark) {
353
				// unset from $classes
354
				$idx = array_search($mark, $classes, true);
355
				if ($idx !== false) {
356
					unset($classes[$idx]);
357
				}
358
			}
359
		}
360
361
		return $classes;
362
	}
363
364
	/**
365
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
366
	 *
367
	 * @param array      $arguments
368
	 * @param string     $content
369
	 * @param TextParser $parser
370
	 * @return string
371
	 */
372
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
373
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
374
375
		if (
376
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
377
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
378
		) {
379
			 return null; // There were no suitable matches at all.
380
		}
381
382
		$link = Convert::raw2att($page->Link());
383
384
		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...
385
			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...
386
		} else {
387
			return $link;
388
		}
389
	}
390
391
	/**
392
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
393
	 *
394
	 * @param string $action Optional controller action (method).
395
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
396
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
397
	 *                       append GET parameters.
398
	 * @return string
399
	 */
400
	public function Link($action = null) {
401
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
402
	}
403
404
	/**
405
	 * Get the absolute URL for this page, including protocol and host.
406
	 *
407
	 * @param string $action See {@link Link()}
408
	 * @return string
409
	 */
410
	public function AbsoluteLink($action = null) {
411
		if($this->hasMethod('alternateAbsoluteLink')) {
412
			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...
413
		} else {
414
			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 414 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...
415
		}
416
	}
417
418
	/**
419
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
420
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
421
	 *
422
	 * @param string $action See {@link Link()}
423
	 * @return string
424
	 */
425
	public function PreviewLink($action = null) {
426
		if($this->hasMethod('alternatePreviewLink')) {
427
			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...
428
		} else {
429
			return $this->AbsoluteLink($action);
430
		}
431
	}
432
433
	/**
434
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
435
	 *
436
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
437
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
438
	 * and returned in its full form.
439
	 *
440
	 * @uses RootURLController::get_homepage_link()
441
	 *
442
	 * @param string $action See {@link Link()}
443
	 * @return string
444
	 */
445
	public function RelativeLink($action = null) {
446
		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...
447
			$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...
448
			// If page is removed select parent from version history (for archive page view)
449
			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...
450
				$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...
451
			}
452
			$base = $parent->RelativeLink($this->URLSegment);
453
		} 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...
454
			// Unset base for root-level homepages.
455
			// Note: Homepages with action parameters (or $action === true)
456
			// need to retain their URLSegment.
457
			$base = null;
458
		} else {
459
			$base = $this->URLSegment;
460
		}
461
462
		$this->extend('updateRelativeLink', $base, $action);
463
464
		// Legacy support: If $action === true, retain URLSegment for homepages,
465
		// but don't append any action
466
		if($action === true) $action = null;
467
468
		return Controller::join_links($base, '/', $action);
469
	}
470
471
	/**
472
	 * Get the absolute URL for this page on the Live site.
473
	 *
474
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
475
	 * @return string
476
	 */
477
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
478
		$oldStage = Versioned::get_stage();
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

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

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

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

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

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

Loading history...
480
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::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...
481
			'"SiteTree"."ID"' => $this->ID
482
		));
483
		if($live) {
484
			$link = $live->AbsoluteLink();
485
			if($includeStageEqualsLive) $link .= '?stage=Live';
486
		} else {
487
			$link = null;
488
		}
489
490
		Versioned::set_stage($oldStage);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

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

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

Loading history...
491
		return $link;
492
	}
493
494
	/**
495
	 * Generates a link to edit this page in the CMS.
496
	 *
497
	 * @return string
498
	 */
499
	public function CMSEditLink() {
500
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
501
	}
502
503
504
	/**
505
	 * Return a CSS identifier generated from this page's link.
506
	 *
507
	 * @return string The URL segment
508
	 */
509
	public function ElementName() {
510
		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...
511
	}
512
513
	/**
514
	 * Returns true if this is the currently active page being used to handle this request.
515
	 *
516
	 * @return bool
517
	 */
518
	public function isCurrent() {
519
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
520
	}
521
522
	/**
523
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
524
	 * currently being viewed).
525
	 *
526
	 * @return bool
527
	 */
528
	public function isSection() {
529
		return $this->isCurrent() || (
530
			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...
531
		);
532
	}
533
534
	/**
535
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
536
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
537
	 * to external users.
538
	 *
539
	 * @return bool
540
	 */
541
	public function isOrphaned() {
542
		// Always false for root pages
543
		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...
544
545
		// Parent must exist and not be an orphan itself
546
		$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...
547
		return !$parent || !$parent->exists() || $parent->isOrphaned();
548
	}
549
550
	/**
551
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
552
	 *
553
	 * @return string
554
	 */
555
	public function LinkOrCurrent() {
556
		return $this->isCurrent() ? 'current' : 'link';
557
	}
558
559
	/**
560
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
561
	 *
562
	 * @return string
563
	 */
564
	public function LinkOrSection() {
565
		return $this->isSection() ? 'section' : 'link';
566
	}
567
568
	/**
569
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
570
	 * but in the current section.
571
	 *
572
	 * @return string
573
	 */
574
	public function LinkingMode() {
575
		if($this->isCurrent()) {
576
			return 'current';
577
		} elseif($this->isSection()) {
578
			return 'section';
579
		} else {
580
			return 'link';
581
		}
582
	}
583
584
	/**
585
	 * Check if this page is in the given current section.
586
	 *
587
	 * @param string $sectionName Name of the section to check
588
	 * @return bool True if we are in the given section
589
	 */
590
	public function InSection($sectionName) {
591
		$page = Director::get_current_page();
592
		while($page) {
593
			if($sectionName == $page->URLSegment)
594
				return true;
595
			$page = $page->Parent;
596
		}
597
		return false;
598
	}
599
600
	/**
601
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
602
	 * such behaviour.
603
	 *
604
	 * @param bool $doWrite Whether to write the new object before returning it
605
	 * @return self The duplicated object
606
	 */
607
	 public function duplicate($doWrite = true) {
608
609
		$page = parent::duplicate(false);
610
		$page->Sort = 0;
611
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
612
613
		if($doWrite) {
614
			$page->write();
615
616
			$page = $this->duplicateManyManyRelations($this, $page);
617
		}
618
		$this->invokeWithExtensions('onAfterDuplicate', $page);
619
620
		return $page;
621
	}
622
623
	/**
624
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
625
	 *
626
	 * @return self The duplicated object
627
	 */
628
	public function duplicateWithChildren() {
629
		$clone = $this->duplicate();
630
		$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...
631
632
		if($children) {
633
			foreach($children as $child) {
634
				$childClone = method_exists($child, 'duplicateWithChildren')
635
					? $child->duplicateWithChildren()
636
					: $child->duplicate();
637
				$childClone->ParentID = $clone->ID;
638
				$childClone->write();
639
			}
640
		}
641
642
		return $clone;
643
	}
644
645
	/**
646
	 * Duplicate this node and its children as a child of the node with the given ID
647
	 *
648
	 * @param int $id ID of the new node's new parent
649
	 */
650
	public function duplicateAsChild($id) {
651
		$newSiteTree = $this->duplicate();
652
		$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...
653
		$newSiteTree->Sort = 0;
654
		$newSiteTree->write();
655
	}
656
657
	/**
658
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
659
	 *
660
	 * @param int $maxDepth The maximum depth to traverse.
661
	 * @param boolean $unlinked Whether to link page titles.
662
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
663
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
664
	 * @return HTMLText The breadcrumb trail.
665
	 */
666
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
667
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
668
		$template = new SSViewer('BreadcrumbsTemplate');
669
		return $template->process($this->customise(new ArrayData(array(
670
			"Pages" => $pages,
671
			"Unlinked" => $unlinked
672
		))));
673
	}
674
675
676
	/**
677
	 * Returns a list of breadcrumbs for the current page.
678
	 *
679
	 * @param int $maxDepth The maximum depth to traverse.
680
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
681
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
682
	 *
683
	 * @return ArrayList
684
	*/
685
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
686
		$page = $this;
687
		$pages = array();
688
689
		while(
690
			$page
691
 			&& (!$maxDepth || count($pages) < $maxDepth)
692
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
693
 		) {
694
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
695
				$pages[] = $page;
696
			}
697
698
			$page = $page->Parent;
699
		}
700
701
		return new ArrayList(array_reverse($pages));
702
	}
703
704
705
	/**
706
	 * Make this page a child of another page.
707
	 *
708
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
709
	 *
710
	 * @param SiteTree|int $item Either the parent object, or the parent ID
711
	 */
712
	public function setParent($item) {
713
		if(is_object($item)) {
714
			if (!$item->exists()) $item->write();
715
			$this->setField("ParentID", $item->ID);
716
		} else {
717
			$this->setField("ParentID", $item);
718
		}
719
	}
720
721
	/**
722
	 * Get the parent of this page.
723
	 *
724
	 * @return SiteTree Parent of this page
725
	 */
726
	public function getParent() {
727
		if ($parentID = $this->getField("ParentID")) {
728
			return DataObject::get_by_id("SiteTree", $parentID);
729
		}
730
	}
731
732
	/**
733
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
734
	 *
735
	 * @param int $level The maximum amount of levels to traverse.
736
	 * @param string $separator Seperating string
737
	 * @return string The resulting string
738
	 */
739
	public function NestedTitle($level = 2, $separator = " - ") {
740
		$item = $this;
741
		while($item && $level > 0) {
742
			$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...
743
			$item = $item->Parent;
744
			$level--;
745
		}
746
		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...
747
	}
748
749
	/**
750
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
751
	 * the security model for an application.
752
	 *
753
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
754
	 * - Checks for existence of a method named "can<$perm>()" on the object
755
	 * - Calls decorators and only returns for FALSE "vetoes"
756
	 * - Falls back to {@link Permission::check()}
757
	 * - Does NOT check for many-many relations named "Can<$perm>"
758
	 *
759
	 * @uses DataObjectDecorator->can()
760
	 *
761
	 * @param string $perm The permission to be checked, such as 'View'
762
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
763
	 * @return bool True if the the member is allowed to do the given action
764
	 */
765
	public function can($perm, $member = null) {
766 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...
767
			$member = Member::currentUserID();
768
		}
769
770
		if($member && Permission::checkMember($member, "ADMIN")) return true;
771
772
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
773
			$method = 'can' . ucfirst($perm);
774
			return $this->$method($member);
775
		}
776
777
		$results = $this->extend('can', $member);
778
		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...
779
780
		return ($member && Permission::checkMember($member, $perm));
781
	}
782
783
	/**
784
	 * This function should return true if the current user can add children to this page. It can be overloaded to
785
	 * customise the security model for an application.
786
	 *
787
	 * Denies permission if any of the following conditions is true:
788
	 * - alternateCanAddChildren() on a extension returns false
789
	 * - canEdit() is not granted
790
	 * - There are no classes defined in {@link $allowed_children}
791
	 *
792
	 * @uses SiteTreeExtension->canAddChildren()
793
	 * @uses canEdit()
794
	 * @uses $allowed_children
795
	 *
796
	 * @param Member|int $member
797
	 * @return bool True if the current user can add children
798
	 */
799
	public function canAddChildren($member = null) {
800
		// Disable adding children to archived pages
801
		if($this->getIsDeletedFromStage()) {
802
			return false;
803
		}
804
805 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...
806
			$member = Member::currentUserID();
807
		}
808
809
		if($member && Permission::checkMember($member, "ADMIN")) return true;
810
811
		// Standard mechanism for accepting permission changes from extensions
812
		$extended = $this->extendedCan('canAddChildren', $member);
813
		if($extended !== null) return $extended;
814
815
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
816
	}
817
818
	/**
819
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
820
	 * security model for an application.
821
	 *
822
	 * Denies permission if any of the following conditions is true:
823
	 * - canView() on any extension returns false
824
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
825
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
826
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
827
	 *
828
	 * @uses DataExtension->canView()
829
	 * @uses ViewerGroups()
830
	 *
831
	 * @param Member|int $member
832
	 * @return bool True if the current user can view this page
833
	 */
834
	public function canView($member = null) {
835 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...
836
			$member = Member::currentUserID();
837
		}
838
839
		// admin override
840
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
841
842
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
843
		if($this->isOrphaned()) return false;
844
845
		// Standard mechanism for accepting permission changes from extensions
846
		$extended = $this->extendedCan('canView', $member);
847
		if($extended !== null) return $extended;
848
849
		// check for empty spec
850
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
851
852
		// check for inherit
853
		if($this->CanViewType == 'Inherit') {
854
			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...
855
			else return $this->getSiteConfig()->canViewPages($member);
856
		}
857
858
		// check for any logged-in users
859
		if($this->CanViewType == 'LoggedInUsers' && $member) {
860
			return true;
861
		}
862
863
		// check for specific groups
864
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
865
		if(
866
			$this->CanViewType == 'OnlyTheseUsers'
867
			&& $member
868
			&& $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...
869
		) return true;
870
871
		return false;
872
	}
873
874
	/**
875
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
876
	 * security model for an application.
877
	 *
878
	 * Denies permission if any of the following conditions is true:
879
	 * - canDelete() returns false on any extension
880
	 * - canEdit() returns false
881
	 * - any descendant page returns false for canDelete()
882
	 *
883
	 * @uses canDelete()
884
	 * @uses SiteTreeExtension->canDelete()
885
	 * @uses canEdit()
886
	 *
887
	 * @param Member $member
888
	 * @return bool True if the current user can delete this page
889
	 */
890
	public function canDelete($member = null) {
891 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...
892
		else if(is_numeric($member)) $memberID = $member;
893
		else $memberID = Member::currentUserID();
894
895
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
896
			return true;
897
		}
898
899
		// Standard mechanism for accepting permission changes from extensions
900
		$extended = $this->extendedCan('canDelete', $memberID);
901
		if($extended !== null) return $extended;
902
903
		// Regular canEdit logic is handled by can_edit_multiple
904
		$results = self::can_delete_multiple(array($this->ID), $memberID);
905
906
		// If this page no longer exists in stage/live results won't contain the page.
907
		// Fail-over to false
908
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
909
	}
910
911
	/**
912
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
913
	 * can be overloaded to customise the security model for an application.
914
	 *
915
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
916
	 * create beneath a parent is based on the ability to edit that parent page.
917
	 *
918
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
919
	 *
920
	 * @uses $can_create
921
	 * @uses DataExtension->canCreate()
922
	 *
923
	 * @param Member $member
924
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
925
	 *                       If a parent page is known, it will be checked for validity.
926
	 *                       If omitted, it will be assumed this is to be created as a top level page.
927
	 * @return bool True if the current user can create pages on this class.
928
	 */
929
	public function canCreate($member = null, $context = array()) {
930 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...
931
			$member = Member::currentUserID();
932
		}
933
934
		// Check parent (custom canCreate option for SiteTree)
935
		// Block children not allowed for this parent type
936
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
937
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
938
			return false;
939
		}
940
941
		// Check permission
942
		if($member && Permission::checkMember($member, "ADMIN")) {
943
			return true;
944
		}
945
946
		// Standard mechanism for accepting permission changes from extensions
947
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
948
		if($extended !== null) {
949
			return $extended;
950
		}
951
952
		// Fall over to inherited permissions
953
		if($parent) {
954
			return $parent->canAddChildren($member);
955
		} else {
956
			// This doesn't necessarily mean we are creating a root page, but that
957
			// we don't know if there is a parent, so default to this permission
958
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
959
		}
960
	}
961
962
	/**
963
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
964
	 * security model for an application.
965
	 *
966
	 * Denies permission if any of the following conditions is true:
967
	 * - canEdit() on any extension returns false
968
	 * - canView() return false
969
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
970
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
971
	 *   CMS_Access_CMSMAIN permission code
972
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
973
	 *
974
	 * @uses canView()
975
	 * @uses EditorGroups()
976
	 * @uses DataExtension->canEdit()
977
	 *
978
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
979
	 *                       unit tests)
980
	 * @return bool True if the current user can edit this page
981
	 */
982
	public function canEdit($member = null) {
983 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...
984
		else if(is_numeric($member)) $memberID = $member;
985
		else $memberID = Member::currentUserID();
986
987
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
988
989
		// Standard mechanism for accepting permission changes from extensions
990
		$extended = $this->extendedCan('canEdit', $memberID);
991
		if($extended !== null) return $extended;
992
993
		if($this->ID) {
994
			// Regular canEdit logic is handled by can_edit_multiple
995
			$results = self::can_edit_multiple(array($this->ID), $memberID);
996
997
			// If this page no longer exists in stage/live results won't contain the page.
998
			// Fail-over to false
999
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1000
1001
		// Default for unsaved pages
1002
		} else {
1003
			return $this->getSiteConfig()->canEditPages($member);
1004
		}
1005
	}
1006
1007
	/**
1008
	 * Stub method to get the site config, unless the current class can provide an alternate.
1009
	 *
1010
	 * @return SiteConfig
1011
	 */
1012
	public function getSiteConfig() {
1013
1014
		if($this->hasMethod('alternateSiteConfig')) {
1015
			$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...
1016
			if($altConfig) return $altConfig;
1017
		}
1018
1019
		return SiteConfig::current_site_config();
1020
	}
1021
1022
	/**
1023
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1024
	 * can_(perm)_multiple method for efficiency.
1025
	 *
1026
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1027
	 * @param array           $ids           An array of page IDs
1028
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1029
	 *                                       to 'SiteTree::can_(permission)_multiple'
1030
	 */
1031
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1032
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1033
1034
		if(is_callable($batchCallback)) {
1035
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1036
		} else {
1037
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1038
				. "with callback '$batchCallback'", E_USER_WARNING);
1039
		}
1040
	}
1041
1042
	/**
1043
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1044
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1045
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1046
	 * efficiently.
1047
	 *
1048
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1049
	 * property to FALSE.
1050
	 *
1051
	 * @param array  $ids              Of {@link SiteTree} IDs
1052
	 * @param int    $memberID         Member ID
1053
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1054
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1055
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1056
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1057
	 * @param bool   $useCached
1058
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1059
	 */
1060
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1061
												  $globalPermission = null, $useCached = true) {
1062
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1063
1064
		// Sanitise the IDs
1065
		$ids = array_filter($ids, 'is_numeric');
1066
1067
		// This is the name used on the permission cache
1068
		// converts something like 'CanEditType' to 'edit'.
1069
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1070
1071
		// Default result: nothing editable
1072
		$result = array_fill_keys($ids, false);
1073
		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...
1074
1075
			// Look in the cache for values
1076
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1077
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1078
1079
				// If we can't find everything in the cache, then look up the remainder separately
1080
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1081
				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...
1082
					$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 1062 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...
1083
				}
1084
				return $cachedValues;
1085
			}
1086
1087
			// If a member doesn't have a certain permission then they can't edit anything
1088
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1089
				return $result;
1090
			}
1091
1092
			// Placeholder for parameterised ID list
1093
			$idPlaceholders = DB::placeholders($ids);
1094
1095
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1096
			// be enabled
1097
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1098
1099
			// Get the groups that the given member belongs to
1100
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1101
			$SQL_groupList = implode(", ", $groupIDs);
1102
			if (!$SQL_groupList) $SQL_groupList = '0';
1103
1104
			$combinedStageResult = array();
1105
1106
			foreach(array('Stage', 'Live') as $stage) {
1107
				// Start by filling the array with the pages that actually exist
1108
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1109
1110
				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...
1111
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1112
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1113
				} else {
1114
					$stageIds = array();
1115
				}
1116
				$result = array_fill_keys($stageIds, false);
1117
1118
				// Get the uninherited permissions
1119
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1120
					->where(array(
1121
						"(\"$typeField\" = 'LoggedInUsers' OR
1122
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1123
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1124
						=> $ids
1125
					))
1126
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1127
1128
				if($uninheritedPermissions) {
1129
					// Set all the relevant items in $result to true
1130
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1131
				}
1132
1133
				// Get permissions that are inherited
1134
				$potentiallyInherited = Versioned::get_by_stage(
1135
					"SiteTree",
1136
					$stage,
1137
					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...
1138
				);
1139
1140
				if($potentiallyInherited) {
1141
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1142
					// then see which ones the user has permission on
1143
					$groupedByParent = array();
1144
					foreach($potentiallyInherited as $item) {
1145
						if($item->ParentID) {
1146
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1147
							$groupedByParent[$item->ParentID][] = $item->ID;
1148
						} else {
1149
							// Might return different site config based on record context, e.g. when subsites module
1150
							// is used
1151
							$siteConfig = $item->getSiteConfig();
1152
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1153
						}
1154
					}
1155
1156
					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...
1157
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1158
						if($actuallyInherited) {
1159
							$parentIDs = array_keys(array_filter($actuallyInherited));
1160
							foreach($parentIDs as $parentID) {
1161
								// Set all the relevant items in $result to true
1162
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1163
							}
1164
						}
1165
					}
1166
				}
1167
1168
				$combinedStageResult = $combinedStageResult + $result;
1169
1170
			}
1171
		}
1172
1173
		if(isset($combinedStageResult)) {
1174
			// Cache the results
1175
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1176
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1177
1178
			return $combinedStageResult;
1179
		} else {
1180
			return array();
1181
		}
1182
	}
1183
1184
	/**
1185
	 * Get the 'can edit' information for a number of SiteTree pages.
1186
	 *
1187
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1188
	 * @param int   $memberID  ID of member
1189
	 * @param bool  $useCached Return values from the permission cache if they exist
1190
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1191
	 *                         edited
1192
	 */
1193
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1194
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1195
	}
1196
1197
	/**
1198
	 * Get the 'can edit' information for a number of SiteTree pages.
1199
	 *
1200
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1201
	 * @param int   $memberID  ID of member
1202
	 * @param bool  $useCached Return values from the permission cache if they exist
1203
	 * @return array
1204
	 */
1205
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1206
		$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...
1207
		$result = array_fill_keys($ids, false);
1208
		$cacheKey = "delete-$memberID";
1209
1210
		// Look in the cache for values
1211
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1212
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1213
1214
			// If we can't find everything in the cache, then look up the remainder separately
1215
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1216
			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...
1217
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1218
					+ $cachedValues;
1219
			}
1220
			return $cachedValues;
1221
		}
1222
1223
		// You can only delete pages that you can edit
1224
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1225
		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...
1226
1227
			// You can only delete pages whose children you can delete
1228
			$editablePlaceholders = DB::placeholders($editableIDs);
1229
			$childRecords = SiteTree::get()->where(array(
1230
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1231
			));
1232
			if($childRecords) {
1233
				$children = $childRecords->map("ID", "ParentID");
1234
1235
				// Find out the children that can be deleted
1236
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1237
1238
				// Get a list of all the parents that have no undeletable children
1239
				$deletableParents = array_fill_keys($editableIDs, true);
1240
				foreach($deletableChildren as $id => $canDelete) {
1241
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1242
				}
1243
1244
				// Use that to filter the list of deletable parents that have children
1245
				$deletableParents = array_keys($deletableParents);
1246
1247
				// Also get the $ids that don't have children
1248
				$parents = array_unique($children->values());
1249
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1250
1251
				// Combine the two
1252
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1253
1254
			} else {
1255
				$deletable = $editableIDs;
1256
			}
1257
		} else {
1258
			$deletable = array();
1259
		}
1260
1261
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1262
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1263
	}
1264
1265
	/**
1266
	 * Collate selected descendants of this page.
1267
	 *
1268
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1269
	 * $collator array.
1270
	 *
1271
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1272
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1273
	 * @return bool
1274
	 */
1275
	public function collateDescendants($condition, &$collator) {
1276
		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...
1277
			foreach($children as $item) {
1278
				if(eval("return $condition;")) $collator[] = $item;
1279
				$item->collateDescendants($condition, $collator);
1280
			}
1281
			return true;
1282
		}
1283
	}
1284
1285
	/**
1286
	 * Return the title, description, keywords and language metatags.
1287
	 *
1288
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1289
	 *
1290
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1291
	 * @return string The XHTML metatags
1292
	 */
1293
	public function MetaTags($includeTitle = true) {
1294
		$tags = "";
1295
		if($includeTitle === true || $includeTitle == 'true') {
1296
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1297
		}
1298
1299
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1300
		if (!empty($generator)) {
1301
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1302
		}
1303
1304
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1305
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1306
		if($this->MetaDescription) {
1307
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1308
		}
1309
		if($this->ExtraMeta) {
1310
			$tags .= $this->ExtraMeta . "\n";
1311
		}
1312
1313
		if(Permission::check('CMS_ACCESS_CMSMain')
1314
			&& in_array('CMSPreviewable', class_implements($this))
1315
			&& !$this instanceof ErrorPage
1316
			&& $this->ID > 0
1317
		) {
1318
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1319
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1320
		}
1321
1322
		$this->extend('MetaTags', $tags);
1323
1324
		return $tags;
1325
	}
1326
1327
	/**
1328
	 * Returns the object that contains the content that a user would associate with this page.
1329
	 *
1330
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1331
	 * return the page that is linked to.
1332
	 *
1333
	 * @return $this
1334
	 */
1335
	public function ContentSource() {
1336
		return $this;
1337
	}
1338
1339
	/**
1340
	 * Add default records to database.
1341
	 *
1342
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1343
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1344
	 */
1345
	public function requireDefaultRecords() {
1346
		parent::requireDefaultRecords();
1347
1348
		// default pages
1349
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1350
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1351
				$homepage = new Page();
1352
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1353
				$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>');
1354
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1355
				$homepage->Sort = 1;
1356
				$homepage->write();
1357
				$homepage->publish('Stage', 'Live');
1358
				$homepage->flushCache();
1359
				DB::alteration_message('Home page created', 'created');
1360
			}
1361
1362
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1363
				$aboutus = new Page();
1364
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1365
				$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>');
1366
				$aboutus->Sort = 2;
1367
				$aboutus->write();
1368
				$aboutus->publish('Stage', 'Live');
1369
				$aboutus->flushCache();
1370
				DB::alteration_message('About Us page created', 'created');
1371
1372
				$contactus = new Page();
1373
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1374
				$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>');
1375
				$contactus->Sort = 3;
1376
				$contactus->write();
1377
				$contactus->publish('Stage', 'Live');
1378
				$contactus->flushCache();
1379
				DB::alteration_message('Contact Us page created', 'created');
1380
			}
1381
		}
1382
1383
		// schema migration
1384
		// @todo Move to migration task once infrastructure is implemented
1385
		if($this->class == 'SiteTree') {
1386
			$conn = DB::get_schema();
1387
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1388
			if($conn->hasField('SiteTree' ,'Viewers')) {
1389
				$task = new UpgradeSiteTreePermissionSchemaTask();
1390
				$task->run(new SS_HTTPRequest('GET','/'));
1391
			}
1392
		}
1393
	}
1394
1395
	protected function onBeforeWrite() {
1396
		parent::onBeforeWrite();
1397
1398
		// If Sort hasn't been set, make this page come after it's siblings
1399
		if(!$this->Sort) {
1400
			$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...
1401
			$this->Sort = DB::prepared_query(
1402
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1403
				array($parentID)
1404
			)->value();
1405
		}
1406
1407
		// If there is no URLSegment set, generate one from Title
1408
		$defaultSegment = $this->generateURLSegment(_t(
1409
			'CMSMain.NEWPAGE',
1410
			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...
1411
		));
1412
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1413
			$this->URLSegment = $this->generateURLSegment($this->Title);
1414
		} else if($this->isChanged('URLSegment', 2)) {
1415
			// Do a strict check on change level, to avoid double encoding caused by
1416
			// bogus changes through forceChange()
1417
			$filter = URLSegmentFilter::create();
1418
			$this->URLSegment = $filter->filter($this->URLSegment);
1419
			// If after sanitising there is no URLSegment, give it a reasonable default
1420
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1421
		}
1422
1423
		// Ensure that this object has a non-conflicting URLSegment value.
1424
		$count = 2;
1425
		while(!$this->validURLSegment()) {
1426
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1427
			$count++;
1428
		}
1429
1430
		$this->syncLinkTracking();
1431
1432
		// Check to see if we've only altered fields that shouldn't affect versioning
1433
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1434
		$changedFields = array_keys($this->getChangedFields(true, 2));
1435
1436
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1437
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1438
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1439
1440
		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...
1441
			// This will have the affect of preserving the versioning
1442
			$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...
1443
		}
1444
	}
1445
1446
	/**
1447
	 * Trigger synchronisation of link tracking
1448
	 *
1449
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1450
	 */
1451
	public function syncLinkTracking() {
1452
		$this->extend('augmentSyncLinkTracking');
1453
	}
1454
1455
	public function onBeforeDelete() {
1456
		parent::onBeforeDelete();
1457
1458
		// If deleting this page, delete all its children.
1459
		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...
1460
			foreach($children as $child) {
1461
				$child->delete();
1462
			}
1463
		}
1464
	}
1465
1466
	public function onAfterDelete() {
1467
		// Need to flush cache to avoid outdated versionnumber references
1468
		$this->flushCache();
1469
1470
		// Need to mark pages depending to this one as broken
1471
		$dependentPages = $this->DependentPages();
1472
		if($dependentPages) foreach($dependentPages as $page) {
1473
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1474
			$page->write();
1475
		}
1476
1477
		parent::onAfterDelete();
1478
	}
1479
1480
	public function flushCache($persistent = true) {
1481
		parent::flushCache($persistent);
1482
		$this->_cache_statusFlags = null;
1483
	}
1484
1485
	public function validate() {
1486
		$result = parent::validate();
1487
1488
		// Allowed children validation
1489
		$parent = $this->getParent();
1490
		if($parent && $parent->exists()) {
1491
			// No need to check for subclasses or instanceof, as allowedChildren() already
1492
			// deconstructs any inheritance trees already.
1493
			$allowed = $parent->allowedChildren();
1494
			$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...
1495
			if(!in_array($subject->ClassName, $allowed)) {
1496
1497
				$result->error(
1498
					_t(
1499
						'SiteTree.PageTypeNotAllowed',
1500
						'Page type "{type}" not allowed as child of this parent page',
1501
						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...
1502
					),
1503
					'ALLOWED_CHILDREN'
1504
				);
1505
			}
1506
		}
1507
1508
		// "Can be root" validation
1509
		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...
1510
			$result->error(
1511
				_t(
1512
					'SiteTree.PageTypNotAllowedOnRoot',
1513
					'Page type "{type}" is not allowed on the root level',
1514
					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...
1515
				),
1516
				'CAN_BE_ROOT'
1517
			);
1518
		}
1519
1520
		return $result;
1521
	}
1522
1523
	/**
1524
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1525
	 * checks for:
1526
	 *  - A page with the same URLSegment that has a conflict
1527
	 *  - Conflicts with actions on the parent page
1528
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1529
	 *
1530
	 * @return bool
1531
	 */
1532
	public function validURLSegment() {
1533
		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...
1534
			if($controller = ModelAsController::controller_for($parent)) {
1535
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1536
			}
1537
		}
1538
1539
		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...
1540
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1541
		}
1542
1543
		// Filters by url, id, and parent
1544
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1545
		if($this->ID) {
1546
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1547
		}
1548
		if(self::config()->nested_urls) {
1549
			$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...
1550
		}
1551
1552
		$votes = array_filter(
1553
			(array)$this->extend('augmentValidURLSegment'),
1554
			function($v) {return !is_null($v);}
1555
		);
1556
		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...
1557
			return min($votes);
1558
		}
1559
1560
		// Check existence
1561
		$existingPage = DataObject::get_one('SiteTree', $filter);
1562
		if ($existingPage) return false;
1563
1564
		return !($existingPage);
1565
		}
1566
1567
	/**
1568
	 * Generate a URL segment based on the title provided.
1569
	 *
1570
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1571
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1572
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1573
	 * scratch, or incrementally modify the generated URL.
1574
	 *
1575
	 * @param string $title Page title
1576
	 * @return string Generated url segment
1577
	 */
1578
	public function generateURLSegment($title){
1579
		$filter = URLSegmentFilter::create();
1580
		$t = $filter->filter($title);
1581
1582
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1583
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1584
1585
		// Hook for extensions
1586
		$this->extend('updateURLSegment', $t, $title);
1587
1588
		return $t;
1589
	}
1590
1591
	/**
1592
	 * Gets the URL segment for the latest draft version of this page.
1593
	 *
1594
	 * @return string
1595
	 */
1596
	public function getStageURLSegment() {
1597
		$stageRecord = Versioned::get_one_by_stage('SiteTree', Versioned::DRAFT, 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...
1598
			'"SiteTree"."ID"' => $this->ID
1599
		));
1600
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1601
	}
1602
1603
	/**
1604
	 * Gets the URL segment for the currently published version of this page.
1605
	 *
1606
	 * @return string
1607
	 */
1608
	public function getLiveURLSegment() {
1609
		$liveRecord = Versioned::get_one_by_stage('SiteTree', Versioned::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...
1610
			'"SiteTree"."ID"' => $this->ID
1611
		));
1612
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1613
	}
1614
1615
	/**
1616
	 * Rewrites any linked images on this page without creating a new version record.
1617
	 * Non-image files should be linked via shortcodes
1618
	 * Triggers the onRenameLinkedAsset action on extensions.
1619
	 *
1620
	 * @todo Implement image shortcodes and remove this feature
1621
	 */
1622
	public function rewriteFileLinks() {
1623
		// Skip live stage
1624
		if(\Versioned::get_stage() === \Versioned::LIVE) {
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

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

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

Loading history...
1625
			return;
1626
		}
1627
1628
		// Update the content without actually creating a new version
1629
		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...
1630
			// Skip if non HTML or if empty
1631
			if ($fieldType !== 'HTMLText') {
1632
				continue;
1633
			}
1634
			$fieldValue = $this->{$fieldName};
1635
			if(empty($fieldValue)) {
1636
				continue;
1637
			}
1638
1639
			// Regenerate content
1640
			$content = Image::regenerate_html_links($fieldValue);
1641
			if($content === $fieldValue) {
1642
				continue;
1643
			}
1644
1645
			// Write content directly without updating linked assets
1646
			$table = ClassInfo::table_for_object_field($this, $fieldName);
1647
			$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
1648
			DB::prepared_query($query, array($content, $this->ID));
1649
1650
			// Update linked assets
1651
			$this->invokeWithExtensions('onRenameLinkedAsset');
1652
		}
1653
	}
1654
1655
	/**
1656
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1657
	 *
1658
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1659
	 * @return ArrayList
1660
	 */
1661
	public function DependentPages($includeVirtuals = true) {
1662
		if(class_exists('Subsite')) {
1663
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1664
			Subsite::disable_subsite_filter(true);
1665
		}
1666
1667
		// Content links
1668
		$items = new ArrayList();
1669
1670
		// We merge all into a regular SS_List, because DataList doesn't support merge
1671
		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...
1672
			$linkList = new ArrayList();
1673
			foreach($contentLinks as $item) {
1674
				$item->DependentLinkType = 'Content link';
1675
				$linkList->push($item);
1676
			}
1677
			$items->merge($linkList);
1678
		}
1679
1680
		// Virtual pages
1681
		if($includeVirtuals) {
1682
			$virtuals = $this->VirtualPages();
1683
			if($virtuals) {
1684
				$virtualList = new ArrayList();
1685
				foreach($virtuals as $item) {
1686
					$item->DependentLinkType = 'Virtual page';
1687
					$virtualList->push($item);
1688
				}
1689
				$items->merge($virtualList);
1690
			}
1691
		}
1692
1693
		// Redirector pages
1694
		$redirectors = RedirectorPage::get()->where(array(
1695
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1696
			'"RedirectorPage"."LinkToID"' => $this->ID
1697
		));
1698
		if($redirectors) {
1699
			$redirectorList = new ArrayList();
1700
			foreach($redirectors as $item) {
1701
				$item->DependentLinkType = 'Redirector page';
1702
				$redirectorList->push($item);
1703
			}
1704
			$items->merge($redirectorList);
1705
		}
1706
1707
		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...
1708
1709
		return $items;
1710
	}
1711
1712
	/**
1713
	 * Return all virtual pages that link to this page.
1714
	 *
1715
	 * @return DataList
1716
	 */
1717
	public function VirtualPages() {
1718
		$pages = parent::VirtualPages();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DataObject as the method VirtualPages() does only exist in the following sub-classes of DataObject: SiteTree. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1719
1720
		// Disable subsite filter for these pages
1721
		if($pages instanceof DataList) {
1722
			return $pages->setDataQueryParam('Subsite.filter', false);
1723
		} else {
1724
			return $pages;
1725
		}
1726
	}
1727
1728
	/**
1729
	 * Returns a FieldList with which to create the main editing form.
1730
	 *
1731
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1732
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1733
	 *
1734
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1735
	 * e.g. access control.
1736
	 *
1737
	 * @return FieldList The fields to be displayed in the CMS
1738
	 */
1739
	public function getCMSFields() {
1740
		require_once("forms/Form.php");
1741
		// Status / message
1742
		// Create a status message for multiple parents
1743
		if($this->ID && is_numeric($this->ID)) {
1744
			$linkedPages = $this->VirtualPages();
1745
1746
			$parentPageLinks = array();
1747
1748
			if($linkedPages->Count() > 0) {
1749
				foreach($linkedPages as $linkedPage) {
1750
					$parentPage = $linkedPage->Parent;
1751
					if($parentPage) {
1752
						if($parentPage->ID) {
1753
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1754
						} else {
1755
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1756
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1757
								"</a>";
1758
						}
1759
					}
1760
				}
1761
1762
				$lastParent = array_pop($parentPageLinks);
1763
				$parentList = "'$lastParent'";
1764
1765
				if(count($parentPageLinks) > 0) {
1766
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1767
						. $parentList;
1768
				}
1769
1770
				$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...
1771
					'SiteTree.APPEARSVIRTUALPAGES',
1772
					"This content also appears on the virtual pages in the {title} sections.",
1773
					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...
1774
				);
1775
			}
1776
		}
1777
1778
		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...
1779
			$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...
1780
		}
1781
1782
		$dependentNote = '';
1783
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1784
1785
		// Create a table for showing pages linked to this one
1786
		$dependentPages = $this->DependentPages();
1787
		$dependentPagesCount = $dependentPages->Count();
1788
		if($dependentPagesCount) {
1789
			$dependentColumns = array(
1790
				'Title' => $this->fieldLabel('Title'),
1791
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1792
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1793
			);
1794
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1795
1796
			$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>');
1797
			$dependentTable = GridField::create(
1798
				'DependentPages',
1799
				false,
1800
				$dependentPages
1801
			);
1802
			$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...
1803
				->setDisplayFields($dependentColumns)
1804
				->setFieldFormatting(array(
1805
					'Title' => function($value, &$item) {
1806
						return sprintf(
1807
							'<a href="admin/pages/edit/show/%d">%s</a>',
1808
							(int)$item->ID,
1809
							Convert::raw2xml($item->Title)
1810
						);
1811
					},
1812
					'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...
1813
						return sprintf(
1814
							'<a href="%s" target="_blank">%s</a>',
1815
							Convert::raw2xml($value),
1816
							Convert::raw2xml($value)
1817
						);
1818
					}
1819
				));
1820
		}
1821
1822
		$baseLink = Controller::join_links (
1823
			Director::absoluteBaseURL(),
1824
			(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...
1825
		);
1826
1827
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1828
			->setURLPrefix($baseLink)
1829
			->setDefaultURL($this->generateURLSegment(_t(
1830
				'CMSMain.NEWPAGE',
1831
				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...
1832
			)));
1833
		$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...
1834
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1835
			$helpText .= $helpText ? '<br />' : '';
1836
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1837
		}
1838
		$urlsegment->setHelpText($helpText);
1839
1840
		$fields = new FieldList(
1841
			$rootTab = new TabSet("Root",
1842
				$tabMain = new Tab('Main',
1843
					new TextField("Title", $this->fieldLabel('Title')),
1844
					$urlsegment,
1845
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1846
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1847
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1848
						array(
1849
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1850
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1851
						)
1852
					)->setHeadingLevel(4)
1853
				),
1854
				$tabDependent = new Tab('Dependent',
1855
					$dependentNote,
1856
					$dependentTable
1857
				)
1858
			)
1859
		);
1860
		$htmlField->addExtraClass('stacked');
1861
1862
		// Help text for MetaData on page content editor
1863
		$metaFieldDesc
1864
			->setRightTitle(
1865
				_t(
1866
					'SiteTree.METADESCHELP',
1867
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1868
				)
1869
			)
1870
			->addExtraClass('help');
1871
		$metaFieldExtra
1872
			->setRightTitle(
1873
				_t(
1874
					'SiteTree.METAEXTRAHELP',
1875
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1876
				)
1877
			)
1878
			->addExtraClass('help');
1879
1880
		// Conditional dependent pages tab
1881
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1882
		else $fields->removeFieldFromTab('Root', 'Dependent');
1883
1884
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1885
1886
		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...
1887
			$obsoleteWarning = _t(
1888
				'SiteTree.OBSOLETECLASS',
1889
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1890
				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...
1891
			);
1892
1893
			$fields->addFieldToTab(
1894
				"Root.Main",
1895
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1896
				"Title"
1897
			);
1898
		}
1899
1900
		if(file_exists(BASE_PATH . '/install.php')) {
1901
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
1902
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
1903
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
1904
				. "</p>"), "Title");
1905
		}
1906
1907
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
1908
		$fields->setTabPathRewrites(array(
1909
			'/^Root\.Content\.Main$/' => 'Root.Main',
1910
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
1911
		));
1912
1913
		if(self::$runCMSFieldsExtensions) {
1914
			$this->extend('updateCMSFields', $fields);
1915
		}
1916
1917
		return $fields;
1918
	}
1919
1920
1921
	/**
1922
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1923
	 * for content-related fields.
1924
	 *
1925
	 * @return FieldList
1926
	 */
1927
	public function getSettingsFields() {
1928
		$groupsMap = array();
1929
		foreach(Group::get() as $group) {
1930
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
1931
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1932
		}
1933
		asort($groupsMap);
1934
1935
		$fields = new FieldList(
1936
			$rootTab = new TabSet("Root",
1937
				$tabBehaviour = new Tab('Settings',
1938
					new DropdownField(
1939
						"ClassName",
1940
						$this->fieldLabel('ClassName'),
1941
						$this->getClassDropdown()
1942
					),
1943
					$parentTypeSelector = new CompositeField(
1944
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
1945
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1946
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1947
						)),
1948
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
1949
					),
1950
					$visibility = new FieldGroup(
1951
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1952
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1953
					),
1954
					$viewersOptionsField = new OptionsetField(
1955
						"CanViewType",
1956
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
1957
					),
1958
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
1959
						->setSource($groupsMap)
1960
						->setAttribute(
1961
							'data-placeholder',
1962
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
1963
						),
1964
					$editorsOptionsField = new OptionsetField(
1965
						"CanEditType",
1966
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
1967
					),
1968
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
1969
						->setSource($groupsMap)
1970
						->setAttribute(
1971
							'data-placeholder',
1972
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
1973
						)
1974
				)
1975
			)
1976
		);
1977
1978
		$visibility->setTitle($this->fieldLabel('Visibility'));
1979
1980
1981
		// This filter ensures that the ParentID dropdown selection does not show this node,
1982
		// or its descendents, as this causes vanishing bugs
1983
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
1984
		$parentTypeSelector->addExtraClass('parentTypeSelector');
1985
1986
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
1987
1988
		// Make page location fields read-only if the user doesn't have the appropriate permission
1989
		if(!Permission::check("SITETREE_REORGANISE")) {
1990
			$fields->makeFieldReadonly('ParentType');
1991
			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...
1992
				$fields->removeByName('ParentID');
1993
			} else {
1994
				$fields->makeFieldReadonly('ParentID');
1995
			}
1996
		}
1997
1998
		$viewersOptionsSource = array();
1999
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2000
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2001
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2002
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2003
		$viewersOptionsField->setSource($viewersOptionsSource);
2004
2005
		$editorsOptionsSource = array();
2006
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2007
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2008
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2009
		$editorsOptionsField->setSource($editorsOptionsSource);
2010
2011
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2012
			$fields->makeFieldReadonly($viewersOptionsField);
2013
			if($this->CanViewType == 'OnlyTheseUsers') {
2014
				$fields->makeFieldReadonly($viewerGroupsField);
2015
			} else {
2016
				$fields->removeByName('ViewerGroups');
2017
			}
2018
2019
			$fields->makeFieldReadonly($editorsOptionsField);
2020
			if($this->CanEditType == 'OnlyTheseUsers') {
2021
				$fields->makeFieldReadonly($editorGroupsField);
2022
			} else {
2023
				$fields->removeByName('EditorGroups');
2024
			}
2025
		}
2026
2027
		if(self::$runCMSFieldsExtensions) {
2028
			$this->extend('updateSettingsFields', $fields);
2029
		}
2030
2031
		return $fields;
2032
	}
2033
2034
	/**
2035
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2036
	 * @return array
2037
	 */
2038
	public function fieldLabels($includerelations = true) {
2039
		$cacheKey = $this->class . '_' . $includerelations;
2040
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2041
			$labels = parent::fieldLabels($includerelations);
2042
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2043
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2044
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2045
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2046
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2047
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2048
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2049
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2050
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2051
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2052
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2053
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2054
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2055
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2056
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2057
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2058
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2059
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2060
			$labels['LinkChangeNote'] = _t (
2061
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2062
			);
2063
2064
			if($includerelations){
2065
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2066
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2067
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2068
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2069
			}
2070
2071
			self::$_cache_field_labels[$cacheKey] = $labels;
2072
		}
2073
2074
		return self::$_cache_field_labels[$cacheKey];
2075
	}
2076
2077
	/**
2078
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2079
	 *
2080
	 * Frontend scripts and styles know how to handle the following FormFields:
2081
	 * - top-level FormActions appear as standalone buttons
2082
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2083
	 * - TabSet & Tabs appear as a drop ups
2084
	 * - FormActions within the Tab are restyled as links
2085
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2086
	 *
2087
	 * @return FieldList The available actions for this page.
2088
	 */
2089
	public function getCMSActions() {
2090
		$existsOnLive = $this->getExistsOnLive();
2091
2092
		// Major actions appear as buttons immediately visible as page actions.
2093
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset noborder');
2094
2095
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2096
		$rootTabSet = new TabSet('ActionMenus');
2097
		$moreOptions = new Tab(
2098
			'MoreOptions',
2099
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2100
		);
2101
		$rootTabSet->push($moreOptions);
2102
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2103
2104
		// Render page information into the "more-options" drop-up, on the top.
2105
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::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...
2106
			'"SiteTree"."ID"' => $this->ID
2107
		));
2108
		$moreOptions->push(
2109
			new LiteralField('Information',
2110
				$this->customise(array(
2111
					'Live' => $live,
2112
					'ExistsOnLive' => $existsOnLive
2113
				))->renderWith('SiteTree_Information')
2114
			)
2115
		);
2116
2117
		// "readonly"/viewing version that isn't the current version of the record
2118
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), array(
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2119
			'"SiteTree"."ID"' => $this->ID
2120
		));
2121
		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...
2122
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2123
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2124
2125
			$actions = new FieldList(array($majorActions, $rootTabSet));
2126
2127
			// getCMSActions() can be extended with updateCMSActions() on a extension
2128
			$this->extend('updateCMSActions', $actions);
2129
2130
			return $actions;
2131
		}
2132
2133 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...
2134
			// "unpublish"
2135
			$moreOptions->push(
2136
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2137
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2138
					->addExtraClass('ss-ui-action-destructive')
2139
			);
2140
		}
2141
2142 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...
2143
			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...
2144
				// "rollback"
2145
				$moreOptions->push(
2146
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2147
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2148
				);
2149
			}
2150
		}
2151
2152
		if($this->canEdit()) {
2153
			if($this->getIsDeletedFromStage()) {
2154
				// The usual major actions are not available, so we provide alternatives here.
2155
				if($existsOnLive) {
2156
					// "restore"
2157
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2158
					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...
2159
						// "delete from live"
2160
						$majorActions->push(
2161
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2162
								->addExtraClass('ss-ui-action-destructive')
2163
						);
2164
					}
2165
				} else {
2166
					// Determine if we should force a restore to root (where once it was a subpage)
2167
					$restoreToRoot = $this->isParentArchived();
2168
2169
					// "restore"
2170
					$title = $restoreToRoot
2171
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2172
						: _t('CMSMain.RESTORE','Restore draft');
2173
					$description = $restoreToRoot
2174
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2175
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2176
					$majorActions->push(
2177
						FormAction::create('restore', $title)
2178
							->setDescription($description)
2179
							->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...
2180
							->setAttribute('data-icon', 'decline')
2181
					);
2182
				}
2183
			} else {
2184
				if($this->canDelete()) {
2185
					// delete
2186
					$moreOptions->push(
2187
						FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2188
							->addExtraClass('delete ss-ui-action-destructive')
2189
					);
2190
				}
2191
				if($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...
2192
					// "archive"
2193
					$moreOptions->push(
2194
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2195
							->setDescription(_t(
2196
								'SiteTree.BUTTONARCHIVEDESC',
2197
								'Unpublish and send to archive'
2198
							))
2199
							->addExtraClass('delete ss-ui-action-destructive')
2200
					);
2201
				}
2202
2203
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2204
				$majorActions->push(
2205
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2206
						->setAttribute('data-icon', 'accept')
2207
						->setAttribute('data-icon-alternate', 'addpage')
2208
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2209
				);
2210
			}
2211
		}
2212
2213
		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...
2214
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2215
			$majorActions->push(
2216
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2217
					->setAttribute('data-icon', 'accept')
2218
					->setAttribute('data-icon-alternate', 'disk')
2219
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2220
			);
2221
2222
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2223
			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...
2224
				$publish->addExtraClass('ss-ui-alternate');
2225
			}
2226
		}
2227
2228
		$actions = new FieldList(array($majorActions, $rootTabSet));
2229
2230
		// Hook for extensions to add/remove actions.
2231
		$this->extend('updateCMSActions', $actions);
2232
2233
		return $actions;
2234
	}
2235
2236
	public function onAfterPublish() {
2237
		// Force live sort order to match stage sort order
2238
		DB::prepared_query('UPDATE "SiteTree_Live"
2239
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2240
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2241
			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...
2242
		);
2243
	}
2244
2245
	/**
2246
	 * Update draft dependant pages
2247
	 */
2248
	public function onAfterRevertToLive() {
2249
		// Use an alias to get the updates made by $this->publish
2250
		/** @var SiteTree $stageSelf */
2251
		$stageSelf = Versioned::get_by_stage('SiteTree', Versioned::DRAFT)->byID($this->ID);
2252
		$stageSelf->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion 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
2254
		// Need to update pages linking to this one as no longer broken
2255
		foreach($stageSelf->DependentPages() as $page) {
2256
			/** @var SiteTree $page */
2257
			$page->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion 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...
2258
		}
2259
	}
2260
2261
	/**
2262
	 * Determine if this page references a parent which is archived, and not available in stage
2263
	 *
2264
	 * @return bool True if there is an archived parent
2265
	 */
2266
	protected function isParentArchived() {
2267
		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...
2268
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2269
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2270
				return true;
2271
			}
2272
		}
2273
		return false;
2274
	}
2275
2276
	/**
2277
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2278
	 *
2279
	 * @return self
2280
	 */
2281
	public function doRestoreToStage() {
2282
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2283
2284
		// Ensure that the parent page is restored, otherwise restore to root
2285
		if($this->isParentArchived()) {
2286
			$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...
2287
		}
2288
2289
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2290
		// create an empty record
2291
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2292
			$conn = DB::get_conn();
2293
			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...
2294
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2295
			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...
2296
		}
2297
2298
		$oldStage = Versioned::get_stage();
0 ignored issues
show
Bug introduced by
The method get_stage() does not seem to exist on object<Versioned>.

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

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

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

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

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

Loading history...
2300
		$this->forceChange();
2301
		$this->write();
2302
2303
		$result = DataObject::get_by_id($this->class, $this->ID);
2304
2305
		// Need to update pages linking to this one as no longer broken
2306
		foreach($result->DependentPages(false) as $page) {
2307
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2308
			$page->write();
2309
		}
2310
2311
		Versioned::set_stage($oldStage);
0 ignored issues
show
Bug introduced by
The method set_stage() does not seem to exist on object<Versioned>.

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

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

Loading history...
2312
2313
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2314
2315
		return $result;
2316
	}
2317
2318
	/**
2319
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2320
	 *
2321
	 * @return bool
2322
	 */
2323
	public function isNew() {
2324
		/**
2325
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2326
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2327
		 */
2328
		if(empty($this->ID)) return true;
2329
2330
		if(is_numeric($this->ID)) return false;
2331
2332
		return stripos($this->ID, 'new') === 0;
2333
	}
2334
2335
	/**
2336
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2337
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2338
	 * {@link SiteTree::$needs_permission}.
2339
	 *
2340
	 * @return array
2341
	 */
2342
	protected function getClassDropdown() {
2343
		$classes = self::page_type_classes();
2344
		$currentClass = null;
2345
		$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...
2346
2347
		$result = array();
2348
		foreach($classes as $class) {
2349
			$instance = singleton($class);
2350
2351
			// if the current page type is this the same as the class type always show the page type in the list
2352
			if ($this->ClassName != $instance->ClassName) {
2353
				if($instance instanceof HiddenClass) continue;
2354
				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...
2355
			}
2356
2357
			if($perms = $instance->stat('need_permission')) {
2358
				if(!$this->can($perms)) continue;
2359
			}
2360
2361
			$pageTypeName = $instance->i18n_singular_name();
2362
2363
			$currentClass = $class;
2364
			$result[$class] = $pageTypeName;
2365
2366
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2367
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2368
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2369
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2370
				$result[$class] = $result[$class] .  " ({$class})";
2371
			}
2372
		}
2373
2374
		// sort alphabetically, and put current on top
2375
		asort($result);
2376
		if($currentClass) {
2377
			$currentPageTypeName = $result[$currentClass];
2378
			unset($result[$currentClass]);
2379
			$result = array_reverse($result);
2380
			$result[$currentClass] = $currentPageTypeName;
2381
			$result = array_reverse($result);
2382
		}
2383
2384
		return $result;
2385
	}
2386
2387
	/**
2388
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2389
	 *
2390
	 * @return string[]
2391
	 */
2392
	public function allowedChildren() {
2393
		$allowedChildren = array();
2394
		$candidates = $this->stat('allowed_children');
2395
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2396
			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...
2397
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2398
				// Otherwise, the class and all its subclasses are allowed.
2399
				if(substr($candidate,0,1) == '*') {
2400
					$allowedChildren[] = substr($candidate,1);
2401
				} else {
2402
					$subclasses = ClassInfo::subclassesFor($candidate);
2403
					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...
2404
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2405
					}
2406
				}
2407
			}
2408
		}
2409
2410
		return $allowedChildren;
2411
	}
2412
2413
	/**
2414
	 * Returns the class name of the default class for children of this page.
2415
	 *
2416
	 * @return string
2417
	 */
2418
	public function defaultChild() {
2419
		$default = $this->stat('default_child');
2420
		$allowed = $this->allowedChildren();
2421
		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...
2422
			if(!$default || !in_array($default, $allowed))
2423
				$default = reset($allowed);
2424
			return $default;
2425
		}
2426
	}
2427
2428
	/**
2429
	 * Returns the class name of the default class for the parent of this page.
2430
	 *
2431
	 * @return string
2432
	 */
2433
	public function defaultParent() {
2434
		return $this->stat('default_parent');
2435
	}
2436
2437
	/**
2438
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2439
	 * Title field.
2440
	 *
2441
	 * @return string
2442
	 */
2443
	public function getMenuTitle(){
2444
		if($value = $this->getField("MenuTitle")) {
2445
			return $value;
2446
		} else {
2447
			return $this->getField("Title");
2448
		}
2449
	}
2450
2451
2452
	/**
2453
	 * Set the menu title for this page.
2454
	 *
2455
	 * @param string $value
2456
	 */
2457
	public function setMenuTitle($value) {
2458
		if($value == $this->getField("Title")) {
2459
			$this->setField("MenuTitle", null);
2460
		} else {
2461
			$this->setField("MenuTitle", $value);
2462
		}
2463
	}
2464
2465
	/**
2466
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2467
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2468
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2469
	 * the flags.
2470
	 *
2471
	 * Example (simple):
2472
	 *   "deletedonlive" => "Deleted"
2473
	 *
2474
	 * Example (with optional title attribute):
2475
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2476
	 *
2477
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2478
	 * @return array
2479
	 */
2480
	public function getStatusFlags($cached = true) {
2481
		if(!$this->_cache_statusFlags || !$cached) {
2482
			$flags = array();
2483
			if($this->getIsDeletedFromStage()) {
2484
				if($this->getExistsOnLive()) {
2485
					$flags['removedfromdraft'] = array(
2486
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2487
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2488
					);
2489
				} else {
2490
					$flags['archived'] = array(
2491
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2492
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2493
					);
2494
				}
2495
			} else if($this->getIsAddedToStage()) {
2496
				$flags['addedtodraft'] = array(
2497
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2498
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2499
				);
2500
			} else if($this->getIsModifiedOnStage()) {
2501
				$flags['modified'] = array(
2502
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2503
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2504
				);
2505
			}
2506
2507
			$this->extend('updateStatusFlags', $flags);
2508
2509
			$this->_cache_statusFlags = $flags;
2510
		}
2511
2512
		return $this->_cache_statusFlags;
2513
	}
2514
2515
	/**
2516
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2517
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2518
	 * publication status.
2519
	 *
2520
	 * @return string An HTML string ready to be directly used in a template
2521
	 */
2522
	public function getTreeTitle() {
2523
		// Build the list of candidate children
2524
		$children = array();
2525
		$candidates = static::page_type_classes();
2526
		foreach($this->allowedChildren() as $childClass) {
2527
			if(!in_array($childClass, $candidates)) continue;
2528
			$child = singleton($childClass);
2529
			if($child->canCreate(null, array('Parent' => $this))) {
2530
				$children[$childClass] = $child->i18n_singular_name();
2531
			}
2532
		}
2533
		$flags = $this->getStatusFlags();
2534
		$treeTitle = sprintf(
2535
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2536
			Convert::raw2att(Convert::raw2json($children)),
2537
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2538
		);
2539
		foreach($flags as $class => $data) {
2540
			if(is_string($data)) $data = array('text' => $data);
2541
			$treeTitle .= sprintf(
2542
				"<span class=\"badge %s\"%s>%s</span>",
2543
				'status-' . Convert::raw2xml($class),
2544
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2545
				Convert::raw2xml($data['text'])
2546
			);
2547
		}
2548
2549
		return $treeTitle;
2550
	}
2551
2552
	/**
2553
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2554
	 * we're currently inside, etc.
2555
	 *
2556
	 * @param int $level
2557
	 * @return SiteTree
2558
	 */
2559
	public function Level($level) {
2560
		$parent = $this;
2561
		$stack = array($parent);
2562
		while($parent = $parent->Parent) {
2563
			array_unshift($stack, $parent);
2564
		}
2565
2566
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2567
	}
2568
2569
	/**
2570
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2571
	 *
2572
	 * @return int
2573
	 */
2574
	public function getPageLevel() {
2575
		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...
2576
			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...
2577
		}
2578
		return 1;
2579
	}
2580
2581
	/**
2582
	 * Return the CSS classes to apply to this node in the CMS tree.
2583
	 *
2584
	 * @param string $numChildrenMethod
2585
	 * @return string
2586
	 */
2587
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2588
		$classes = sprintf('class-%s', $this->class);
2589
		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...
2590
			$classes .= " BrokenLink";
2591
		}
2592
2593
		if(!$this->canAddChildren()) {
2594
			$classes .= " nochildren";
2595
		}
2596
2597
		if(!$this->canEdit() && !$this->canAddChildren()) {
2598
			if (!$this->canView()) {
2599
				$classes .= " disabled";
2600
			} else {
2601
				$classes .= " edit-disabled";
2602
			}
2603
		}
2604
2605
		if(!$this->ShowInMenus) {
2606
			$classes .= " notinmenu";
2607
		}
2608
2609
		//TODO: Add integration
2610
		/*
2611
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2612
			$classes .= " untranslated ";
2613
		*/
2614
		$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...
2615
2616
		return $classes;
2617
	}
2618
2619
	/**
2620
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2621
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2622
	 *
2623
	 * @return bool
2624
	 */
2625
	public function getIsDeletedFromStage() {
2626
		if(!$this->ID) return true;
2627
		if($this->isNew()) return false;
2628
2629
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2630
2631
		// Return true for both completely deleted pages and for pages just deleted from stage
2632
		return !($stageVersion);
2633
	}
2634
2635
	/**
2636
	 * Return true if this page exists on the live site
2637
	 *
2638
	 * @return bool
2639
	 */
2640
	public function getExistsOnLive() {
2641
		return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2642
	}
2643
2644
	/**
2645
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2646
	 * unpublished changes to the draft site.
2647
	 *
2648
	 * @return bool
2649
	 */
2650
	public function getIsModifiedOnStage() {
2651
		// New unsaved pages could be never be published
2652
		if($this->isNew()) return false;
2653
2654
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2655
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2656
2657
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2658
		$this->extend('getIsModifiedOnStage', $isModified);
2659
2660
		return $isModified;
2661
	}
2662
2663
	/**
2664
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2665
	 * published.
2666
	 *
2667
	 * @return bool
2668
	 */
2669
	public function getIsAddedToStage() {
2670
		// New unsaved pages could be never be published
2671
		if($this->isNew()) return false;
2672
2673
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2674
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2675
2676
		return ($stageVersion && !$liveVersion);
2677
	}
2678
2679
	/**
2680
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2681
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2682
	 */
2683
	static public function disableCMSFieldsExtensions() {
2684
		self::$runCMSFieldsExtensions = false;
2685
	}
2686
2687
	/**
2688
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2689
	 * disableCMSFieldsExtensions().
2690
	 */
2691
	static public function enableCMSFieldsExtensions() {
2692
		self::$runCMSFieldsExtensions = true;
2693
	}
2694
2695
	public function providePermissions() {
2696
		return array(
2697
			'SITETREE_GRANT_ACCESS' => array(
2698
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2699
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2700
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2701
				'sort' => 100
2702
			),
2703
			'SITETREE_VIEW_ALL' => array(
2704
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2705
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2706
				'sort' => -100,
2707
				'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')
2708
			),
2709
			'SITETREE_EDIT_ALL' => array(
2710
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2711
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2712
				'sort' => -50,
2713
				'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')
2714
			),
2715
			'SITETREE_REORGANISE' => array(
2716
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2717
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2718
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2719
				'sort' => 100
2720
			),
2721
			'VIEW_DRAFT_CONTENT' => array(
2722
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2723
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2724
				'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.'),
2725
				'sort' => 100
2726
			)
2727
		);
2728
	}
2729
2730
	/**
2731
	 * Return the translated Singular name.
2732
	 *
2733
	 * @return string
2734
	 */
2735
	public function i18n_singular_name() {
2736
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2737
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2738
		return _t($class.'.SINGULARNAME', $this->singular_name());
2739
	}
2740
2741
	/**
2742
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2743
	 * picks it up for the wrong folder.
2744
	 *
2745
	 * @return array
2746
	 */
2747
	public function provideI18nEntities() {
2748
		$entities = parent::provideI18nEntities();
2749
2750
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2751
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2752
2753
		$entities[$this->class . '.DESCRIPTION'] = array(
2754
			$this->stat('description'),
2755
			'Description of the page type (shown in the "add page" dialog)'
2756
		);
2757
2758
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2759
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2760
2761
		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...
2762
	}
2763
2764
	/**
2765
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2766
	 *
2767
	 * @return string
2768
	 */
2769
	public function getParentType() {
2770
		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...
2771
	}
2772
2773
	/**
2774
	 * Clear the permissions cache for SiteTree
2775
	 */
2776
	public static function reset() {
2777
		self::$cache_permissions = array();
2778
	}
2779
2780
	static public function on_db_reset() {
2781
		self::$cache_permissions = array();
2782
	}
2783
2784
}
2785