Completed
Push — master ( 41d4aa...7dfe14 )
by Damian
02:52
created

SiteTree   F

Complexity

Total Complexity 489

Size/Duplication

Total Lines 3043
Duplicated Lines 1.68 %

Coupling/Cohesion

Components 1
Dependencies 49
Metric Value
wmc 489
lcom 1
cbo 49
dl 51
loc 3043
rs 0.5217

100 Methods

Rating   Name   Duplication   Size   Complexity  
A set_enforce_strict_hierarchy() 0 4 1
A get_enforce_strict_hierarchy() 0 4 1
A nested_urls() 0 4 1
A enable_nested_urls() 0 4 1
A disable_nested_urls() 0 4 1
A set_create_default_pages() 0 4 1
A get_create_default_pages() 0 4 1
A Link() 0 3 1
A CMSEditLink() 0 3 1
A ElementName() 0 3 1
A isCurrent() 0 3 2
A isOrphaned() 0 8 4
A LinkOrCurrent() 0 3 2
A LinkOrSection() 0 3 2
A duplicateWithChildren() 0 16 4
C get_by_link() 6 66 20
B page_type_classes() 0 32 6
B link_shortcode_handler() 0 18 6
A AbsoluteLink() 0 7 2
A PreviewLink() 0 7 2
D RelativeLink() 0 25 9
A getAbsoluteLiveLink() 0 16 3
A isSection() 0 5 3
A LinkingMode() 0 9 3
A InSection() 0 9 3
A duplicate() 0 15 2
A duplicateAsChild() 0 6 1
A Breadcrumbs() 0 8 1
B getBreadcrumbItems() 0 18 9
A setParent() 0 8 3
A getParent() 0 5 2
A NestedTitle() 0 9 3
B can() 3 17 12
B canAddChildren() 3 18 9
D canView() 3 39 19
B canDelete() 3 20 7
C canCreate() 3 32 11
C canEdit() 3 24 8
B canPublish() 0 12 7
A canDeleteFromLive() 0 7 2
A getSiteConfig() 0 9 3
A prepopulate_permission_cache() 0 10 3
D batch_permission_check() 0 123 23
A can_edit_multiple() 0 3 1
B can_delete_multiple() 0 59 8
A collateDescendants() 0 9 4
D MetaTags() 0 33 10
A ContentSource() 0 3 1
C requireDefaultRecords() 0 49 7
C onBeforeWrite() 0 50 11
A syncLinkTracking() 0 3 1
B onAfterWrite() 0 19 5
A onBeforeDelete() 0 10 4
A onAfterDelete() 0 13 3
A flushCache() 0 4 1
C validate() 10 37 8
D validURLSegment() 0 34 15
A generateURLSegment() 0 12 4
A getStageURLSegment() 0 6 2
A getLiveURLSegment() 0 6 2
C rewriteFileLinks() 0 34 7
C DependentPages() 0 50 10
B VirtualPages() 0 22 4
F getCMSFields() 0 180 22
C getSettingsFields() 0 108 8
B fieldLabels() 0 38 3
D getCMSActions() 17 151 24
C doPublish() 0 42 7
D doUnpublish() 0 38 9
A doRevertToLive() 0 18 2
A isParentArchived() 0 9 4
B doRestoreToStage() 0 36 6
A doArchive() 0 12 2
B canArchive() 0 23 6
A doDeleteFromLive() 0 3 1
A isNew() 0 11 3
A isPublished() 0 8 3
D getClassDropdown() 0 43 9
B allowedChildren() 0 20 8
A defaultChild() 0 9 4
A defaultParent() 0 3 1
A getMenuTitle() 0 7 2
A setMenuTitle() 0 7 2
C getStatusFlags() 0 34 7
C getTreeTitle() 0 29 7
A Level() 0 9 3
A getPageLevel() 0 6 2
C CMSTreeClasses() 0 31 8
A getIsDeletedFromStage() 0 9 3
A getExistsOnLive() 0 3 1
A getIsModifiedOnStage() 0 12 3
A getIsAddedToStage() 0 9 3
A disableCMSFieldsExtensions() 0 3 1
A enableCMSFieldsExtensions() 0 3 1
B providePermissions() 0 34 1
A i18n_singular_name() 0 5 2
A provideI18nEntities() 0 16 3
A getParentType() 0 3 2
A reset() 0 3 1
A on_db_reset() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SiteTree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SiteTree, and based on these observations, apply Extract Interface, too.

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
545
			'"SiteTree"."ID"' => $this->ID
546
		));
547
		if($live) {
548
			$link = $live->AbsoluteLink();
549
			if($includeStageEqualsLive) $link .= '?stage=Live';
550
		} else {
551
			$link = null;
552
		}
553
554
		Versioned::reading_stage($oldStage);
555
		return $link;
556
	}
557
	
558
	/**
559
	 * Generates a link to edit this page in the CMS.
560
	 *
561
	 * @return string
562
	 */
563
	public function CMSEditLink() {
564
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
565
	}
566
	
567
		
568
	/**
569
	 * Return a CSS identifier generated from this page's link.
570
	 *
571
	 * @return string The URL segment
572
	 */
573
	public function ElementName() {
574
		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...
575
	}
576
	
577
	/**
578
	 * Returns true if this is the currently active page being used to handle this request.
579
	 *
580
	 * @return bool
581
	 */
582
	public function isCurrent() {
583
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
584
	}
585
	
586
	/**
587
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
588
	 * currently being viewed).
589
	 *
590
	 * @return bool
591
	 */
592
	public function isSection() {
593
		return $this->isCurrent() || (
594
			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...
595
		);
596
	}
597
	
598
	/**
599
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
600
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
601
	 * to external users.
602
	 *
603
	 * @return bool
604
	 */
605
	public function isOrphaned() {
606
		// Always false for root pages
607
		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...
608
		
609
		// Parent must exist and not be an orphan itself
610
		$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...
611
		return !$parent || !$parent->exists() || $parent->isOrphaned();
612
	}
613
	
614
	/**
615
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
616
	 *
617
	 * @return string
618
	 */
619
	public function LinkOrCurrent() {
620
		return $this->isCurrent() ? 'current' : 'link';
621
	}
622
	
623
	/**
624
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
625
	 *
626
	 * @return string
627
	 */
628
	public function LinkOrSection() {
629
		return $this->isSection() ? 'section' : 'link';
630
	}
631
	
632
	/**
633
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
634
	 * but in the current section.
635
	 *
636
	 * @return string
637
	 */
638
	public function LinkingMode() {
639
		if($this->isCurrent()) {
640
			return 'current';
641
		} elseif($this->isSection()) {
642
			return 'section';
643
		} else {
644
			return 'link';
645
		}
646
	}
647
	
648
	/**
649
	 * Check if this page is in the given current section.
650
	 *
651
	 * @param string $sectionName Name of the section to check
652
	 * @return bool True if we are in the given section
653
	 */
654
	public function InSection($sectionName) {
655
		$page = Director::get_current_page();
656
		while($page) {
657
			if($sectionName == $page->URLSegment)
658
				return true;
659
			$page = $page->Parent;
660
		}
661
		return false;
662
	}
663
664
	/**
665
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
666
	 * such behaviour.
667
	 *
668
	 * @param bool $doWrite Whether to write the new object before returning it
669
	 * @return self The duplicated object
670
	 */
671
	 public function duplicate($doWrite = true) {
672
		
673
		$page = parent::duplicate(false);
674
		$page->Sort = 0;
675
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
676
		
677
		if($doWrite) {
678
			$page->write();
679
680
			$page = $this->duplicateManyManyRelations($this, $page);
681
		}
682
		$this->invokeWithExtensions('onAfterDuplicate', $page);
683
		
684
		return $page;
685
	}
686
687
	/**
688
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
689
	 *
690
	 * @return self The duplicated object
691
	 */
692
	public function duplicateWithChildren() {
693
		$clone = $this->duplicate();
694
		$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...
695
696
		if($children) {
697
			foreach($children as $child) {
698
				$childClone = method_exists($child, 'duplicateWithChildren')
699
					? $child->duplicateWithChildren()
700
					: $child->duplicate();
701
				$childClone->ParentID = $clone->ID;
702
				$childClone->write();
703
			}
704
		}
705
706
		return $clone;
707
	}
708
709
	/**
710
	 * Duplicate this node and its children as a child of the node with the given ID
711
	 *
712
	 * @param int $id ID of the new node's new parent
713
	 */
714
	public function duplicateAsChild($id) {
715
		$newSiteTree = $this->duplicate();
716
		$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...
717
		$newSiteTree->Sort = 0;
718
		$newSiteTree->write();
719
	}
720
	
721
	/**
722
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
723
	 *
724
	 * @param int $maxDepth The maximum depth to traverse.
725
	 * @param boolean $unlinked Whether to link page titles.
726
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
727
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
728
	 * @return HTMLText The breadcrumb trail.
729
	 */
730
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
731
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
732
		$template = new SSViewer('BreadcrumbsTemplate');
733
		return $template->process($this->customise(new ArrayData(array(
734
			"Pages" => $pages,
735
			"Unlinked" => $unlinked
736
		))));
737
	}
738
739
740
	/**
741
	 * Returns a list of breadcrumbs for the current page.
742
	 *
743
	 * @param int $maxDepth The maximum depth to traverse.
744
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
745
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
746
	 *
747
	 * @return ArrayList
748
	*/
749
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
750
		$page = $this;
751
		$pages = array();
752
		
753
		while(
754
			$page
755
 			&& (!$maxDepth || count($pages) < $maxDepth)
756
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
757
 		) {
758
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
759
				$pages[] = $page;
760
			}
761
			
762
			$page = $page->Parent;
763
		}
764
765
		return new ArrayList(array_reverse($pages));
766
	}
767
768
769
	/**
770
	 * Make this page a child of another page.
771
	 *
772
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
773
	 *
774
	 * @param SiteTree|int $item Either the parent object, or the parent ID
775
	 */
776
	public function setParent($item) {
777
		if(is_object($item)) {
778
			if (!$item->exists()) $item->write();
779
			$this->setField("ParentID", $item->ID);
780
		} else {
781
			$this->setField("ParentID", $item);
782
		}
783
	}
784
 	
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
785
	/**
786
	 * Get the parent of this page.
787
	 *
788
	 * @return SiteTree Parent of this page
789
	 */
790
	public function getParent() {
791
		if ($parentID = $this->getField("ParentID")) {
792
			return DataObject::get_by_id("SiteTree", $parentID);
793
		}
794
	}
795
796
	/**
797
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
798
	 *
799
	 * @param int $level The maximum amount of levels to traverse.
800
	 * @param string $separator Seperating string
801
	 * @return string The resulting string
802
	 */
803
	public function NestedTitle($level = 2, $separator = " - ") {
804
		$item = $this;
805
		while($item && $level > 0) {
806
			$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...
807
			$item = $item->Parent;
808
			$level--;
809
		}
810
		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...
811
	}
812
813
	/**
814
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
815
	 * the security model for an application.
816
	 *
817
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
818
	 * - Checks for existence of a method named "can<$perm>()" on the object
819
	 * - Calls decorators and only returns for FALSE "vetoes"
820
	 * - Falls back to {@link Permission::check()}
821
	 * - Does NOT check for many-many relations named "Can<$perm>"
822
	 *
823
	 * @uses DataObjectDecorator->can()
824
	 *
825
	 * @param string $perm The permission to be checked, such as 'View'
826
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
827
	 * @return bool True if the the member is allowed to do the given action
828
	 */
829
	public function can($perm, $member = null) {
830 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...
831
			$member = Member::currentUserID();
832
		}
833
834
		if($member && Permission::checkMember($member, "ADMIN")) return true;
835
		
836
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
837
			$method = 'can' . ucfirst($perm);
838
			return $this->$method($member);
839
		}
840
		
841
		$results = $this->extend('can', $member);
842
		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...
843
844
		return ($member && Permission::checkMember($member, $perm));
845
	}
846
847
	/**
848
	 * This function should return true if the current user can add children to this page. It can be overloaded to
849
	 * customise the security model for an application.
850
	 *
851
	 * Denies permission if any of the following conditions is true:
852
	 * - alternateCanAddChildren() on a extension returns false
853
	 * - canEdit() is not granted
854
	 * - There are no classes defined in {@link $allowed_children}
855
	 *
856
	 * @uses SiteTreeExtension->canAddChildren()
857
	 * @uses canEdit()
858
	 * @uses $allowed_children
859
	 *
860
	 * @param Member|int $member
861
	 * @return bool True if the current user can add children
862
	 */
863
	public function canAddChildren($member = null) {
864
		// Disable adding children to archived pages
865
		if($this->getIsDeletedFromStage()) {
866
			return false;
867
		}
868
869 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...
870
			$member = Member::currentUserID();
871
		}
872
873
		if($member && Permission::checkMember($member, "ADMIN")) return true;
874
		
875
		// Standard mechanism for accepting permission changes from extensions
876
		$extended = $this->extendedCan('canAddChildren', $member);
877
		if($extended !== null) return $extended;
878
		
879
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
880
	}
881
882
	/**
883
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
884
	 * security model for an application.
885
	 *
886
	 * Denies permission if any of the following conditions is true:
887
	 * - canView() on any extension returns false
888
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
889
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
890
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
891
	 *
892
	 * @uses DataExtension->canView()
893
	 * @uses ViewerGroups()
894
	 *
895
	 * @param Member|int $member
896
	 * @return bool True if the current user can view this page
897
	 */
898
	public function canView($member = null) {
899 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...
900
			$member = Member::currentUserID();
901
		}
902
903
		// admin override
904
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
905
		
906
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
907
		if($this->isOrphaned()) return false;
908
909
		// Standard mechanism for accepting permission changes from extensions
910
		$extended = $this->extendedCan('canView', $member);
911
		if($extended !== null) return $extended;
912
		
913
		// check for empty spec
914
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
915
916
		// check for inherit
917
		if($this->CanViewType == 'Inherit') {
918
			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...
919
			else return $this->getSiteConfig()->canViewPages($member);
920
		}
921
		
922
		// check for any logged-in users
923
		if($this->CanViewType == 'LoggedInUsers' && $member) {
924
			return true;
925
		}
926
		
927
		// check for specific groups
928
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
929
		if(
930
			$this->CanViewType == 'OnlyTheseUsers'
931
			&& $member
932
			&& $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...
933
		) return true;
934
		
935
		return false;
936
	}
937
938
	/**
939
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
940
	 * security model for an application.
941
	 *
942
	 * Denies permission if any of the following conditions is true:
943
	 * - canDelete() returns false on any extension
944
	 * - canEdit() returns false
945
	 * - any descendant page returns false for canDelete()
946
	 *
947
	 * @uses canDelete()
948
	 * @uses SiteTreeExtension->canDelete()
949
	 * @uses canEdit()
950
	 *
951
	 * @param Member $member
952
	 * @return bool True if the current user can delete this page
953
	 */
954
	public function canDelete($member = null) {
955 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...
956
		else if(is_numeric($member)) $memberID = $member;
957
		else $memberID = Member::currentUserID();
958
		
959
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
960
			return true;
961
		}
962
		
963
		// Standard mechanism for accepting permission changes from extensions
964
		$extended = $this->extendedCan('canDelete', $memberID);
965
		if($extended !== null) return $extended;
966
				
967
		// Regular canEdit logic is handled by can_edit_multiple
968
		$results = self::can_delete_multiple(array($this->ID), $memberID);
969
		
970
		// If this page no longer exists in stage/live results won't contain the page.
971
		// Fail-over to false
972
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
973
	}
974
975
	/**
976
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
977
	 * can be overloaded to customise the security model for an application.
978
	 *
979
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
980
	 * create beneath a parent is based on the ability to edit that parent page.
981
	 *
982
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
983
	 *
984
	 * @uses $can_create
985
	 * @uses DataExtension->canCreate()
986
	 *
987
	 * @param Member $member
988
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
989
	 *                       If a parent page is known, it will be checked for validity.
990
	 *                       If omitted, it will be assumed this is to be created as a top level page.
991
	 * @return bool True if the current user can create pages on this class.
992
	 */
993
	public function canCreate($member = null, $context = array()) {
994 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...
995
			$member = Member::currentUserID();
996
		}
997
998
		// Check parent (custom canCreate option for SiteTree)
999
		// Block children not allowed for this parent type
1000
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1001
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1002
			return false;
1003
		}
1004
1005
		// Check permission
1006
		if($member && Permission::checkMember($member, "ADMIN")) {
1007
			return true;
1008
		}
1009
1010
		// Standard mechanism for accepting permission changes from extensions
1011
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1012
		if($extended !== null) {
1013
			return $extended;
1014
		}
1015
1016
		// Fall over to inherited permissions
1017
		if($parent) {
1018
			return $parent->canAddChildren($member);
1019
		} else {
1020
			// This doesn't necessarily mean we are creating a root page, but that
1021
			// we don't know if there is a parent, so default to this permission
1022
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1023
		}
1024
	}
1025
1026
	/**
1027
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1028
	 * security model for an application.
1029
	 *
1030
	 * Denies permission if any of the following conditions is true:
1031
	 * - canEdit() on any extension returns false
1032
	 * - canView() return false
1033
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1034
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1035
	 *   CMS_Access_CMSMAIN permission code
1036
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1037
	 *
1038
	 * @uses canView()
1039
	 * @uses EditorGroups()
1040
	 * @uses DataExtension->canEdit()
1041
	 *
1042
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1043
	 *                       unit tests)
1044
	 * @return bool True if the current user can edit this page
1045
	 */
1046
	public function canEdit($member = null) {
1047 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...
1048
		else if(is_numeric($member)) $memberID = $member;
1049
		else $memberID = Member::currentUserID();
1050
		
1051
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
1052
		
1053
		// Standard mechanism for accepting permission changes from extensions
1054
		$extended = $this->extendedCan('canEdit', $memberID);
1055
		if($extended !== null) return $extended;
1056
1057
		if($this->ID) {
1058
			// Regular canEdit logic is handled by can_edit_multiple
1059
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1060
1061
			// If this page no longer exists in stage/live results won't contain the page.
1062
			// Fail-over to false
1063
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1064
			
1065
		// Default for unsaved pages
1066
		} else {
1067
			return $this->getSiteConfig()->canEditPages($member);
1068
		}
1069
	}
1070
1071
	/**
1072
	 * This function should return true if the current user can publish this page. It can be overloaded to customise
1073
	 * the security model for an application.
1074
	 *
1075
	 * Denies permission if any of the following conditions is true:
1076
	 * - canPublish() on any extension returns false
1077
	 * - canEdit() returns false
1078
	 *
1079
	 * @uses SiteTreeExtension->canPublish()
1080
	 *
1081
	 * @param Member $member
1082
	 * @return bool True if the current user can publish this page.
1083
	 */
1084
	public function canPublish($member = null) {
1085
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1086
		
1087
		if($member && Permission::checkMember($member, "ADMIN")) return true;
1088
1089
		// Standard mechanism for accepting permission changes from extensions
1090
		$extended = $this->extendedCan('canPublish', $member);
0 ignored issues
show
Documentation introduced by
$member is of type null|object<DataObject>, but the function expects a object<Member>|integer.

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...
1091
		if($extended !== null) return $extended;
1092
1093
		// Normal case - fail over to canEdit()
1094
		return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 1085 can also be of type object<DataObject>; however, SiteTree::canEdit() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1095
	}
1096
	
1097
	public function canDeleteFromLive($member = null) {
1098
		// Standard mechanism for accepting permission changes from extensions
1099
		$extended = $this->extendedCan('canDeleteFromLive', $member);
1100
		if($extended !==null) return $extended;
1101
1102
		return $this->canPublish($member);
1103
	}
1104
	
1105
	/**
1106
	 * Stub method to get the site config, unless the current class can provide an alternate.
1107
	 *
1108
	 * @return SiteConfig
1109
	 */
1110
	public function getSiteConfig() {
1111
		
1112
		if($this->hasMethod('alternateSiteConfig')) {
1113
			$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...
1114
			if($altConfig) return $altConfig;
1115
		}
1116
		
1117
		return SiteConfig::current_site_config();
1118
	}
1119
1120
	/**
1121
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1122
	 * can_(perm)_multiple method for efficiency.
1123
	 *
1124
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1125
	 * @param array           $ids           An array of page IDs
1126
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1127
	 *                                       to 'SiteTree::can_(permission)_multiple'
1128
	 */
1129
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1130
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1131
		
1132
		if(is_callable($batchCallback)) {
1133
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1134
		} else {
1135
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1136
				. "with callback '$batchCallback'", E_USER_WARNING);
1137
		}
1138
	}
1139
1140
	/**
1141
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1142
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1143
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1144
	 * efficiently.
1145
	 *
1146
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1147
	 * property to FALSE.
1148
	 *
1149
	 * @param array  $ids              Of {@link SiteTree} IDs
1150
	 * @param int    $memberID         Member ID
1151
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1152
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1153
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1154
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1155
	 * @param bool   $useCached
1156
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1157
	 */
1158
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1159
												  $globalPermission = null, $useCached = true) {
1160
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1161
1162
		// Sanitise the IDs
1163
		$ids = array_filter($ids, 'is_numeric');
1164
		
1165
		// This is the name used on the permission cache
1166
		// converts something like 'CanEditType' to 'edit'.
1167
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1168
1169
		// Default result: nothing editable
1170
		$result = array_fill_keys($ids, false);
1171
		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...
1172
1173
			// Look in the cache for values
1174
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1175
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1176
			
1177
				// If we can't find everything in the cache, then look up the remainder separately
1178
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1179
				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...
1180
					$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 1160 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...
1181
				}
1182
				return $cachedValues;
1183
			}
1184
		
1185
			// If a member doesn't have a certain permission then they can't edit anything
1186
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1187
				return $result;
1188
			}
1189
1190
			// Placeholder for parameterised ID list
1191
			$idPlaceholders = DB::placeholders($ids);
1192
1193
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1194
			// be enabled
1195
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1196
		
1197
			// Get the groups that the given member belongs to
1198
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1199
			$SQL_groupList = implode(", ", $groupIDs);
1200
			if (!$SQL_groupList) $SQL_groupList = '0';
1201
			
1202
			$combinedStageResult = array();
1203
1204
			foreach(array('Stage', 'Live') as $stage) {
1205
				// Start by filling the array with the pages that actually exist
1206
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1207
				
1208
				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...
1209
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1210
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1211
				} else {
1212
					$stageIds = array();
1213
				}
1214
				$result = array_fill_keys($stageIds, false);
1215
				
1216
				// Get the uninherited permissions
1217
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1218
					->where(array(
1219
						"(\"$typeField\" = 'LoggedInUsers' OR
1220
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1221
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1222
						=> $ids
1223
					))
1224
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1225
				
1226
				if($uninheritedPermissions) {
1227
					// Set all the relevant items in $result to true
1228
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1229
				}
1230
1231
				// Get permissions that are inherited
1232
				$potentiallyInherited = Versioned::get_by_stage(
1233
					"SiteTree",
1234
					$stage,
1235
					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...
1236
				);
1237
1238
				if($potentiallyInherited) {
1239
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1240
					// then see which ones the user has permission on
1241
					$groupedByParent = array();
1242
					foreach($potentiallyInherited as $item) {
1243
						if($item->ParentID) {
1244
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1245
							$groupedByParent[$item->ParentID][] = $item->ID;
1246
						} else {
1247
							// Might return different site config based on record context, e.g. when subsites module
1248
							// is used
1249
							$siteConfig = $item->getSiteConfig();
1250
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1251
						}
1252
					}
1253
1254
					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...
1255
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1256
						if($actuallyInherited) {
1257
							$parentIDs = array_keys(array_filter($actuallyInherited));
1258
							foreach($parentIDs as $parentID) {
1259
								// Set all the relevant items in $result to true
1260
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1261
							}
1262
						}
1263
					}
1264
				}
1265
				
1266
				$combinedStageResult = $combinedStageResult + $result;
1267
				
1268
			}
1269
		}
1270
1271
		if(isset($combinedStageResult)) {
1272
			// Cache the results
1273
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1274
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1275
1276
			return $combinedStageResult;
1277
		} else {
1278
			return array();
1279
		}
1280
	}
1281
1282
	/**
1283
	 * Get the 'can edit' information for a number of SiteTree pages.
1284
	 *
1285
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1286
	 * @param int   $memberID  ID of member
1287
	 * @param bool  $useCached Return values from the permission cache if they exist
1288
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1289
	 *                         edited
1290
	 */
1291
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1292
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1293
	}
1294
1295
	/**
1296
	 * Get the 'can edit' information for a number of SiteTree pages.
1297
	 *
1298
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1299
	 * @param int   $memberID  ID of member
1300
	 * @param bool  $useCached Return values from the permission cache if they exist
1301
	 * @return array
1302
	 */
1303
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1304
		$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...
1305
		$result = array_fill_keys($ids, false);
1306
		$cacheKey = "delete-$memberID";
1307
		
1308
		// Look in the cache for values
1309
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1310
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1311
			
1312
			// If we can't find everything in the cache, then look up the remainder separately
1313
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1314
			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...
1315
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1316
					+ $cachedValues;
1317
			}
1318
			return $cachedValues;
1319
		}
1320
1321
		// You can only delete pages that you can edit
1322
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1323
		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...
1324
		
1325
			// You can only delete pages whose children you can delete
1326
			$editablePlaceholders = DB::placeholders($editableIDs);
1327
			$childRecords = SiteTree::get()->where(array(
1328
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1329
			));
1330
			if($childRecords) {
1331
				$children = $childRecords->map("ID", "ParentID");
1332
1333
				// Find out the children that can be deleted
1334
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1335
				
1336
				// Get a list of all the parents that have no undeletable children
1337
				$deletableParents = array_fill_keys($editableIDs, true);
1338
				foreach($deletableChildren as $id => $canDelete) {
1339
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1340
				}
1341
1342
				// Use that to filter the list of deletable parents that have children
1343
				$deletableParents = array_keys($deletableParents);
1344
1345
				// Also get the $ids that don't have children
1346
				$parents = array_unique($children->values());
1347
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1348
1349
				// Combine the two
1350
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1351
1352
			} else {
1353
				$deletable = $editableIDs;
1354
			}
1355
		} else {
1356
			$deletable = array();
1357
		}
1358
		
1359
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1360
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1361
	}
1362
1363
	/**
1364
	 * Collate selected descendants of this page.
1365
	 *
1366
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1367
	 * $collator array.
1368
	 *
1369
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1370
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1371
	 * @return bool
1372
	 */
1373
	public function collateDescendants($condition, &$collator) {
1374
		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...
1375
			foreach($children as $item) {
1376
				if(eval("return $condition;")) $collator[] = $item;
1377
				$item->collateDescendants($condition, $collator);
1378
			}
1379
			return true;
1380
		}
1381
	}
1382
1383
	/**
1384
	 * Return the title, description, keywords and language metatags.
1385
	 *
1386
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1387
	 *
1388
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1389
	 * @return string The XHTML metatags
1390
	 */
1391
	public function MetaTags($includeTitle = true) {
1392
		$tags = "";
1393
		if($includeTitle === true || $includeTitle == 'true') {
1394
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1395
		}
1396
1397
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1398
		if (!empty($generator)) {
1399
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1400
		}
1401
1402
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1403
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1404
		if($this->MetaDescription) {
1405
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1406
		}
1407
		if($this->ExtraMeta) {
1408
			$tags .= $this->ExtraMeta . "\n";
1409
		}
1410
		
1411
		if(Permission::check('CMS_ACCESS_CMSMain')
1412
			&& in_array('CMSPreviewable', class_implements($this))
1413
			&& !$this instanceof ErrorPage
1414
			&& $this->ID > 0
1415
		) {
1416
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1417
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1418
		}
1419
1420
		$this->extend('MetaTags', $tags);
1421
1422
		return $tags;
1423
	}
1424
1425
	/**
1426
	 * Returns the object that contains the content that a user would associate with this page.
1427
	 *
1428
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1429
	 * return the page that is linked to.
1430
	 *
1431
	 * @return $this
1432
	 */
1433
	public function ContentSource() {
1434
		return $this;
1435
	}
1436
1437
	/**
1438
	 * Add default records to database.
1439
	 *
1440
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1441
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1442
	 */
1443
	public function requireDefaultRecords() {
1444
		parent::requireDefaultRecords();
1445
		
1446
		// default pages
1447
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1448
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1449
				$homepage = new Page();
1450
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1451
				$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>');
1452
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1453
				$homepage->Sort = 1;
1454
				$homepage->write();
1455
				$homepage->publish('Stage', 'Live');
1456
				$homepage->flushCache();
1457
				DB::alteration_message('Home page created', 'created');
1458
			}
1459
1460
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1461
				$aboutus = new Page();
1462
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1463
				$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>');
1464
				$aboutus->Sort = 2;
1465
				$aboutus->write();
1466
				$aboutus->publish('Stage', 'Live');
1467
				$aboutus->flushCache();
1468
				DB::alteration_message('About Us page created', 'created');
1469
1470
				$contactus = new Page();
1471
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1472
				$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>');
1473
				$contactus->Sort = 3;
1474
				$contactus->write();
1475
				$contactus->publish('Stage', 'Live');
1476
				$contactus->flushCache();
1477
				DB::alteration_message('Contact Us page created', 'created');
1478
			}
1479
		}
1480
		
1481
		// schema migration
1482
		// @todo Move to migration task once infrastructure is implemented
1483
		if($this->class == 'SiteTree') {
1484
			$conn = DB::get_schema();
1485
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1486
			if($conn->hasField('SiteTree' ,'Viewers')) {
1487
				$task = new UpgradeSiteTreePermissionSchemaTask();
1488
				$task->run(new SS_HTTPRequest('GET','/'));
1489
			}
1490
		}
1491
	}
1492
1493
	protected function onBeforeWrite() {
1494
		parent::onBeforeWrite();
1495
1496
		// If Sort hasn't been set, make this page come after it's siblings
1497
		if(!$this->Sort) {
1498
			$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...
1499
			$this->Sort = DB::prepared_query(
1500
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1501
				array($parentID)
1502
			)->value();
1503
		}
1504
1505
		// If there is no URLSegment set, generate one from Title
1506
		$defaultSegment = $this->generateURLSegment(_t(
1507
			'CMSMain.NEWPAGE',
1508
			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...
1509
		));
1510
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1511
			$this->URLSegment = $this->generateURLSegment($this->Title);
1512
		} else if($this->isChanged('URLSegment', 2)) {
1513
			// Do a strict check on change level, to avoid double encoding caused by
1514
			// bogus changes through forceChange()
1515
			$filter = URLSegmentFilter::create();
1516
			$this->URLSegment = $filter->filter($this->URLSegment);
1517
			// If after sanitising there is no URLSegment, give it a reasonable default
1518
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1519
		}
1520
		
1521
		// Ensure that this object has a non-conflicting URLSegment value.
1522
		$count = 2;
1523
		while(!$this->validURLSegment()) {
1524
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1525
			$count++;
1526
		}
1527
1528
		$this->syncLinkTracking();
1529
1530
		// Check to see if we've only altered fields that shouldn't affect versioning
1531
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1532
		$changedFields = array_keys($this->getChangedFields(true, 2));
1533
1534
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1535
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1536
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1537
1538
		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...
1539
			// This will have the affect of preserving the versioning
1540
			$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...
1541
		}
1542
	}
1543
	
1544
	public function syncLinkTracking() {
1545
		$this->extend('augmentSyncLinkTracking');
1546
	}
1547
	
1548
	public function onAfterWrite() {
1549
		// Need to flush cache to avoid outdated versionnumber references
1550
		$this->flushCache();
1551
		
1552
		$linkedPages = $this->VirtualPages();
1553
		if($linkedPages) {
1554
			// The only way after a write() call to determine if it was triggered by a writeWithoutVersion(),
1555
			// which we have to pass on to the virtual page writes as well.
1556
			$previous = ($this->Version > 1) ? Versioned::get_version($this->class, $this->ID, $this->Version-1) : null;
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

Loading history...
Unused Code introduced by
$previous is not used, you could remove the assignment.

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

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

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

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

Loading history...
1557
			$withoutVersion = $this->getExtensionInstance('Versioned')->_nextWriteWithoutVersion;
0 ignored issues
show
Bug introduced by
The property _nextWriteWithoutVersion does not seem to exist in Extension.

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

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

Loading history...
1558
			foreach($linkedPages as $page) {
1559
				 $page->copyFrom($page->CopyContentFrom());
1560
				 if($withoutVersion) $page->writeWithoutVersion();
1561
				 else $page->write();
1562
			}
1563
		}
1564
		
1565
		parent::onAfterWrite();
1566
	}
1567
	
1568
	public function onBeforeDelete() {
1569
		parent::onBeforeDelete();
1570
		
1571
		// If deleting this page, delete all its children.
1572
		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...
1573
			foreach($children as $child) {
1574
				$child->delete();
1575
			}
1576
		}
1577
	}
1578
	
1579
	public function onAfterDelete() {
1580
		// Need to flush cache to avoid outdated versionnumber references
1581
		$this->flushCache();
1582
		
1583
		// Need to mark pages depending to this one as broken
1584
		$dependentPages = $this->DependentPages();
1585
		if($dependentPages) foreach($dependentPages as $page) {
1586
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1587
			$page->write();
1588
		}
1589
		
1590
		parent::onAfterDelete();
1591
	}
1592
1593
	public function flushCache($persistent = true) {
1594
		parent::flushCache($persistent);
1595
		$this->_cache_statusFlags = null;
1596
	}
1597
	
1598
	public function validate() {
1599
		$result = parent::validate();
1600
1601
		// Allowed children validation
1602
		$parent = $this->getParent();
1603
		if($parent && $parent->exists()) {
1604
			// No need to check for subclasses or instanceof, as allowedChildren() already
1605
			// deconstructs any inheritance trees already.
1606
			$allowed = $parent->allowedChildren();
1607
			$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...
1608
			if(!in_array($subject->ClassName, $allowed)) {
1609
				
1610
				$result->error(
1611
					_t(
1612
						'SiteTree.PageTypeNotAllowed',
1613
						'Page type "{type}" not allowed as child of this parent page',
1614
						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...
1615
					),
1616
					'ALLOWED_CHILDREN'
1617
				);
1618
			}
1619
		}
1620
1621
		// "Can be root" validation
1622 View Code Duplication
		if(!$this->stat('can_be_root') && !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1623
			$result->error(
1624
				_t(
1625
					'SiteTree.PageTypNotAllowedOnRoot',
1626
					'Page type "{type}" is not allowed on the root level',
1627
					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...
1628
				),
1629
				'CAN_BE_ROOT'
1630
			);
1631
		}
1632
		
1633
		return $result;
1634
	}
1635
	
1636
	/**
1637
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1638
	 * checks for:
1639
	 *  - A page with the same URLSegment that has a conflict
1640
	 *  - Conflicts with actions on the parent page
1641
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1642
	 *
1643
	 * @return bool
1644
	 */
1645
	public function validURLSegment() {
1646
		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...
1647
			if($controller = ModelAsController::controller_for($parent)) {
1648
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1649
			}
1650
		}
1651
		
1652
		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...
1653
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1654
		}
1655
		
1656
		// Filters by url, id, and parent
1657
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1658
		if($this->ID) {
1659
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1660
		}
1661
		if(self::config()->nested_urls) {
1662
			$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...
1663
		}
1664
		
1665
		$votes = array_filter(
1666
			(array)$this->extend('augmentValidURLSegment'),
1667
			function($v) {return !is_null($v);}
1668
		);
1669
		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...
1670
			return min($votes);
1671
		}
1672
1673
		// Check existence
1674
		$existingPage = DataObject::get_one('SiteTree', $filter);
1675
		if ($existingPage) return false;
1676
1677
		return !($existingPage);
1678
		}
1679
		
1680
	/**
1681
	 * Generate a URL segment based on the title provided.
1682
	 *
1683
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1684
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1685
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1686
	 * scratch, or incrementally modify the generated URL.
1687
	 *
1688
	 * @param string $title Page title
1689
	 * @return string Generated url segment
1690
	 */
1691
	public function generateURLSegment($title){
1692
		$filter = URLSegmentFilter::create();
1693
		$t = $filter->filter($title);
1694
		
1695
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1696
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1697
		
1698
		// Hook for extensions
1699
		$this->extend('updateURLSegment', $t, $title);
1700
		
1701
		return $t;
1702
	}
1703
	
1704
	/**
1705
	 * Gets the URL segment for the latest draft version of this page.
1706
	 *
1707
	 * @return string
1708
	 */
1709
	public function getStageURLSegment() {
1710
		$stageRecord = Versioned::get_one_by_stage('SiteTree', 'Stage', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1711
			'"SiteTree"."ID"' => $this->ID
1712
		));
1713
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1714
	}
1715
	
1716
	/**
1717
	 * Gets the URL segment for the currently published version of this page.
1718
	 *
1719
	 * @return string
1720
	 */
1721
	public function getLiveURLSegment() {
1722
		$liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1723
			'"SiteTree"."ID"' => $this->ID
1724
		));
1725
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1726
	}
1727
	
1728
	/**
1729
	 * Rewrites any linked images on this page.
1730
	 * Non-image files should be linked via shortcodes
1731
	 * Triggers the onRenameLinkedAsset action on extensions.
1732
	 * TODO: This doesn't work for HTMLText fields on other tables.
1733
	 */
1734
	public function rewriteFileLinks() {
1735
		// Update the content without actually creating a new version
1736
		foreach(array("SiteTree_Live", "SiteTree") as $table) {
1737
			// Published site
1738
			$published = DB::prepared_query(
1739
				"SELECT * FROM  \"$table\" WHERE \"ID\" = ?",
1740
				array($this->ID)
1741
			)->record();
1742
			$origPublished = $published;
1743
1744
			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...
1745
				// Skip if non HTML or if empty
1746
				if ($fieldType !== 'HTMLText' || empty($published[$fieldName])) {
1747
					continue;
1748
				}
1749
1750
				// Regenerate content
1751
				$content = Image::regenerate_html_links($published[$fieldName]);
1752
				if($content === $published[$fieldName]) {
1753
					continue;
1754
				}
1755
1756
				$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
1757
				DB::prepared_query($query, array($content, $this->ID));
1758
1759
				// Tell static caching to update itself
1760
				if($table == 'SiteTree_Live') {
1761
					$publishedClass = $origPublished['ClassName'];
1762
					$origPublishedObj = new $publishedClass($origPublished);
1763
					$this->invokeWithExtensions('onRenameLinkedAsset', $origPublishedObj);
1764
				}
1765
			}
1766
		}
1767
	}
1768
	
1769
	/**
1770
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1771
	 *
1772
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1773
	 * @return ArrayList
1774
	 */
1775
	public function DependentPages($includeVirtuals = true) {
1776
		if(class_exists('Subsite')) {
1777
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1778
			Subsite::disable_subsite_filter(true);
1779
		}
1780
		
1781
		// Content links
1782
		$items = new ArrayList();
1783
1784
		// We merge all into a regular SS_List, because DataList doesn't support merge
1785
		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...
1786
			$linkList = new ArrayList();
1787
			foreach($contentLinks as $item) {
1788
				$item->DependentLinkType = 'Content link';
1789
				$linkList->push($item);
1790
			}
1791
			$items->merge($linkList);
1792
		}
1793
		
1794
		// Virtual pages
1795
		if($includeVirtuals) {
1796
			$virtuals = $this->VirtualPages();
1797
			if($virtuals) {
1798
				$virtualList = new ArrayList();
1799
				foreach($virtuals as $item) {
1800
					$item->DependentLinkType = 'Virtual page';
1801
					$virtualList->push($item);
1802
				}
1803
				$items->merge($virtualList);
1804
			}
1805
		}
1806
1807
		// Redirector pages
1808
		$redirectors = RedirectorPage::get()->where(array(
1809
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1810
			'"RedirectorPage"."LinkToID"' => $this->ID
1811
		));
1812
		if($redirectors) {
1813
			$redirectorList = new ArrayList();
1814
			foreach($redirectors as $item) {
1815
				$item->DependentLinkType = 'Redirector page';
1816
				$redirectorList->push($item);
1817
			}
1818
			$items->merge($redirectorList);
1819
		}
1820
1821
		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...
1822
		
1823
		return $items;
1824
	}
1825
1826
	/**
1827
	 * Return all virtual pages that link to this page.
1828
	 *
1829
	 * @return DataList
1830
	 */
1831
	public function VirtualPages() {
1832
		
1833
		// Ignore new records
1834
		if(!$this->ID) return null;
1835
		
1836
		// Check subsite virtual pages
1837
		// @todo Refactor out subsite module specific code
1838
		if(class_exists('Subsite')) {
1839
			return Subsite::get_from_all_subsites('VirtualPage', array(
1840
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1841
			));
1842
		}
1843
		
1844
		// Check existing virtualpages
1845
		if(class_exists('VirtualPage')) {
1846
			return VirtualPage::get()->where(array(
1847
				'"VirtualPage"."CopyContentFromID"' => $this->ID
1848
			));
1849
		}
1850
		
1851
		return null;
1852
	}
1853
1854
	/**
1855
	 * Returns a FieldList with which to create the main editing form.
1856
	 *
1857
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1858
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1859
	 *
1860
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1861
	 * e.g. access control.
1862
	 *
1863
	 * @return FieldList The fields to be displayed in the CMS
1864
	 */
1865
	public function getCMSFields() {
1866
		require_once("forms/Form.php");
1867
		// Status / message
1868
		// Create a status message for multiple parents
1869
		if($this->ID && is_numeric($this->ID)) {
1870
			$linkedPages = $this->VirtualPages();
1871
1872
			$parentPageLinks = array();
1873
1874
			if($linkedPages->Count() > 0) {
1875
				foreach($linkedPages as $linkedPage) {
1876
					$parentPage = $linkedPage->Parent;
1877
					if($parentPage) {
1878
						if($parentPage->ID) {
1879
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1880
						} else {
1881
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1882
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1883
								"</a>";
1884
						}
1885
					}
1886
				}
1887
1888
				$lastParent = array_pop($parentPageLinks);
1889
				$parentList = "'$lastParent'";
1890
1891
				if(count($parentPageLinks) > 0) {
1892
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1893
						. $parentList;
1894
				}
1895
1896
				$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...
1897
					'SiteTree.APPEARSVIRTUALPAGES',
1898
					"This content also appears on the virtual pages in the {title} sections.",
1899
					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...
1900
				);
1901
			}
1902
		}
1903
1904
		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...
1905
			$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...
1906
		}
1907
1908
		$dependentNote = '';
1909
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1910
		
1911
		// Create a table for showing pages linked to this one
1912
		$dependentPages = $this->DependentPages();
1913
		$dependentPagesCount = $dependentPages->Count();
1914
		if($dependentPagesCount) {
1915
			$dependentColumns = array(
1916
				'Title' => $this->fieldLabel('Title'),
1917
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1918
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1919
			);
1920
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1921
			
1922
			$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>');
1923
			$dependentTable = GridField::create(
1924
				'DependentPages',
1925
				false,
1926
				$dependentPages
1927
			);
1928
			$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...
1929
				->setDisplayFields($dependentColumns)
1930
				->setFieldFormatting(array(
1931
					'Title' => function($value, &$item) {
1932
						return sprintf(
1933
							'<a href="admin/pages/edit/show/%d">%s</a>',
1934
							(int)$item->ID,
1935
							Convert::raw2xml($item->Title)
1936
						);
1937
					},
1938
					'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...
1939
						return sprintf(
1940
							'<a href="%s" target="_blank">%s</a>',
1941
							Convert::raw2xml($value),
1942
							Convert::raw2xml($value)
1943
						);
1944
					}
1945
				));
1946
		}
1947
		
1948
		$baseLink = Controller::join_links (
1949
			Director::absoluteBaseURL(),
1950
			(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...
1951
		);
1952
		
1953
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1954
			->setURLPrefix($baseLink)
1955
			->setDefaultURL($this->generateURLSegment(_t(
1956
				'CMSMain.NEWPAGE',
1957
				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...
1958
			)));
1959
		$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...
1960
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1961
			$helpText .= $helpText ? '<br />' : '';
1962
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1963
		}
1964
		$urlsegment->setHelpText($helpText);
1965
		
1966
		$fields = new FieldList(
1967
			$rootTab = new TabSet("Root",
1968
				$tabMain = new Tab('Main',
1969
					new TextField("Title", $this->fieldLabel('Title')),
1970
					$urlsegment,
1971
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1972
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1973
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1974
						array(
1975
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1976
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1977
						)
1978
					)->setHeadingLevel(4)
1979
				),
1980
				$tabDependent = new Tab('Dependent',
1981
					$dependentNote,
1982
					$dependentTable
1983
				)
1984
			)
1985
		);
1986
		$htmlField->addExtraClass('stacked');
1987
		
1988
		// Help text for MetaData on page content editor
1989
		$metaFieldDesc
1990
			->setRightTitle(
1991
				_t(
1992
					'SiteTree.METADESCHELP',
1993
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1994
				)
1995
			)
1996
			->addExtraClass('help');
1997
		$metaFieldExtra
1998
			->setRightTitle(
1999
				_t(
2000
					'SiteTree.METAEXTRAHELP',
2001
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2002
				)
2003
			)
2004
			->addExtraClass('help');
2005
2006
		// Conditional dependent pages tab
2007
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2008
		else $fields->removeFieldFromTab('Root', 'Dependent');
2009
		
2010
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2011
2012
		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...
2013
			$obsoleteWarning = _t(
2014
				'SiteTree.OBSOLETECLASS',
2015
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2016
				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...
2017
			);
2018
2019
			$fields->addFieldToTab(
2020
				"Root.Main",
2021
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2022
				"Title"
2023
			);
2024
		}
2025
2026
		if(file_exists(BASE_PATH . '/install.php')) {
2027
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2028
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2029
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2030
				. "</p>"), "Title");
2031
		}
2032
2033
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
2034
		$fields->setTabPathRewrites(array(
2035
			'/^Root\.Content\.Main$/' => 'Root.Main',
2036
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
2037
		));
2038
		
2039
		if(self::$runCMSFieldsExtensions) {
2040
			$this->extend('updateCMSFields', $fields);
2041
		}
2042
2043
		return $fields;
2044
	}
2045
	
2046
	
2047
	/**
2048
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2049
	 * for content-related fields.
2050
	 *
2051
	 * @return FieldList
2052
	 */
2053
	public function getSettingsFields() {
2054
		$groupsMap = array();
2055
		foreach(Group::get() as $group) {
2056
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2057
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2058
		}
2059
		asort($groupsMap);
2060
		
2061
		$fields = new FieldList(
2062
			$rootTab = new TabSet("Root",
2063
				$tabBehaviour = new Tab('Settings',
2064
					new DropdownField(
2065
						"ClassName",
2066
						$this->fieldLabel('ClassName'),
2067
						$this->getClassDropdown()
2068
					),
2069
					$parentTypeSelector = new CompositeField(
2070
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2071
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2072
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2073
						)),
2074
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
2075
					),
2076
					$visibility = new FieldGroup(
2077
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2078
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2079
					),
2080
					$viewersOptionsField = new OptionsetField(
2081
						"CanViewType",
2082
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2083
					),
2084
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2085
						->setMultiple(true)
2086
						->setSource($groupsMap)
2087
						->setAttribute(
2088
							'data-placeholder',
2089
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2090
						),
2091
					$editorsOptionsField = new OptionsetField(
2092
						"CanEditType",
2093
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2094
					),
2095
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2096
						->setMultiple(true)
2097
						->setSource($groupsMap)
2098
						->setAttribute(
2099
							'data-placeholder',
2100
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2101
						)
2102
				)
2103
			)
2104
		);
2105
		
2106
		$visibility->setTitle($this->fieldLabel('Visibility'));
2107
		
2108
2109
		// This filter ensures that the ParentID dropdown selection does not show this node,
2110
		// or its descendents, as this causes vanishing bugs
2111
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2112
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2113
		
2114
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2115
		
2116
		// Make page location fields read-only if the user doesn't have the appropriate permission
2117
		if(!Permission::check("SITETREE_REORGANISE")) {
2118
			$fields->makeFieldReadonly('ParentType');
2119
			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...
2120
				$fields->removeByName('ParentID');
2121
			} else {
2122
				$fields->makeFieldReadonly('ParentID');
2123
			}
2124
		}
2125
		
2126
		$viewersOptionsSource = array();
2127
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2128
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2129
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2130
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2131
		$viewersOptionsField->setSource($viewersOptionsSource);
2132
		
2133
		$editorsOptionsSource = array();
2134
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2135
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2136
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2137
		$editorsOptionsField->setSource($editorsOptionsSource);
2138
2139
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2140
			$fields->makeFieldReadonly($viewersOptionsField);
2141
			if($this->CanViewType == 'OnlyTheseUsers') {
2142
				$fields->makeFieldReadonly($viewerGroupsField);
2143
			} else {
2144
				$fields->removeByName('ViewerGroups');
2145
			}
2146
			
2147
			$fields->makeFieldReadonly($editorsOptionsField);
2148
			if($this->CanEditType == 'OnlyTheseUsers') {
2149
				$fields->makeFieldReadonly($editorGroupsField);
2150
			} else {
2151
				$fields->removeByName('EditorGroups');
2152
			}
2153
		}
2154
		
2155
		if(self::$runCMSFieldsExtensions) {
2156
			$this->extend('updateSettingsFields', $fields);
2157
		}
2158
		
2159
		return $fields;
2160
	}
2161
	
2162
	/**
2163
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2164
	 * @return array
2165
	 */
2166
	public function fieldLabels($includerelations = true) {
2167
		$cacheKey = $this->class . '_' . $includerelations;
2168
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2169
			$labels = parent::fieldLabels($includerelations);
2170
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2171
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2172
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2173
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2174
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2175
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2176
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2177
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2178
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2179
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2180
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2181
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2182
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2183
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2184
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2185
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2186
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2187
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2188
			$labels['LinkChangeNote'] = _t (
2189
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2190
			);
2191
			
2192
			if($includerelations){
2193
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2194
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2195
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2196
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2197
			}
2198
2199
			self::$_cache_field_labels[$cacheKey] = $labels;
2200
		}
2201
2202
		return self::$_cache_field_labels[$cacheKey];
2203
	}
2204
2205
	/**
2206
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2207
	 *
2208
	 * Frontend scripts and styles know how to handle the following FormFields:
2209
	 * - top-level FormActions appear as standalone buttons
2210
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2211
	 * - TabSet & Tabs appear as a drop ups
2212
	 * - FormActions within the Tab are restyled as links
2213
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2214
	 *
2215
	 * @return FieldList The available actions for this page.
2216
	 */
2217
	public function getCMSActions() {
2218
		$existsOnLive = $this->getExistsOnLive();
2219
2220
		// Major actions appear as buttons immediately visible as page actions.
2221
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset');
2222
2223
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2224
		$rootTabSet = new TabSet('ActionMenus');
2225
		$moreOptions = new Tab(
2226
			'MoreOptions',
2227
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2228
		);
2229
		$rootTabSet->push($moreOptions);
2230
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus');
2231
2232
		// Render page information into the "more-options" drop-up, on the top.
2233
		$live = Versioned::get_one_by_stage('SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2234
			'"SiteTree"."ID"' => $this->ID
2235
		));
2236
		$moreOptions->push(
2237
			new LiteralField('Information',
2238
				$this->customise(array(
2239
					'Live' => $live,
2240
					'ExistsOnLive' => $existsOnLive
2241
				))->renderWith('SiteTree_Information')
2242
			)
2243
		);
2244
2245
		// "readonly"/viewing version that isn't the current version of the record
2246
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::current_stage(), array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2247
			'"SiteTree"."ID"' => $this->ID
2248
		));
2249
		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...
2250
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2251
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2252
2253
			$actions = new FieldList(array($majorActions, $rootTabSet));
2254
2255
			// getCMSActions() can be extended with updateCMSActions() on a extension
2256
			$this->extend('updateCMSActions', $actions);
2257
2258
			return $actions;
2259
		}
2260
2261 View Code Duplication
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canDeleteFromLive()) {
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...
2262
			// "unpublish"
2263
			$moreOptions->push(
2264
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2265
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2266
					->addExtraClass('ss-ui-action-destructive')
2267
			);
2268
		}
2269
2270 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...
2271
			if($this->isPublished() && $this->canEdit())	{
2272
				// "rollback"
2273
				$moreOptions->push(
2274
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2275
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2276
				);
2277
			}
2278
		}
2279
2280
		if($this->canEdit()) {
2281
			if($this->getIsDeletedFromStage()) {
2282
				// The usual major actions are not available, so we provide alternatives here.
2283
				if($existsOnLive) {
2284
					// "restore"
2285
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2286
					if($this->canDelete() && $this->canDeleteFromLive()) {
2287
						// "delete from live"
2288
						$majorActions->push(
2289
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2290
								->addExtraClass('ss-ui-action-destructive')
2291
						);
2292
					}
2293
				} else {
2294
					// Determine if we should force a restore to root (where once it was a subpage)
2295
					$restoreToRoot = $this->isParentArchived();
2296
					
2297
					// "restore"
2298
					$title = $restoreToRoot
2299
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2300
						: _t('CMSMain.RESTORE','Restore draft');
2301
					$description = $restoreToRoot
2302
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2303
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2304
					$majorActions->push(
2305
						FormAction::create('restore', $title)
2306
							->setDescription($description)
2307
							->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...
2308
							->setAttribute('data-icon', 'decline')
2309
					);
2310
				}
2311
			} else {
2312
				// Detect use of legacy actions
2313
				// {@see CMSMain::enabled_legacy_actions}
2314
				$legacy = CMSMain::config()->enabled_legacy_actions;
2315
				if(in_array('CMSBatchAction_Delete', $legacy)) {
2316
					Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
2317
					if($this->canDelete()) {
2318
						// delete
2319
						$moreOptions->push(
2320
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2321
								->addExtraClass('delete ss-ui-action-destructive')
2322
						);
2323
					}
2324
				} elseif($this->canArchive()) {
2325
					// "archive"
2326
					$moreOptions->push(
2327
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2328
							->setDescription(_t(
2329
								'SiteTree.BUTTONARCHIVEDESC',
2330
								'Unpublish and send to archive'
2331
							))
2332
							->addExtraClass('delete ss-ui-action-destructive')
2333
					);
2334
				}
2335
			
2336
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2337
				$majorActions->push(
2338
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2339
						->setAttribute('data-icon', 'accept')
2340
						->setAttribute('data-icon-alternate', 'addpage')
2341
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2342
				);
2343
			}
2344
		}
2345
2346
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
2347
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2348
			$majorActions->push(
2349
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2350
					->setAttribute('data-icon', 'accept')
2351
					->setAttribute('data-icon-alternate', 'disk')
2352
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2353
			);
2354
2355
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2356
			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...
2357
				$publish->addExtraClass('ss-ui-alternate');
2358
			}
2359
		}
2360
		
2361
		$actions = new FieldList(array($majorActions, $rootTabSet));
2362
		
2363
		// Hook for extensions to add/remove actions.
2364
		$this->extend('updateCMSActions', $actions);
2365
		
2366
		return $actions;
2367
	}
2368
	
2369
	/**
2370
	 * Publish this page.
2371
	 *
2372
	 * @uses SiteTreeExtension->onBeforePublish()
2373
	 * @uses SiteTreeExtension->onAfterPublish()
2374
	 * @return bool True if published
2375
	 */
2376
	public function doPublish() {
2377
		if (!$this->canPublish()) return false;
2378
		
2379
		$original = Versioned::get_one_by_stage("SiteTree", "Live", array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2380
			'"SiteTree"."ID"' => $this->ID
2381
		));
2382
		if(!$original) $original = new SiteTree();
2383
2384
		// Handle activities undertaken by extensions
2385
		$this->invokeWithExtensions('onBeforePublish', $original);
2386
		//$this->PublishedByID = Member::currentUser()->ID;
2387
		$this->write();
2388
		$this->publish("Stage", "Live");
0 ignored issues
show
Bug introduced by
The method publish() does not exist on SiteTree. Did you maybe mean canPublish()?

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

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...
2471
2472
		// Use a clone to get the updates made by $this->publish
2473
		$clone = DataObject::get_by_id("SiteTree", $this->ID);
2474
		$clone->writeWithoutVersion();
2475
2476
		// Need to update pages linking to this one as no longer broken
2477
		foreach($this->DependentPages(false) as $page) {
2478
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2479
			$page->write();
2480
		}
2481
		
2482
		$this->invokeWithExtensions('onAfterRevertToLive', $this);
2483
		return true;
2484
	}
2485
2486
	/**
2487
	 * Determine if this page references a parent which is archived, and not available in stage
2488
	 *
2489
	 * @return bool True if there is an archived parent
2490
	 */
2491
	protected function isParentArchived() {
2492
		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...
2493
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2494
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2495
				return true;
2496
			}
2497
		}
2498
		return false;
2499
	}
2500
	
2501
	/**
2502
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2503
	 *
2504
	 * @return self
2505
	 */
2506
	public function doRestoreToStage() {
2507
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2508
2509
		// Ensure that the parent page is restored, otherwise restore to root
2510
		if($this->isParentArchived()) {
2511
			$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...
2512
		}
2513
		
2514
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2515
		// create an empty record
2516
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2517
			$conn = DB::get_conn();
2518
			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...
2519
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2520
			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...
2521
		}
2522
		
2523
		$oldStage = Versioned::current_stage();
2524
		Versioned::reading_stage('Stage');
2525
		$this->forceChange();
2526
		$this->write();
2527
		
2528
		$result = DataObject::get_by_id($this->class, $this->ID);
2529
2530
		// Need to update pages linking to this one as no longer broken
2531
		foreach($result->DependentPages(false) as $page) {
2532
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2533
			$page->write();
2534
		}
2535
		
2536
		Versioned::reading_stage($oldStage);
2537
2538
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2539
		
2540
		return $result;
2541
	}
2542
2543
	/**
2544
	 * Removes the page from both live and stage
2545
	 *
2546
	 * @return bool Success
2547
	 */
2548
	public function doArchive() {
2549
		$this->invokeWithExtensions('onBeforeArchive', $this);
2550
2551
		if($this->doUnpublish()) {
2552
			$this->delete();
2553
			$this->invokeWithExtensions('onAfterArchive', $this);
2554
2555
			return true;
2556
		}
2557
2558
		return false;
2559
	}
2560
2561
	/**
2562
	 * Check if the current user is allowed to archive this page.
2563
	 * If extended, ensure that both canDelete and canDeleteFromLive are extended also
2564
	 *
2565
	 * @param Member $member
2566
	 * @return bool
2567
	 */
2568
	public function canArchive($member = null) {
2569
		if(!$member) {
2570
            $member = Member::currentUser();
2571
        }
2572
		
2573
		// Standard mechanism for accepting permission changes from extensions
2574
		$extended = $this->extendedCan('canArchive', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

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...
2575
		if($extended !== null) {
2576
            return $extended;
2577
        }
2578
2579
		// Check if this page can be deleted
2580
        if(!$this->canDelete($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 2570 can also be of type object<DataObject>; however, SiteTree::canDelete() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2581
            return false;
2582
        }
2583
2584
        // If published, check if we can delete from live
2585
        if($this->ExistsOnLive && !$this->canDeleteFromLive($member)) {
0 ignored issues
show
Documentation introduced by
The property ExistsOnLive 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...
2586
            return false;
2587
        }
2588
2589
		return true;
2590
	}
2591
2592
	/**
2593
	 * Synonym of {@link doUnpublish}
2594
	 */
2595
	public function doDeleteFromLive() {
2596
		return $this->doUnpublish();
2597
	}
2598
2599
	/**
2600
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2601
	 *
2602
	 * @return bool
2603
	 */
2604
	public function isNew() {
2605
		/**
2606
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2607
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2608
		 */
2609
		if(empty($this->ID)) return true;
2610
2611
		if(is_numeric($this->ID)) return false;
2612
2613
		return stripos($this->ID, 'new') === 0;
2614
	}
2615
2616
2617
	/**
2618
	 * Check if this page has been published.
2619
	 *
2620
	 * @return bool
2621
	 */
2622
	public function isPublished() {
2623
		if($this->isNew())
2624
			return false;
2625
2626
		return (DB::prepared_query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($this->ID))->value())
2627
			? true
2628
			: false;
2629
	}
2630
2631
	/**
2632
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2633
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2634
	 * {@link SiteTree::$needs_permission}.
2635
	 *
2636
	 * @return array
2637
	 */
2638
	protected function getClassDropdown() {
2639
		$classes = self::page_type_classes();
2640
		$currentClass = null;
2641
		$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...
2642
		
2643
		$result = array();
2644
		foreach($classes as $class) {
2645
			$instance = singleton($class);
2646
2647
			// if the current page type is this the same as the class type always show the page type in the list
2648
			if ($this->ClassName != $instance->ClassName) {
2649
				if((($instance instanceof HiddenClass) || !$instance->canCreate())) continue;
2650
			}
2651
			
2652
			if($perms = $instance->stat('need_permission')) {
2653
				if(!$this->can($perms)) continue;
2654
			}
2655
2656
			$pageTypeName = $instance->i18n_singular_name();
2657
2658
			$currentClass = $class;
2659
			$result[$class] = $pageTypeName;
2660
2661
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2662
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2663
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2664
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2665
				$result[$class] = $result[$class] .  " ({$class})";
2666
			}
2667
		}
2668
		
2669
		// sort alphabetically, and put current on top
2670
		asort($result);
2671
		if($currentClass) {
2672
			$currentPageTypeName = $result[$currentClass];
2673
			unset($result[$currentClass]);
2674
			$result = array_reverse($result);
2675
			$result[$currentClass] = $currentPageTypeName;
2676
			$result = array_reverse($result);
2677
		}
2678
		
2679
		return $result;
2680
	}
2681
2682
	/**
2683
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2684
	 *
2685
	 * @return string[]
2686
	 */
2687
	public function allowedChildren() {
2688
		$allowedChildren = array();
2689
		$candidates = $this->stat('allowed_children');
2690
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2691
			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...
2692
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2693
				// Otherwise, the class and all its subclasses are allowed.
2694
				if(substr($candidate,0,1) == '*') {
2695
					$allowedChildren[] = substr($candidate,1);
2696
				} else {
2697
					$subclasses = ClassInfo::subclassesFor($candidate);
2698
					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...
2699
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2700
					}
2701
				}
2702
			}
2703
		}
2704
		
2705
		return $allowedChildren;
2706
	}
2707
2708
	/**
2709
	 * Returns the class name of the default class for children of this page.
2710
	 *
2711
	 * @return string
2712
	 */
2713
	public function defaultChild() {
2714
		$default = $this->stat('default_child');
2715
		$allowed = $this->allowedChildren();
2716
		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...
2717
			if(!$default || !in_array($default, $allowed))
2718
				$default = reset($allowed);
2719
			return $default;
2720
		}
2721
	}
2722
2723
	/**
2724
	 * Returns the class name of the default class for the parent of this page.
2725
	 *
2726
	 * @return string
2727
	 */
2728
	public function defaultParent() {
2729
		return $this->stat('default_parent');
2730
	}
2731
2732
	/**
2733
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2734
	 * Title field.
2735
	 *
2736
	 * @return string
2737
	 */
2738
	public function getMenuTitle(){
2739
		if($value = $this->getField("MenuTitle")) {
2740
			return $value;
2741
		} else {
2742
			return $this->getField("Title");
2743
		}
2744
	}
2745
2746
2747
	/**
2748
	 * Set the menu title for this page.
2749
	 *
2750
	 * @param string $value
2751
	 */
2752
	public function setMenuTitle($value) {
2753
		if($value == $this->getField("Title")) {
2754
			$this->setField("MenuTitle", null);
2755
		} else {
2756
			$this->setField("MenuTitle", $value);
2757
		}
2758
	}
2759
	
2760
	/**
2761
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2762
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2763
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2764
	 * the flags.
2765
	 *
2766
	 * Example (simple):
2767
	 *   "deletedonlive" => "Deleted"
2768
	 *
2769
	 * Example (with optional title attribute):
2770
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2771
	 *
2772
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2773
	 * @return array
2774
	 */
2775
	public function getStatusFlags($cached = true) {
2776
		if(!$this->_cache_statusFlags || !$cached) {
2777
			$flags = array();
2778
			if($this->getIsDeletedFromStage()) {
2779
				if($this->getExistsOnLive()) {
2780
					$flags['removedfromdraft'] = array(
2781
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2782
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2783
					);
2784
				} else {
2785
					$flags['archived'] = array(
2786
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2787
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2788
					);
2789
				}
2790
			} else if($this->getIsAddedToStage()) {
2791
				$flags['addedtodraft'] = array(
2792
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2793
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2794
				);
2795
			} else if($this->getIsModifiedOnStage()) {
2796
				$flags['modified'] = array(
2797
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2798
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2799
				);
2800
			}
2801
2802
			$this->extend('updateStatusFlags', $flags);
2803
2804
			$this->_cache_statusFlags = $flags;
2805
		}
2806
		
2807
		return $this->_cache_statusFlags;
2808
	}
2809
2810
	/**
2811
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2812
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2813
	 * publication status.
2814
	 *
2815
	 * @return string An HTML string ready to be directly used in a template
2816
	 */
2817
	public function getTreeTitle() {
2818
		// Build the list of candidate children
2819
		$children = array();
2820
		$candidates = static::page_type_classes();
2821
		foreach($this->allowedChildren() as $childClass) {
2822
			if(!in_array($childClass, $candidates)) continue;
2823
			$child = singleton($childClass);
2824
			if($child->canCreate(null, array('Parent' => $this))) {
2825
				$children[$childClass] = $child->i18n_singular_name();
2826
			}
2827
		}
2828
		$flags = $this->getStatusFlags();
2829
		$treeTitle = sprintf(
2830
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2831
			Convert::raw2att(Convert::raw2json($children)),
2832
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2833
		);
2834
		foreach($flags as $class => $data) {
2835
			if(is_string($data)) $data = array('text' => $data);
2836
			$treeTitle .= sprintf(
2837
				"<span class=\"badge %s\"%s>%s</span>",
2838
				'status-' . Convert::raw2xml($class),
2839
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2840
				Convert::raw2xml($data['text'])
2841
			);
2842
		}
2843
		
2844
		return $treeTitle;
2845
	}
2846
2847
	/**
2848
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2849
	 * we're currently inside, etc.
2850
	 *
2851
	 * @param int $level
2852
	 * @return SiteTree
2853
	 */
2854
	public function Level($level) {
2855
		$parent = $this;
2856
		$stack = array($parent);
2857
		while($parent = $parent->Parent) {
2858
			array_unshift($stack, $parent);
2859
		}
2860
2861
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2862
	}
2863
2864
	/**
2865
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2866
	 *
2867
	 * @return int
2868
	 */
2869
	public function getPageLevel() {
2870
		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...
2871
			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...
2872
		}
2873
		return 1;
2874
	}
2875
2876
	/**
2877
	 * Return the CSS classes to apply to this node in the CMS tree.
2878
	 *
2879
	 * @param string $numChildrenMethod
2880
	 * @return string
2881
	 */
2882
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2883
		$classes = sprintf('class-%s', $this->class);
2884
		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...
2885
			$classes .= " BrokenLink";
2886
		}
2887
2888
		if(!$this->canAddChildren()) {
2889
			$classes .= " nochildren";
2890
		}
2891
2892
		if(!$this->canEdit() && !$this->canAddChildren()) {
2893
			if (!$this->canView()) {
2894
				$classes .= " disabled";
2895
			} else {
2896
				$classes .= " edit-disabled";
2897
			}
2898
		}
2899
2900
		if(!$this->ShowInMenus) {
2901
			$classes .= " notinmenu";
2902
		}
2903
			
2904
		//TODO: Add integration
2905
		/*
2906
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2907
			$classes .= " untranslated ";
2908
		*/
2909
		$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...
2910
2911
		return $classes;
2912
	}
2913
	
2914
	/**
2915
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2916
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2917
	 *
2918
	 * @return bool
2919
	 */
2920
	public function getIsDeletedFromStage() {
2921
		if(!$this->ID) return true;
2922
		if($this->isNew()) return false;
2923
		
2924
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2925
2926
		// Return true for both completely deleted pages and for pages just deleted from stage
2927
		return !($stageVersion);
2928
	}
2929
	
2930
	/**
2931
	 * Return true if this page exists on the live site
2932
	 *
2933
	 * @return bool
2934
	 */
2935
	public function getExistsOnLive() {
2936
		return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2937
	}
2938
2939
	/**
2940
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2941
	 * unpublished changes to the draft site.
2942
	 *
2943
	 * @return bool
2944
	 */
2945
	public function getIsModifiedOnStage() {
2946
		// New unsaved pages could be never be published
2947
		if($this->isNew()) return false;
2948
		
2949
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2950
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2951
		
2952
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2953
		$this->extend('getIsModifiedOnStage', $isModified);
2954
		
2955
		return $isModified;
2956
	}
2957
	
2958
	/**
2959
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2960
	 * published.
2961
	 *
2962
	 * @return bool
2963
	 */
2964
	public function getIsAddedToStage() {
2965
		// New unsaved pages could be never be published
2966
		if($this->isNew()) return false;
2967
		
2968
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2969
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2970
2971
		return ($stageVersion && !$liveVersion);
2972
	}
2973
	
2974
	/**
2975
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2976
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2977
	 */
2978
	static public function disableCMSFieldsExtensions() {
2979
		self::$runCMSFieldsExtensions = false;
2980
	}
2981
	
2982
	/**
2983
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2984
	 * disableCMSFieldsExtensions().
2985
	 */
2986
	static public function enableCMSFieldsExtensions() {
2987
		self::$runCMSFieldsExtensions = true;
2988
	}
2989
2990
	public function providePermissions() {
2991
		return array(
2992
			'SITETREE_GRANT_ACCESS' => array(
2993
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2994
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2995
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2996
				'sort' => 100
2997
			),
2998
			'SITETREE_VIEW_ALL' => array(
2999
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
3000
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3001
				'sort' => -100,
3002
				'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')
3003
			),
3004
			'SITETREE_EDIT_ALL' => array(
3005
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
3006
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3007
				'sort' => -50,
3008
				'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')
3009
			),
3010
			'SITETREE_REORGANISE' => array(
3011
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
3012
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3013
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
3014
				'sort' => 100
3015
			),
3016
			'VIEW_DRAFT_CONTENT' => array(
3017
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
3018
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3019
				'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.'),
3020
				'sort' => 100
3021
			)
3022
		);
3023
	}
3024
	
3025
	/**
3026
	 * Return the translated Singular name.
3027
	 *
3028
	 * @return string
3029
	 */
3030
	public function i18n_singular_name() {
3031
		// Convert 'Page' to 'SiteTree' for correct localization lookups
3032
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
3033
		return _t($class.'.SINGULARNAME', $this->singular_name());
3034
	}
3035
	
3036
	/**
3037
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
3038
	 * picks it up for the wrong folder.
3039
	 *
3040
	 * @return array
3041
	 */
3042
	public function provideI18nEntities() {
3043
		$entities = parent::provideI18nEntities();
3044
		
3045
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
3046
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;		
3047
3048
		$entities[$this->class . '.DESCRIPTION'] = array(
3049
			$this->stat('description'),
3050
			'Description of the page type (shown in the "add page" dialog)'
3051
		);
3052
3053
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
3054
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
3055
3056
		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...
3057
	}
3058
3059
	/**
3060
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
3061
	 *
3062
	 * @return string
3063
	 */
3064
	public function getParentType() {
3065
		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...
3066
	}
3067
3068
	/**
3069
	 * Clear the permissions cache for SiteTree
3070
	 */
3071
	public static function reset() {
3072
		self::$cache_permissions = array();
3073
	}
3074
	
3075
	static public function on_db_reset() {
3076
		self::$cache_permissions = array();
3077
	}
3078
3079
}
3080