Completed
Push — master ( 9f01cc...540a80 )
by Damian
10s
created

SiteTree::validURLSegment()   C

Complexity

Conditions 14
Paths 76

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 5.0864
c 0
b 0
f 0
cc 14
eloc 17
nc 76
nop 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\Admin\AddToCampaignHandler_FormAction;
7
use SilverStripe\Admin\CMSPreviewable;
8
use SilverStripe\CMS\Controllers\CMSPageEditController;
9
use SilverStripe\CMS\Controllers\ContentController;
10
use SilverStripe\CMS\Controllers\ModelAsController;
11
use SilverStripe\CMS\Controllers\RootURLController;
12
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
13
use SilverStripe\Control\Controller;
14
use SilverStripe\Control\Director;
15
use SilverStripe\Core\ClassInfo;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Convert;
18
use SilverStripe\Dev\Deprecation;
19
use SilverStripe\Forms\CheckboxField;
20
use SilverStripe\Forms\CompositeField;
21
use SilverStripe\Forms\DropdownField;
22
use SilverStripe\Forms\FieldGroup;
23
use SilverStripe\Forms\FieldList;
24
use SilverStripe\Forms\FormAction;
25
use SilverStripe\Forms\FormField;
26
use SilverStripe\Forms\GridField\GridField;
27
use SilverStripe\Forms\GridField\GridFieldDataColumns;
28
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
29
use SilverStripe\Forms\ListboxField;
30
use SilverStripe\Forms\LiteralField;
31
use SilverStripe\Forms\OptionsetField;
32
use SilverStripe\Forms\Tab;
33
use SilverStripe\Forms\TabSet;
34
use SilverStripe\Forms\TextareaField;
35
use SilverStripe\Forms\TextField;
36
use SilverStripe\Forms\ToggleCompositeField;
37
use SilverStripe\Forms\TreeDropdownField;
38
use SilverStripe\i18n\i18n;
39
use SilverStripe\i18n\i18nEntityProvider;
40
use SilverStripe\ORM\ArrayList;
41
use SilverStripe\ORM\DataList;
42
use SilverStripe\ORM\DataObject;
43
use SilverStripe\ORM\DB;
44
use SilverStripe\ORM\HiddenClass;
45
use SilverStripe\ORM\Hierarchy\Hierarchy;
46
use SilverStripe\ORM\ManyManyList;
47
use SilverStripe\ORM\ValidationResult;
48
use SilverStripe\ORM\Versioning\Versioned;
49
use SilverStripe\Security\Group;
50
use SilverStripe\Security\Member;
51
use SilverStripe\Security\Permission;
52
use SilverStripe\Security\PermissionProvider;
53
use SilverStripe\SiteConfig\SiteConfig;
54
use SilverStripe\View\ArrayData;
55
use SilverStripe\View\Parsers\ShortcodeParser;
56
use SilverStripe\View\Parsers\URLSegmentFilter;
57
use SilverStripe\View\SSViewer;
58
use Subsite;
59
60
/**
61
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
62
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
63
 * draft and published states.
64
 *
65
 * <h2>URLs</h2>
66
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
67
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
68
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
69
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
70
 * {@link URLSegmentFilter::$default_allow_multibyte}.
71
 *
72
 * @property string URLSegment
73
 * @property string Title
74
 * @property string MenuTitle
75
 * @property string Content HTML content of the page.
76
 * @property string MetaDescription
77
 * @property string ExtraMeta
78
 * @property string ShowInMenus
79
 * @property string ShowInSearch
80
 * @property string Sort Integer value denoting the sort order.
81
 * @property string ReportClass
82
 * @property string CanViewType Type of restriction for viewing this object.
83
 * @property string CanEditType Type of restriction for editing this object.
84
 *
85
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
86
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
87
 * @method SiteTree Parent()
88
 *
89
 * @mixin Hierarchy
90
 * @mixin Versioned
91
 * @mixin SiteTreeLinkTracking
92
 */
93
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
94
95
	/**
96
	 * Indicates what kind of children this page type can have.
97
	 * This can be an array of allowed child classes, or the string "none" -
98
	 * indicating that this page type can't have children.
99
	 * If a classname is prefixed by "*", such as "*Page", then only that
100
	 * class is allowed - no subclasses. Otherwise, the class and all its
101
	 * subclasses are allowed.
102
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
103
	 *
104
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
105
	 *
106
	 * @config
107
	 * @var array
108
	 */
109
	private static $allowed_children = array("SilverStripe\\CMS\\Model\\SiteTree");
110
111
	/**
112
	 * The default child class for this page.
113
	 * Note: Value might be cached, see {@link $allowed_chilren}.
114
	 *
115
	 * @config
116
	 * @var string
117
	 */
118
	private static $default_child = "Page";
119
120
	/**
121
	 * Default value for SiteTree.ClassName enum
122
	 * {@see DBClassName::getDefault}
123
	 *
124
	 * @config
125
	 * @var string
126
	 */
127
	private static $default_classname = "Page";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
128
129
	/**
130
	 * The default parent class for this page.
131
	 * Note: Value might be cached, see {@link $allowed_chilren}.
132
	 *
133
	 * @config
134
	 * @var string
135
	 */
136
	private static $default_parent = null;
137
138
	/**
139
	 * Controls whether a page can be in the root of the site tree.
140
	 * Note: Value might be cached, see {@link $allowed_chilren}.
141
	 *
142
	 * @config
143
	 * @var bool
144
	 */
145
	private static $can_be_root = true;
146
147
	/**
148
	 * List of permission codes a user can have to allow a user to create a page of this type.
149
	 * Note: Value might be cached, see {@link $allowed_chilren}.
150
	 *
151
	 * @config
152
	 * @var array
153
	 */
154
	private static $need_permission = null;
155
156
	/**
157
	 * If you extend a class, and don't want to be able to select the old class
158
	 * in the cms, set this to the old class name. Eg, if you extended Product
159
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
160
	 *
161
	 * @config
162
	 * @var string
163
	 */
164
	private static $hide_ancestor = null;
165
166
	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...
167
		"URLSegment" => "Varchar(255)",
168
		"Title" => "Varchar(255)",
169
		"MenuTitle" => "Varchar(100)",
170
		"Content" => "HTMLText",
171
		"MetaDescription" => "Text",
172
		"ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
173
		"ShowInMenus" => "Boolean",
174
		"ShowInSearch" => "Boolean",
175
		"Sort" => "Int",
176
		"HasBrokenFile" => "Boolean",
177
		"HasBrokenLink" => "Boolean",
178
		"ReportClass" => "Varchar",
179
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
180
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
181
	);
182
183
	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...
184
		"URLSegment" => true,
185
	);
186
187
	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...
188
		"ViewerGroups" => "SilverStripe\\Security\\Group",
189
		"EditorGroups" => "SilverStripe\\Security\\Group",
190
	);
191
192
	private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
193
		"VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
194
	);
195
196
	private static $owned_by = array(
197
		"VirtualPages"
198
	);
199
200
	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...
201
		"Breadcrumbs" => "HTMLFragment",
202
		"LastEdited" => "Datetime",
203
		"Created" => "Datetime",
204
		'Link' => 'Text',
205
		'RelativeLink' => 'Text',
206
		'AbsoluteLink' => 'Text',
207
		'CMSEditLink' => 'Text',
208
		'TreeTitle' => 'HTMLFragment',
209
		'MetaTags' => 'HTMLFragment',
210
	);
211
212
	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...
213
		"ShowInMenus" => 1,
214
		"ShowInSearch" => 1,
215
		"CanViewType" => "Inherit",
216
		"CanEditType" => "Inherit"
217
	);
218
219
	private static $table_name = 'SiteTree';
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...
220
221
	private static $versioning = array(
222
		"Stage",  "Live"
223
	);
224
225
	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...
226
227
	/**
228
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
229
	 * @var boolean
230
	 * @config
231
	 */
232
	private static $can_create = true;
233
234
	/**
235
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
236
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
237
	 *
238
	 * @see CMSMain::generateTreeStylingCSS()
239
	 * @config
240
	 * @var string
241
	 */
242
	private static $icon = null;
243
244
	/**
245
	 * @config
246
	 * @var string Description of the class functionality, typically shown to a user
247
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
248
	 */
249
	private static $description = 'Generic content page';
250
251
	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...
252
		'SilverStripe\\ORM\\Hierarchy\\Hierarchy',
253
		'SilverStripe\\ORM\\Versioning\\Versioned',
254
		"SilverStripe\\CMS\\Model\\SiteTreeLinkTracking"
255
	);
256
257
	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...
258
		'Title',
259
		'Content',
260
	);
261
262
	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...
263
		'URLSegment' => 'URL'
264
	);
265
266
	/**
267
	 * @config
268
	 */
269
	private static $nested_urls = true;
270
271
	/**
272
	 * @config
273
	*/
274
	private static $create_default_pages = true;
275
276
	/**
277
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
278
	 */
279
	private static $runCMSFieldsExtensions = true;
280
281
	/**
282
	 * Cache for canView/Edit/Publish/Delete permissions.
283
	 * Keyed by permission type (e.g. 'edit'), with an array
284
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
285
	 * See {@link batch_permission_check()} for details.
286
	 */
287
	private static $cache_permissions = array();
288
289
	/**
290
	 * @config
291
	 * @var boolean
292
	 */
293
	private static $enforce_strict_hierarchy = true;
294
295
	/**
296
	 * The value used for the meta generator tag. Leave blank to omit the tag.
297
	 *
298
	 * @config
299
	 * @var string
300
	 */
301
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
302
303
	protected $_cache_statusFlags = null;
304
305
	/**
306
	 * Fetches the {@link SiteTree} object that maps to a link.
307
	 *
308
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
309
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
310
	 *
311
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
312
	 * by a extension attached to {@link SiteTree}
313
	 *
314
	 * @param string $link  The link of the page to search for
315
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
316
	 * @return SiteTree
317
	 */
318
	static public function get_by_link($link, $cache = true) {
319
		if(trim($link, '/')) {
320
			$link = trim(Director::makeRelative($link), '/');
321
		} else {
322
			$link = RootURLController::get_homepage_link();
323
		}
324
325
		$parts = preg_split('|/+|', $link);
326
327
		// Grab the initial root level page to traverse down from.
328
		$URLSegment = array_shift($parts);
329
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
330
		if(self::config()->nested_urls) {
331
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
332
		}
333
		/** @var SiteTree $sitetree */
334
		$sitetree = DataObject::get_one(self::class, $conditions, $cache);
335
336
		/// Fall back on a unique URLSegment for b/c.
337
		if(	!$sitetree
338
			&& self::config()->nested_urls
339
			&& $sitetree = DataObject::get_one(self::class, array(
340
				'"SiteTree"."URLSegment"' => $URLSegment
341
			), $cache)
342
		) {
343
			return $sitetree;
344
		}
345
346
		// Attempt to grab an alternative page from extensions.
347
		if(!$sitetree) {
348
			$parentID = self::config()->nested_urls ? 0 : null;
349
350 View Code Duplication
			if($alternatives = static::singleton()->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...
351
				foreach($alternatives as $alternative) {
352
					if($alternative) {
353
						$sitetree = $alternative;
354
			}
355
				}
356
			}
357
358
			if(!$sitetree) {
359
				return null;
360
		}
361
		}
362
363
		// Check if we have any more URL parts to parse.
364
		if(!self::config()->nested_urls || !count($parts)) {
365
			return $sitetree;
366
		}
367
368
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
369
		foreach($parts as $segment) {
370
			$next = DataObject::get_one(self::class, array(
371
					'"SiteTree"."URLSegment"' => $segment,
372
					'"SiteTree"."ParentID"' => $sitetree->ID
373
				),
374
				$cache
375
			);
376
377
			if(!$next) {
378
				$parentID = (int) $sitetree->ID;
379
380 View Code Duplication
				if($alternatives = static::singleton()->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...
381
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
382
				}
383
384
				if(!$next) {
385
					return null;
386
				}
387
			}
388
389
			$sitetree->destroy();
390
			$sitetree = $next;
391
		}
392
393
		return $sitetree;
394
	}
395
396
	/**
397
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
398
	 *
399
	 * @return array
400
	 */
401
	public static function page_type_classes() {
402
		$classes = ClassInfo::getValidSubClasses();
403
404
		$baseClassIndex = array_search(self::class, $classes);
405
		if($baseClassIndex !== false) {
406
			unset($classes[$baseClassIndex]);
407
		}
408
409
		$kill_ancestors = array();
410
411
		// figure out if there are any classes we don't want to appear
412
		foreach($classes as $class) {
413
			$instance = singleton($class);
414
415
			// do any of the progeny want to hide an ancestor?
416
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
417
				// note for killing later
418
				$kill_ancestors[] = $ancestor_to_hide;
419
			}
420
		}
421
422
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
423
		// requirements
424
		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...
425
			$kill_ancestors = array_unique($kill_ancestors);
426
			foreach($kill_ancestors as $mark) {
427
				// unset from $classes
428
				$idx = array_search($mark, $classes, true);
429
				if ($idx !== false) {
430
					unset($classes[$idx]);
431
				}
432
			}
433
		}
434
435
		return $classes;
436
	}
437
438
	/**
439
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
440
	 *
441
	 * @param array      $arguments
442
	 * @param string     $content
443
	 * @param ShortcodeParser $parser
444
	 * @return string
445
	 */
446
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
447
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) {
448
			return null;
449
		}
450
451
		/** @var SiteTree $page */
452
		if (
453
			   !($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
454
			&& !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
455
		) {
456
			 return null; // There were no suitable matches at all.
457
		}
458
459
		/** @var SiteTree $page */
460
		$link = Convert::raw2att($page->Link());
461
462
		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...
463
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
0 ignored issues
show
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...
464
		} else {
465
			return $link;
466
		}
467
	}
468
469
	/**
470
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
471
	 *
472
	 * @param string $action Optional controller action (method).
473
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
474
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
475
	 *                       append GET parameters.
476
	 * @return string
477
	 */
478
	public function Link($action = null) {
479
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
480
	}
481
482
	/**
483
	 * Get the absolute URL for this page, including protocol and host.
484
	 *
485
	 * @param string $action See {@link Link()}
486
	 * @return string
487
	 */
488
	public function AbsoluteLink($action = null) {
489
		if($this->hasMethod('alternateAbsoluteLink')) {
490
			return $this->alternateAbsoluteLink($action);
0 ignored issues
show
Bug introduced by
The method alternateAbsoluteLink() does not exist on SilverStripe\CMS\Model\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...
491
		} else {
492
			return Director::absoluteURL($this->Link($action));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \SilverStripe\Control\Di...($this->Link($action)); of type string|false adds false to the return on line 492 which is incompatible with the return type documented by SilverStripe\CMS\Model\SiteTree::AbsoluteLink of type string. It seems like you forgot to handle an error condition.
Loading history...
493
		}
494
	}
495
496
	/**
497
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
498
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
499
	 *
500
	 * @param string $action See {@link Link()}
501
	 * @return string
502
	 */
503
	public function PreviewLink($action = null) {
504
		if($this->hasMethod('alternatePreviewLink')) {
505
			Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
506
			return $this->alternatePreviewLink($action);
0 ignored issues
show
Bug introduced by
The method alternatePreviewLink() does not exist on SilverStripe\CMS\Model\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...
507
		}
508
509
		$link = $this->AbsoluteLink($action);
510
		$this->extend('updatePreviewLink', $link, $action);
511
		return $link;
512
	}
513
514
	public function getMimeType() {
515
		return 'text/html';
516
	}
517
518
	/**
519
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
520
	 *
521
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
522
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
523
	 * and returned in its full form.
524
	 *
525
	 * @uses RootURLController::get_homepage_link()
526
	 *
527
	 * @param string $action See {@link Link()}
528
	 * @return string
529
	 */
530
	public function RelativeLink($action = null) {
531
		if($this->ParentID && self::config()->nested_urls) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
532
			$parent = $this->Parent();
533
			// If page is removed select parent from version history (for archive page view)
534
			if((!$parent || !$parent->exists()) && !$this->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\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...
535
				$parent = Versioned::get_latest_version(self::class, $this->ParentID);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
536
			}
537
			$base = $parent->RelativeLink($this->URLSegment);
538
		} 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...
539
			// Unset base for root-level homepages.
540
			// Note: Homepages with action parameters (or $action === true)
541
			// need to retain their URLSegment.
542
			$base = null;
543
		} else {
544
			$base = $this->URLSegment;
545
		}
546
547
		$this->extend('updateRelativeLink', $base, $action);
548
549
		// Legacy support: If $action === true, retain URLSegment for homepages,
550
		// but don't append any action
551
		if($action === true) $action = null;
552
553
		return Controller::join_links($base, '/', $action);
554
	}
555
556
	/**
557
	 * Get the absolute URL for this page on the Live site.
558
	 *
559
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
560
	 * @return string
561
	 */
562
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
563
		$oldReadingMode = Versioned::get_reading_mode();
564
		Versioned::set_stage(Versioned::LIVE);
565
		/** @var SiteTree $live */
566
		$live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|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...
567
			'"SiteTree"."ID"' => $this->ID
568
		));
569
		if($live) {
570
			$link = $live->AbsoluteLink();
571
			if($includeStageEqualsLive) {
572
				$link = Controller::join_links($link, '?stage=Live');
573
			}
574
		} else {
575
			$link = null;
576
		}
577
578
		Versioned::set_reading_mode($oldReadingMode);
579
		return $link;
580
	}
581
582
	/**
583
	 * Generates a link to edit this page in the CMS.
584
	 *
585
	 * @return string
586
	 */
587
	public function CMSEditLink() {
588
		$link = Controller::join_links(
589
			CMSPageEditController::singleton()->Link('show'),
590
			$this->ID
591
		);
592
		return Director::absoluteURL($link);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \SilverStripe\Control\Di...or::absoluteURL($link); of type string|false adds false to the return on line 592 which is incompatible with the return type declared by the interface SilverStripe\Admin\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
593
	}
594
595
596
	/**
597
	 * Return a CSS identifier generated from this page's link.
598
	 *
599
	 * @return string The URL segment
600
	 */
601
	public function ElementName() {
602
		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...
603
	}
604
605
	/**
606
	 * Returns true if this is the currently active page being used to handle this request.
607
	 *
608
	 * @return bool
609
	 */
610
	public function isCurrent() {
611
		$currentPage = Director::get_current_page();
612
		if ($currentPage instanceof ContentController) {
613
			$currentPage = $currentPage->data();
614
	}
615
		if($currentPage instanceof SiteTree) {
616
			return $currentPage === $this || $currentPage->ID === $this->ID;
617
		}
618
		return false;
619
	}
620
621
	/**
622
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
623
	 * currently being viewed).
624
	 *
625
	 * @return bool
626
	 */
627
	public function isSection() {
628
		return $this->isCurrent() || (
629
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
630
		);
631
	}
632
633
	/**
634
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
635
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
636
	 * to external users.
637
	 *
638
	 * @return bool
639
	 */
640
	public function isOrphaned() {
641
		// Always false for root pages
642
		if(empty($this->ParentID)) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
643
			return false;
644
		}
645
646
		// Parent must exist and not be an orphan itself
647
		$parent = $this->Parent();
648
		return !$parent || !$parent->exists() || $parent->isOrphaned();
649
	}
650
651
	/**
652
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
653
	 *
654
	 * @return string
655
	 */
656
	public function LinkOrCurrent() {
657
		return $this->isCurrent() ? 'current' : 'link';
658
	}
659
660
	/**
661
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
662
	 *
663
	 * @return string
664
	 */
665
	public function LinkOrSection() {
666
		return $this->isSection() ? 'section' : 'link';
667
	}
668
669
	/**
670
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
671
	 * but in the current section.
672
	 *
673
	 * @return string
674
	 */
675
	public function LinkingMode() {
676
		if($this->isCurrent()) {
677
			return 'current';
678
		} elseif($this->isSection()) {
679
			return 'section';
680
		} else {
681
			return 'link';
682
		}
683
	}
684
685
	/**
686
	 * Check if this page is in the given current section.
687
	 *
688
	 * @param string $sectionName Name of the section to check
689
	 * @return bool True if we are in the given section
690
	 */
691
	public function InSection($sectionName) {
692
		$page = Director::get_current_page();
693
		while($page && $page->exists()) {
694
			if($sectionName == $page->URLSegment) {
695
				return true;
696
			}
697
			$page = $page->Parent();
698
		}
699
		return false;
700
	}
701
702
	/**
703
	 * Reset Sort on duped page
704
	 *
705
	 * @param SiteTree $original
706
	 * @param bool $doWrite
707
	 */
708
	public function onBeforeDuplicate($original, $doWrite) {
0 ignored issues
show
Unused Code introduced by
The parameter $original 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...
Unused Code introduced by
The parameter $doWrite 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...
709
		$this->Sort = 0;
710
	}
711
712
	/**
713
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
714
	 *
715
	 * @return static The duplicated object
716
	 */
717
	public function duplicateWithChildren() {
718
		/** @var SiteTree $clone */
719
		$clone = $this->duplicate();
720
		$children = $this->AllChildren();
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SilverStripe\CMS\Model\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...
721
722
		if($children) {
723
			/** @var SiteTree $child */
724
			$sort = 0;
725
			foreach($children as $child) {
726
				$childClone = $child->duplicateWithChildren();
727
				$childClone->ParentID = $clone->ID;
728
				//retain sort order by manually setting sort values
729
				$childClone->Sort = ++$sort;
730
				$childClone->write();
731
			}
732
		}
733
734
		return $clone;
735
	}
736
737
	/**
738
	 * Duplicate this node and its children as a child of the node with the given ID
739
	 *
740
	 * @param int $id ID of the new node's new parent
741
	 */
742
	public function duplicateAsChild($id) {
743
		/** @var SiteTree $newSiteTree */
744
		$newSiteTree = $this->duplicate();
745
		$newSiteTree->ParentID = $id;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
746
		$newSiteTree->Sort = 0;
747
		$newSiteTree->write();
748
	}
749
750
	/**
751
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
752
	 *
753
	 * @param int $maxDepth The maximum depth to traverse.
754
	 * @param boolean $unlinked Whether to link page titles.
755
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
756
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
757
	 * @return string The breadcrumb trail.
758
	 */
759
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
760
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
761
		$template = new SSViewer('BreadcrumbsTemplate');
762
		return $template->process($this->customise(new ArrayData(array(
763
			"Pages" => $pages,
764
			"Unlinked" => $unlinked
765
		))));
766
	}
767
768
769
	/**
770
	 * Returns a list of breadcrumbs for the current page.
771
	 *
772
	 * @param int $maxDepth The maximum depth to traverse.
773
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
774
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
775
	 *
776
	 * @return ArrayList
777
	*/
778
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
779
		$page = $this;
780
		$pages = array();
781
782
		while(
783
			$page
784
			&& $page->exists()
785
 			&& (!$maxDepth || count($pages) < $maxDepth)
786
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
787
 		) {
788
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
789
				$pages[] = $page;
790
			}
791
792
			$page = $page->Parent();
793
		}
794
795
		return new ArrayList(array_reverse($pages));
796
	}
797
798
799
	/**
800
	 * Make this page a child of another page.
801
	 *
802
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
803
	 *
804
	 * @param SiteTree|int $item Either the parent object, or the parent ID
805
	 */
806
	public function setParent($item) {
807
		if(is_object($item)) {
808
			if (!$item->exists()) $item->write();
809
			$this->setField("ParentID", $item->ID);
810
		} else {
811
			$this->setField("ParentID", $item);
812
		}
813
	}
814
815
	/**
816
	 * Get the parent of this page.
817
	 *
818
	 * @return SiteTree Parent of this page
819
	 */
820
	public function getParent() {
821
		if ($parentID = $this->getField("ParentID")) {
822
			return DataObject::get_by_id("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
823
		}
824
		return null;
825
	}
826
827
	/**
828
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
829
	 *
830
	 * @param int $level The maximum amount of levels to traverse.
831
	 * @param string $separator Seperating string
832
	 * @return string The resulting string
833
	 */
834
	public function NestedTitle($level = 2, $separator = " - ") {
835
		$item = $this;
836
		$parts = [];
837
		while($item && $level > 0) {
838
			$parts[] = $item->Title;
839
			$item = $item->getParent();
840
			$level--;
841
		}
842
		return implode($separator, array_reverse($parts));
843
	}
844
845
	/**
846
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
847
	 * the security model for an application.
848
	 *
849
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
850
	 * - Checks for existence of a method named "can<$perm>()" on the object
851
	 * - Calls decorators and only returns for FALSE "vetoes"
852
	 * - Falls back to {@link Permission::check()}
853
	 * - Does NOT check for many-many relations named "Can<$perm>"
854
	 *
855
	 * @uses DataObjectDecorator->can()
856
	 *
857
	 * @param string $perm The permission to be checked, such as 'View'
858
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
859
	 * @param array $context Context argument for canCreate()
860
	 * @return bool True if the the member is allowed to do the given action
861
	 */
862
	public function can($perm, $member = null, $context = array()) {
863 View Code Duplication
		if(!$member || !($member instanceof 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...
864
			$member = Member::currentUserID();
865
		}
866
867
		if($member && Permission::checkMember($member, "ADMIN")) return true;
868
869
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
870
			$method = 'can' . ucfirst($perm);
871
			return $this->$method($member);
872
		}
873
874
		$results = $this->extend('can', $member);
875
		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...
876
877
		return ($member && Permission::checkMember($member, $perm));
878
	}
879
880
	/**
881
	 * This function should return true if the current user can add children to this page. It can be overloaded to
882
	 * customise the security model for an application.
883
	 *
884
	 * Denies permission if any of the following conditions is true:
885
	 * - alternateCanAddChildren() on a extension returns false
886
	 * - canEdit() is not granted
887
	 * - There are no classes defined in {@link $allowed_children}
888
	 *
889
	 * @uses SiteTreeExtension->canAddChildren()
890
	 * @uses canEdit()
891
	 * @uses $allowed_children
892
	 *
893
	 * @param Member|int $member
894
	 * @return bool True if the current user can add children
895
	 */
896
	public function canAddChildren($member = null) {
897
		// Disable adding children to archived pages
898
		if(!$this->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\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...
899
			return false;
900
		}
901
902 View Code Duplication
		if(!$member || !($member instanceof 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...
903
			$member = Member::currentUserID();
904
		}
905
906
		// Standard mechanism for accepting permission changes from extensions
907
		$extended = $this->extendedCan('canAddChildren', $member);
908
		if($extended !== null) {
909
			return $extended;
910
		}
911
912
		// Default permissions
913
		if($member && Permission::checkMember($member, "ADMIN")) {
914
			return true;
915
		}
916
917
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
918
	}
919
920
	/**
921
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
922
	 * security model for an application.
923
	 *
924
	 * Denies permission if any of the following conditions is true:
925
	 * - canView() on any extension returns false
926
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
927
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
928
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
929
	 *
930
	 * @uses DataExtension->canView()
931
	 * @uses ViewerGroups()
932
	 *
933
	 * @param Member|int $member
934
	 * @return bool True if the current user can view this page
935
	 */
936
	public function canView($member = null) {
937 View Code Duplication
		if(!$member || !($member instanceof 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...
938
			$member = Member::currentUserID();
939
		}
940
941
		// Standard mechanism for accepting permission changes from extensions
942
		$extended = $this->extendedCan('canView', $member);
943
		if($extended !== null) {
944
			return $extended;
945
		}
946
947
		// admin override
948
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
949
			return true;
950
		}
951
952
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
953
		if($this->isOrphaned()) {
954
			return false;
955
		}
956
957
		// check for empty spec
958
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') {
959
			return true;
960
		}
961
962
		// check for inherit
963
		if($this->CanViewType == 'Inherit') {
964
			if($this->ParentID) return $this->Parent()->canView($member);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
965
			else return $this->getSiteConfig()->canViewPages($member);
966
		}
967
968
		// check for any logged-in users
969
		if($this->CanViewType == 'LoggedInUsers' && $member) {
970
			return true;
971
		}
972
973
		// check for specific groups
974
		if($member && is_numeric($member)) {
975
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
976
		}
977
		if(
978
			$this->CanViewType == 'OnlyTheseUsers'
979
			&& $member
980
			&& $member->inGroups($this->ViewerGroups())
981
		) return true;
982
983
		return false;
984
	}
985
986
	/**
987
	 * Check if this page can be published
988
	 *
989
	 * @param Member $member
990
	 * @return bool
991
	 */
992
	public function canPublish($member = null) {
993
		if(!$member) {
994
			$member = Member::currentUser();
995
		}
996
997
		// Check extension
998
		$extended = $this->extendedCan('canPublish', $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
999
		if($extended !== null) {
1000
			return $extended;
1001
		}
1002
1003
		if(Permission::checkMember($member, "ADMIN")) {
1004
			return true;
1005
		}
1006
1007
		// Default to relying on edit permission
1008
		return $this->canEdit($member);
1009
	}
1010
1011
	/**
1012
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
1013
	 * security model for an application.
1014
	 *
1015
	 * Denies permission if any of the following conditions is true:
1016
	 * - canDelete() returns false on any extension
1017
	 * - canEdit() returns false
1018
	 * - any descendant page returns false for canDelete()
1019
	 *
1020
	 * @uses canDelete()
1021
	 * @uses SiteTreeExtension->canDelete()
1022
	 * @uses canEdit()
1023
	 *
1024
	 * @param Member $member
1025
	 * @return bool True if the current user can delete this page
1026
	 */
1027
	public function canDelete($member = null) {
1028 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...
1029
		else if(is_numeric($member)) $memberID = $member;
1030
		else $memberID = Member::currentUserID();
1031
1032
		// Standard mechanism for accepting permission changes from extensions
1033
		$extended = $this->extendedCan('canDelete', $memberID);
1034
		if($extended !== null) {
1035
			return $extended;
1036
		}
1037
1038
		// Default permission check
1039
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1040
			return true;
1041
		}
1042
1043
		// Regular canEdit logic is handled by can_edit_multiple
1044
		$results = self::can_delete_multiple(array($this->ID), $memberID);
1045
1046
		// If this page no longer exists in stage/live results won't contain the page.
1047
		// Fail-over to false
1048
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
1049
	}
1050
1051
	/**
1052
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
1053
	 * can be overloaded to customise the security model for an application.
1054
	 *
1055
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1056
	 * create beneath a parent is based on the ability to edit that parent page.
1057
	 *
1058
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1059
	 *
1060
	 * @uses $can_create
1061
	 * @uses DataExtension->canCreate()
1062
	 *
1063
	 * @param Member $member
1064
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
1065
	 *                       If a parent page is known, it will be checked for validity.
1066
	 *                       If omitted, it will be assumed this is to be created as a top level page.
1067
	 * @return bool True if the current user can create pages on this class.
1068
	 */
1069
	public function canCreate($member = null, $context = array()) {
1070 View Code Duplication
		if(!$member || !(is_a($member, 'SilverStripe\\Security\\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...
1071
			$member = Member::currentUserID();
1072
		}
1073
1074
		// Check parent (custom canCreate option for SiteTree)
1075
		// Block children not allowed for this parent type
1076
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1077
		if($parent && !in_array(static::class, $parent->allowedChildren())) {
1078
			return false;
1079
		}
1080
1081
		// Standard mechanism for accepting permission changes from extensions
1082
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1083
		if($extended !== null) {
1084
			return $extended;
1085
		}
1086
1087
		// Check permission
1088
		if($member && Permission::checkMember($member, "ADMIN")) {
1089
			return true;
1090
		}
1091
1092
		// Fall over to inherited permissions
1093
		if($parent && $parent->exists()) {
1094
			return $parent->canAddChildren($member);
1095
		} else {
1096
			// This doesn't necessarily mean we are creating a root page, but that
1097
			// we don't know if there is a parent, so default to this permission
1098
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1099
		}
1100
	}
1101
1102
	/**
1103
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1104
	 * security model for an application.
1105
	 *
1106
	 * Denies permission if any of the following conditions is true:
1107
	 * - canEdit() on any extension returns false
1108
	 * - canView() return false
1109
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1110
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1111
	 *   CMS_Access_CMSMAIN permission code
1112
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1113
	 *
1114
	 * @uses canView()
1115
	 * @uses EditorGroups()
1116
	 * @uses DataExtension->canEdit()
1117
	 *
1118
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1119
	 *                       unit tests)
1120
	 * @return bool True if the current user can edit this page
1121
	 */
1122
	public function canEdit($member = null) {
1123 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...
1124
		else if(is_numeric($member)) $memberID = $member;
1125
		else $memberID = Member::currentUserID();
1126
1127
		// Standard mechanism for accepting permission changes from extensions
1128
		$extended = $this->extendedCan('canEdit', $memberID);
1129
		if($extended !== null) {
1130
			return $extended;
1131
		}
1132
1133
		// Default permissions
1134
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1135
			return true;
1136
		}
1137
1138
		if($this->ID) {
1139
			// Regular canEdit logic is handled by can_edit_multiple
1140
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1141
1142
			// If this page no longer exists in stage/live results won't contain the page.
1143
			// Fail-over to false
1144
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1145
1146
		// Default for unsaved pages
1147
		} else {
1148
			return $this->getSiteConfig()->canEditPages($member);
1149
		}
1150
	}
1151
1152
	/**
1153
	 * Stub method to get the site config, unless the current class can provide an alternate.
1154
	 *
1155
	 * @return SiteConfig
1156
	 */
1157
	public function getSiteConfig() {
1158
		$configs = $this->invokeWithExtensions('alternateSiteConfig');
1159
		foreach(array_filter($configs) as $config) {
1160
			return $config;
1161
		}
1162
1163
		return SiteConfig::current_site_config();
1164
	}
1165
1166
	/**
1167
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1168
	 * can_(perm)_multiple method for efficiency.
1169
	 *
1170
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1171
	 * @param array           $ids           An array of page IDs
1172
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1173
	 *                                       to 'SiteTree::can_(permission)_multiple'
1174
	 */
1175
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1176
		if(!$batchCallback) {
1177
			$batchCallback = self::class . "::can_{$permission}_multiple";
1178
		}
1179
1180
		if(is_callable($batchCallback)) {
1181
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1182
		} else {
1183
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1184
				. "with callback '$batchCallback'", E_USER_WARNING);
1185
		}
1186
	}
1187
1188
	/**
1189
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1190
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1191
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1192
	 * efficiently.
1193
	 *
1194
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1195
	 * property to FALSE.
1196
	 *
1197
	 * @param array  $ids              Of {@link SiteTree} IDs
1198
	 * @param int    $memberID         Member ID
1199
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1200
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1201
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1202
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1203
	 * @param bool   $useCached
1204
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1205
	 */
1206
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1207
												  $globalPermission = null, $useCached = true) {
1208
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1209
1210
		// Sanitise the IDs
1211
		$ids = array_filter($ids, 'is_numeric');
1212
1213
		// This is the name used on the permission cache
1214
		// converts something like 'CanEditType' to 'edit'.
1215
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1216
1217
		// Default result: nothing editable
1218
		$result = array_fill_keys($ids, false);
1219
		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...
1220
1221
			// Look in the cache for values
1222
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1223
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1224
1225
				// If we can't find everything in the cache, then look up the remainder separately
1226
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1227
				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...
1228
					$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 1208 can also be of type array<integer,string,{"0":"string","1":"string"}>; however, SilverStripe\CMS\Model\S...atch_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...
1229
				}
1230
				return $cachedValues;
1231
			}
1232
1233
			// If a member doesn't have a certain permission then they can't edit anything
1234
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1235
				return $result;
1236
			}
1237
1238
			// Placeholder for parameterised ID list
1239
			$idPlaceholders = DB::placeholders($ids);
1240
1241
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1242
			// be enabled
1243
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1244
1245
			// Get the groups that the given member belongs to
1246
			/** @var Member $member */
1247
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1248
			$groupIDs = $member->Groups()->column("ID");
1249
			$SQL_groupList = implode(", ", $groupIDs);
1250
			if (!$SQL_groupList) {
1251
				$SQL_groupList = '0';
1252
			}
1253
1254
			$combinedStageResult = array();
1255
1256
			foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1257
				// Start by filling the array with the pages that actually exist
1258
				/** @skipUpgrade */
1259
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1260
1261
				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...
1262
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1263
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1264
				} else {
1265
					$stageIds = array();
1266
				}
1267
				$result = array_fill_keys($stageIds, false);
1268
1269
				// Get the uninherited permissions
1270
				$uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1271
					->where(array(
1272
						"(\"$typeField\" = 'LoggedInUsers' OR
1273
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1274
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1275
						=> $ids
1276
					))
1277
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1278
1279
				if($uninheritedPermissions) {
1280
					// Set all the relevant items in $result to true
1281
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1282
				}
1283
1284
				// Get permissions that are inherited
1285
				$potentiallyInherited = Versioned::get_by_stage(
1286
					"SilverStripe\\CMS\\Model\\SiteTree",
1287
					$stage,
1288
					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...
1289
				);
1290
1291
				if($potentiallyInherited) {
1292
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1293
					// then see which ones the user has permission on
1294
					$groupedByParent = array();
1295
					foreach($potentiallyInherited as $item) {
1296
						/** @var SiteTree $item */
1297
						if($item->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1298
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1299
							$groupedByParent[$item->ParentID][] = $item->ID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1300
						} else {
1301
							// Might return different site config based on record context, e.g. when subsites module
1302
							// is used
1303
							$siteConfig = $item->getSiteConfig();
1304
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1305
						}
1306
					}
1307
1308
					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...
1309
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1310
						if($actuallyInherited) {
1311
							$parentIDs = array_keys(array_filter($actuallyInherited));
1312
							foreach($parentIDs as $parentID) {
1313
								// Set all the relevant items in $result to true
1314
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1315
							}
1316
						}
1317
					}
1318
				}
1319
1320
				$combinedStageResult = $combinedStageResult + $result;
1321
1322
			}
1323
		}
1324
1325
		if(isset($combinedStageResult)) {
1326
			// Cache the results
1327
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1328
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1329
			return $combinedStageResult;
1330
		} else {
1331
			return array();
1332
		}
1333
	}
1334
1335
	/**
1336
	 * Get the 'can edit' information for a number of SiteTree pages.
1337
	 *
1338
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1339
	 * @param int   $memberID  ID of member
1340
	 * @param bool  $useCached Return values from the permission cache if they exist
1341
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1342
	 *                         edited
1343
	 */
1344
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1345
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1346
	}
1347
1348
	/**
1349
	 * Get the 'can edit' information for a number of SiteTree pages.
1350
	 *
1351
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1352
	 * @param int   $memberID  ID of member
1353
	 * @param bool  $useCached Return values from the permission cache if they exist
1354
	 * @return array
1355
	 */
1356
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1357
		$deletable = array();
1358
		$result = array_fill_keys($ids, false);
1359
		$cacheKey = "delete-$memberID";
1360
1361
		// Look in the cache for values
1362
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1363
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1364
1365
			// If we can't find everything in the cache, then look up the remainder separately
1366
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1367
			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...
1368
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1369
					+ $cachedValues;
1370
			}
1371
			return $cachedValues;
1372
		}
1373
1374
		// You can only delete pages that you can edit
1375
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1376
		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...
1377
1378
			// You can only delete pages whose children you can delete
1379
			$editablePlaceholders = DB::placeholders($editableIDs);
1380
			$childRecords = SiteTree::get()->where(array(
1381
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1382
			));
1383
			if($childRecords) {
1384
				$children = $childRecords->map("ID", "ParentID");
1385
1386
				// Find out the children that can be deleted
1387
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1388
1389
				// Get a list of all the parents that have no undeletable children
1390
				$deletableParents = array_fill_keys($editableIDs, true);
1391
				foreach($deletableChildren as $id => $canDelete) {
1392
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1393
				}
1394
1395
				// Use that to filter the list of deletable parents that have children
1396
				$deletableParents = array_keys($deletableParents);
1397
1398
				// Also get the $ids that don't have children
1399
				$parents = array_unique($children->values());
1400
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1401
1402
				// Combine the two
1403
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1404
1405
			} else {
1406
				$deletable = $editableIDs;
1407
			}
1408
		}
1409
1410
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1411
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1412
	}
1413
1414
	/**
1415
	 * Collate selected descendants of this page.
1416
	 *
1417
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1418
	 * $collator array.
1419
	 *
1420
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1421
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1422
	 * @return bool
1423
	 */
1424
	public function collateDescendants($condition, &$collator) {
1425
		$children = $this->Children();
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\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...
1426
		if($children) {
1427
			foreach($children as $item) {
1428
1429
				if(eval("return $condition;")) {
1430
					$collator[] = $item;
1431
				}
1432
				/** @var SiteTree $item */
1433
				$item->collateDescendants($condition, $collator);
1434
			}
1435
			return true;
1436
		}
1437
		return false;
1438
	}
1439
1440
	/**
1441
	 * Return the title, description, keywords and language metatags.
1442
	 *
1443
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1444
	 *
1445
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1446
	 * @return string The XHTML metatags
1447
	 */
1448
	public function MetaTags($includeTitle = true) {
1449
		$tags = array();
1450
		if($includeTitle && strtolower($includeTitle) != 'false') {
1451
			$tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1452
		}
1453
1454
		$generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1455
		if (!empty($generator)) {
1456
			$tags[] = FormField::create_tag('meta', array(
1457
				'name' => 'generator',
1458
				'content' => $generator,
1459
			));
1460
		}
1461
1462
		$charset = Config::inst()->get('SilverStripe\\Control\\ContentNegotiator', 'encoding');
1463
		$tags[] = FormField::create_tag('meta', array(
1464
			'http-equiv' => 'Content-Type',
1465
			'content' => 'text/html; charset=' . $charset,
1466
		));
1467
		if($this->MetaDescription) {
1468
			$tags[] = FormField::create_tag('meta', array(
1469
				'name' => 'description',
1470
				'content' => $this->MetaDescription,
1471
			));
1472
		}
1473
1474
		if(Permission::check('CMS_ACCESS_CMSMain')
1475
			&& !$this instanceof ErrorPage
1476
			&& $this->ID > 0
1477
		) {
1478
			$tags[] = FormField::create_tag('meta', array(
1479
				'name' => 'x-page-id',
1480
				'content' => $this->obj('ID')->forTemplate(),
1481
			));
1482
			$tags[] = FormField::create_tag('meta', array(
1483
				'name' => 'x-cms-edit-link',
1484
				'content' => $this->obj('CMSEditLink')->forTemplate(),
1485
			));
1486
		}
1487
1488
		$tags = implode("\n", $tags);
1489
		if($this->ExtraMeta) {
1490
			$tags .= $this->obj('ExtraMeta')->forTemplate();
1491
		}
1492
1493
		$this->extend('MetaTags', $tags);
1494
1495
		return $tags;
1496
	}
1497
1498
	/**
1499
	 * Returns the object that contains the content that a user would associate with this page.
1500
	 *
1501
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1502
	 * return the page that is linked to.
1503
	 *
1504
	 * @return $this
1505
	 */
1506
	public function ContentSource() {
1507
		return $this;
1508
	}
1509
1510
	/**
1511
	 * Add default records to database.
1512
	 *
1513
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1514
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1515
	 */
1516
	public function requireDefaultRecords() {
1517
		parent::requireDefaultRecords();
1518
1519
		// default pages
1520
		if(static::class == self::class && $this->config()->create_default_pages) {
1521
			if(!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1522
				$homepage = new Page();
1523
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1524
				$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>');
1525
				$homepage->URLSegment = RootURLController::config()->default_homepage_link;
1526
				$homepage->Sort = 1;
1527
				$homepage->write();
1528
				$homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1529
				$homepage->flushCache();
1530
				DB::alteration_message('Home page created', 'created');
1531
			}
1532
1533
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1534
				$aboutus = new Page();
1535
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1536
				$aboutus->Content = _t(
1537
					'SiteTree.DEFAULTABOUTCONTENT',
1538
					'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1539
				);
1540
				$aboutus->Sort = 2;
1541
				$aboutus->write();
1542
				$aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1543
				$aboutus->flushCache();
1544
				DB::alteration_message('About Us page created', 'created');
1545
1546
				$contactus = new Page();
1547
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1548
				$contactus->Content = _t(
1549
					'SiteTree.DEFAULTCONTACTCONTENT',
1550
					'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1551
				);
1552
				$contactus->Sort = 3;
1553
				$contactus->write();
1554
				$contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1555
				$contactus->flushCache();
1556
				DB::alteration_message('Contact Us page created', 'created');
1557
			}
1558
		}
1559
	}
1560
1561
	protected function onBeforeWrite() {
1562
		parent::onBeforeWrite();
1563
1564
		// If Sort hasn't been set, make this page come after it's siblings
1565
		if(!$this->Sort) {
1566
			$parentID = ($this->ParentID) ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1567
			$this->Sort = DB::prepared_query(
1568
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1569
				array($parentID)
1570
			)->value();
1571
		}
1572
1573
		// If there is no URLSegment set, generate one from Title
1574
		$defaultSegment = $this->generateURLSegment(_t(
1575
			'CMSMain.NEWPAGE',
1576
			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...
1577
		));
1578
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1579
			$this->URLSegment = $this->generateURLSegment($this->Title);
1580
		} else if($this->isChanged('URLSegment', 2)) {
1581
			// Do a strict check on change level, to avoid double encoding caused by
1582
			// bogus changes through forceChange()
1583
			$filter = URLSegmentFilter::create();
1584
			$this->URLSegment = $filter->filter($this->URLSegment);
1585
			// If after sanitising there is no URLSegment, give it a reasonable default
1586
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1587
		}
1588
1589
		// Ensure that this object has a non-conflicting URLSegment value.
1590
		$count = 2;
1591
		while(!$this->validURLSegment()) {
1592
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1593
			$count++;
1594
		}
1595
1596
		$this->syncLinkTracking();
1597
1598
		// Check to see if we've only altered fields that shouldn't affect versioning
1599
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1600
		$changedFields = array_keys($this->getChangedFields(true, 2));
1601
1602
		// This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1603
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1604
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1605
1606
		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...
1607
			// This will have the affect of preserving the versioning
1608
			$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<SilverStripe\CMS\Model\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...
1609
		}
1610
	}
1611
1612
	/**
1613
	 * Trigger synchronisation of link tracking
1614
	 *
1615
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1616
	 */
1617
	public function syncLinkTracking() {
1618
		$this->extend('augmentSyncLinkTracking');
1619
	}
1620
1621
	public function onBeforeDelete() {
1622
		parent::onBeforeDelete();
1623
1624
		// If deleting this page, delete all its children.
1625
		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<SilverStripe\CMS\Model\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...
1626
			foreach($children as $child) {
1627
				/** @var SiteTree $child */
1628
				$child->delete();
1629
			}
1630
		}
1631
	}
1632
1633
	public function onAfterDelete() {
1634
		// Need to flush cache to avoid outdated versionnumber references
1635
		$this->flushCache();
1636
1637
		// Need to mark pages depending to this one as broken
1638
		$dependentPages = $this->DependentPages();
1639
		if($dependentPages) foreach($dependentPages as $page) {
1640
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1641
			$page->write();
1642
		}
1643
1644
		parent::onAfterDelete();
1645
	}
1646
1647
	public function flushCache($persistent = true) {
1648
		parent::flushCache($persistent);
1649
		$this->_cache_statusFlags = null;
1650
	}
1651
1652
	public function validate() {
1653
		$result = parent::validate();
1654
1655
		// Allowed children validation
1656
		$parent = $this->getParent();
1657
		if($parent && $parent->exists()) {
1658
			// No need to check for subclasses or instanceof, as allowedChildren() already
1659
			// deconstructs any inheritance trees already.
1660
			$allowed = $parent->allowedChildren();
1661
			$subject = ($this instanceof VirtualPage && $this->CopyContentFromID)
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...
1662
				? $this->CopyContentFrom()
0 ignored issues
show
Documentation Bug introduced by
The method CopyContentFrom does not exist on object<SilverStripe\CMS\Model\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...
1663
				: $this;
1664
			if(!in_array($subject->ClassName, $allowed)) {
1665
				$result->addError(
1666
					_t(
1667
						'SiteTree.PageTypeNotAllowed',
1668
						'Page type "{type}" not allowed as child of this parent page',
1669
						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...
1670
					),
1671
					ValidationResult::TYPE_ERROR,
1672
					'ALLOWED_CHILDREN'
1673
				);
1674
			}
1675
		}
1676
1677
		// "Can be root" validation
1678
		if(!$this->stat('can_be_root') && !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1679
			$result->addError(
1680
				_t(
1681
					'SiteTree.PageTypNotAllowedOnRoot',
1682
					'Page type "{type}" is not allowed on the root level',
1683
					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...
1684
				),
1685
				ValidationResult::TYPE_ERROR,
1686
				'CAN_BE_ROOT'
1687
			);
1688
		}
1689
1690
		return $result;
1691
	}
1692
1693
	/**
1694
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1695
	 * checks for:
1696
	 *  - A page with the same URLSegment that has a conflict
1697
	 *  - Conflicts with actions on the parent page
1698
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1699
	 *
1700
	 * @return bool
1701
	 */
1702
	public function validURLSegment() {
1703
		if(self::config()->nested_urls && $parent = $this->Parent()) {
1704
			if($controller = ModelAsController::controller_for($parent)) {
1705
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1706
			}
1707
		}
1708
1709
		if(!self::config()->nested_urls || !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1710
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'SilverStripe\\Control\\RequestHandler')) return false;
1711
		}
1712
1713
		// Filters by url, id, and parent
1714
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1715
		if($this->ID) {
1716
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1717
		}
1718
		if(self::config()->nested_urls) {
1719
			$filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
1720
		}
1721
1722
		// If any of the extensions return `0` consider the segment invalid
1723
		$extensionResponses = array_filter(
1724
			(array)$this->extend('augmentValidURLSegment'),
1725
			function($response) {return !is_null($response);}
1726
		);
1727
		if($extensionResponses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensionResponses 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...
1728
			return min($extensionResponses);
1729
		}
1730
1731
		// Check existence
1732
		return !DataObject::get(self::class, $filter)->exists();
1733
	}
1734
1735
	/**
1736
	 * Generate a URL segment based on the title provided.
1737
	 *
1738
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1739
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1740
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1741
	 * scratch, or incrementally modify the generated URL.
1742
	 *
1743
	 * @param string $title Page title
1744
	 * @return string Generated url segment
1745
	 */
1746
	public function generateURLSegment($title){
1747
		$filter = URLSegmentFilter::create();
1748
		$t = $filter->filter($title);
1749
1750
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1751
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1752
1753
		// Hook for extensions
1754
		$this->extend('updateURLSegment', $t, $title);
1755
1756
		return $t;
1757
	}
1758
1759
	/**
1760
	 * Gets the URL segment for the latest draft version of this page.
1761
	 *
1762
	 * @return string
1763
	 */
1764
	public function getStageURLSegment() {
1765
		$stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|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...
1766
			'"SiteTree"."ID"' => $this->ID
1767
		));
1768
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1769
	}
1770
1771
	/**
1772
	 * Gets the URL segment for the currently published version of this page.
1773
	 *
1774
	 * @return string
1775
	 */
1776
	public function getLiveURLSegment() {
1777
		$liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|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...
1778
			'"SiteTree"."ID"' => $this->ID
1779
		));
1780
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1781
	}
1782
1783
	/**
1784
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1785
	 *
1786
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1787
	 * @return ArrayList
1788
	 */
1789
	public function DependentPages($includeVirtuals = true) {
1790
		if(class_exists('Subsite')) {
1791
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1792
			Subsite::disable_subsite_filter(true);
1793
		}
1794
1795
		// Content links
1796
		$items = new ArrayList();
1797
1798
		// We merge all into a regular SS_List, because DataList doesn't support merge
1799
		if($contentLinks = $this->BackLinkTracking()) {
0 ignored issues
show
Documentation Bug introduced by
The method BackLinkTracking does not exist on object<SilverStripe\CMS\Model\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...
1800
			$linkList = new ArrayList();
1801
			foreach($contentLinks as $item) {
1802
				$item->DependentLinkType = 'Content link';
1803
				$linkList->push($item);
1804
			}
1805
			$items->merge($linkList);
1806
		}
1807
1808
		// Virtual pages
1809
		if($includeVirtuals) {
1810
			$virtuals = $this->VirtualPages();
1811
			if($virtuals) {
1812
				$virtualList = new ArrayList();
1813
				foreach($virtuals as $item) {
1814
					$item->DependentLinkType = 'Virtual page';
1815
					$virtualList->push($item);
1816
				}
1817
				$items->merge($virtualList);
1818
			}
1819
		}
1820
1821
		// Redirector pages
1822
		$redirectors = RedirectorPage::get()->where(array(
1823
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1824
			'"RedirectorPage"."LinkToID"' => $this->ID
1825
		));
1826
		if($redirectors) {
1827
			$redirectorList = new ArrayList();
1828
			foreach($redirectors as $item) {
1829
				$item->DependentLinkType = 'Redirector page';
1830
				$redirectorList->push($item);
1831
			}
1832
			$items->merge($redirectorList);
1833
		}
1834
1835
		if(class_exists('Subsite')) {
1836
			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...
1837
		}
1838
1839
		return $items;
1840
	}
1841
1842
	/**
1843
	 * Return all virtual pages that link to this page.
1844
	 *
1845
	 * @return DataList
1846
	 */
1847
	public function VirtualPages() {
1848
		$pages = parent::VirtualPages();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SilverStripe\ORM\DataObject as the method VirtualPages() does only exist in the following sub-classes of SilverStripe\ORM\DataObject: SilverStripe\CMS\Model\SiteTree. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1849
1850
		// Disable subsite filter for these pages
1851
		if($pages instanceof DataList) {
1852
			return $pages->setDataQueryParam('Subsite.filter', false);
1853
		} else {
1854
			return $pages;
1855
		}
1856
	}
1857
1858
	/**
1859
	 * Returns a FieldList with which to create the main editing form.
1860
	 *
1861
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1862
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1863
	 *
1864
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1865
	 * e.g. access control.
1866
	 *
1867
	 * @return FieldList The fields to be displayed in the CMS
1868
	 */
1869
	public function getCMSFields() {
1870
		// Status / message
1871
		// Create a status message for multiple parents
1872
		if($this->ID && is_numeric($this->ID)) {
1873
			$linkedPages = $this->VirtualPages();
1874
1875
			$parentPageLinks = array();
1876
1877
			if($linkedPages->count() > 0) {
1878
				/** @var VirtualPage $linkedPage */
1879
				foreach($linkedPages as $linkedPage) {
1880
					$parentPage = $linkedPage->Parent();
1881
					if($parentPage && $parentPage->exists()) {
1882
						$link = Convert::raw2att($parentPage->CMSEditLink());
1883
						$title = Convert::raw2xml($parentPage->Title);
1884
						} else {
1885
						$link = CMSPageEditController::singleton()->Link('show');
1886
						$title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
1887
						}
1888
					$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1889
				}
1890
1891
				$lastParent = array_pop($parentPageLinks);
1892
				$parentList = "'$lastParent'";
1893
1894
				if(count($parentPageLinks)) {
1895
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1896
						. $parentList;
1897
				}
1898
1899
				$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...
1900
					'SiteTree.APPEARSVIRTUALPAGES',
1901
					"This content also appears on the virtual pages in the {title} sections.",
1902
					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...
1903
				);
1904
			}
1905
		}
1906
1907
		if($this->HasBrokenLink || $this->HasBrokenFile) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenLink does not exist on object<SilverStripe\CMS\Model\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<SilverStripe\CMS\Model\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...
1908
			$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...
1909
		}
1910
1911
		$dependentNote = '';
1912
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1913
1914
		// Create a table for showing pages linked to this one
1915
		$dependentPages = $this->DependentPages();
1916
		$dependentPagesCount = $dependentPages->count();
1917
		if($dependentPagesCount) {
1918
			$dependentColumns = array(
1919
				'Title' => $this->fieldLabel('Title'),
1920
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1921
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1922
			);
1923
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1924
1925
			$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>');
1926
			$dependentTable = GridField::create(
1927
				'DependentPages',
1928
				false,
1929
				$dependentPages
1930
			);
1931
			/** @var GridFieldDataColumns $dataColumns */
1932
			$dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1933
			$dataColumns
1934
				->setDisplayFields($dependentColumns)
1935
				->setFieldFormatting(array(
1936
					'Title' => function($value, &$item) {
1937
						return sprintf(
1938
							'<a href="admin/pages/edit/show/%d">%s</a>',
1939
							(int)$item->ID,
1940
							Convert::raw2xml($item->Title)
1941
						);
1942
					},
1943
					'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...
1944
						return sprintf(
1945
							'<a href="%s" target="_blank">%s</a>',
1946
							Convert::raw2xml($value),
1947
							Convert::raw2xml($value)
1948
						);
1949
					}
1950
				));
1951
		}
1952
1953
		$baseLink = Controller::join_links (
1954
			Director::absoluteBaseURL(),
1955
			(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<SilverStripe\CMS\Model\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
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...
1956
		);
1957
1958
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1959
			->setURLPrefix($baseLink)
1960
			->setDefaultURL($this->generateURLSegment(_t(
1961
				'CMSMain.NEWPAGE',
1962
				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...
1963
			)));
1964
		$helpText = (self::config()->nested_urls && $this->Children()->count())
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\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...
1965
			? $this->fieldLabel('LinkChangeNote')
1966
			: '';
1967
		if(!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
1968
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1969
		}
1970
		$urlsegment->setHelpText($helpText);
1971
1972
		$fields = new FieldList(
1973
			$rootTab = new TabSet("Root",
1974
				$tabMain = new Tab('Main',
1975
					new TextField("Title", $this->fieldLabel('Title')),
1976
					$urlsegment,
1977
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1978
					$htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1979
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1980
						array(
1981
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1982
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1983
						)
1984
					)->setHeadingLevel(4)
1985
				),
1986
				$tabDependent = new Tab('Dependent',
1987
					$dependentNote,
1988
					$dependentTable
1989
				)
1990
			)
1991
		);
1992
		$htmlField->addExtraClass('stacked');
1993
1994
		// Help text for MetaData on page content editor
1995
		$metaFieldDesc
1996
			->setRightTitle(
1997
				_t(
1998
					'SiteTree.METADESCHELP',
1999
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
2000
				)
2001
			)
2002
			->addExtraClass('help');
2003
		$metaFieldExtra
2004
			->setRightTitle(
2005
				_t(
2006
					'SiteTree.METAEXTRAHELP',
2007
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2008
				)
2009
			)
2010
			->addExtraClass('help');
2011
2012
		// Conditional dependent pages tab
2013
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2014
		else $fields->removeFieldFromTab('Root', 'Dependent');
2015
2016
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2017
2018
		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...
2019
			$obsoleteWarning = _t(
2020
				'SiteTree.OBSOLETECLASS',
2021
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2022
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
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...
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...
2023
			);
2024
2025
			$fields->addFieldToTab(
2026
				"Root.Main",
2027
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2028
				"Title"
2029
			);
2030
		}
2031
2032
		if(file_exists(BASE_PATH . '/install.php')) {
2033
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2034
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2035
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2036
				. "</p>"), "Title");
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
						$parentType = 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'), self::class, '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
						->setSource($groupsMap)
2086
						->setAttribute(
2087
							'data-placeholder',
2088
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2089
						),
2090
					$editorsOptionsField = new OptionsetField(
2091
						"CanEditType",
2092
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2093
					),
2094
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2095
						->setSource($groupsMap)
2096
						->setAttribute(
2097
							'data-placeholder',
2098
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2099
						)
2100
				)
2101
			)
2102
		);
2103
2104
		$parentType->addExtraClass('noborder');
2105
		$visibility->setTitle($this->fieldLabel('Visibility'));
2106
2107
2108
		// This filter ensures that the ParentID dropdown selection does not show this node,
2109
		// or its descendents, as this causes vanishing bugs
2110
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2111
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2112
2113
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2114
2115
		// Make page location fields read-only if the user doesn't have the appropriate permission
2116
		if(!Permission::check("SITETREE_REORGANISE")) {
2117
			$fields->makeFieldReadonly('ParentType');
2118
			if($this->getParentType() === 'root') {
2119
				$fields->removeByName('ParentID');
2120
			} else {
2121
				$fields->makeFieldReadonly('ParentID');
2122
			}
2123
		}
2124
2125
		$viewersOptionsSource = array();
2126
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2127
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2128
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2129
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2130
		$viewersOptionsField->setSource($viewersOptionsSource);
2131
2132
		$editorsOptionsSource = array();
2133
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2134
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2135
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2136
		$editorsOptionsField->setSource($editorsOptionsSource);
2137
2138
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2139
			$fields->makeFieldReadonly($viewersOptionsField);
2140
			if($this->CanViewType == 'OnlyTheseUsers') {
2141
				$fields->makeFieldReadonly($viewerGroupsField);
2142
			} else {
2143
				$fields->removeByName('ViewerGroups');
2144
			}
2145
2146
			$fields->makeFieldReadonly($editorsOptionsField);
2147
			if($this->CanEditType == 'OnlyTheseUsers') {
2148
				$fields->makeFieldReadonly($editorGroupsField);
2149
			} else {
2150
				$fields->removeByName('EditorGroups');
2151
			}
2152
		}
2153
2154
		if(self::$runCMSFieldsExtensions) {
2155
			$this->extend('updateSettingsFields', $fields);
2156
		}
2157
2158
		return $fields;
2159
	}
2160
2161
	/**
2162
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2163
	 * @return array
2164
	 */
2165
	public function fieldLabels($includerelations = true) {
2166
		$cacheKey = static::class . '_' . $includerelations;
2167
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2168
			$labels = parent::fieldLabels($includerelations);
2169
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2170
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2171
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2172
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2173
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2174
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2175
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2176
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2177
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2178
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2179
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2180
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2181
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2182
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2183
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2184
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2185
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2186
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2187
			$labels['LinkChangeNote'] = _t (
2188
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2189
			);
2190
2191
			if($includerelations){
2192
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2193
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2194
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2195
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2196
			}
2197
2198
			self::$_cache_field_labels[$cacheKey] = $labels;
2199
		}
2200
2201
		return self::$_cache_field_labels[$cacheKey];
2202
	}
2203
2204
	/**
2205
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2206
	 *
2207
	 * Frontend scripts and styles know how to handle the following FormFields:
2208
	 * - top-level FormActions appear as standalone buttons
2209
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2210
	 * - TabSet & Tabs appear as a drop ups
2211
	 * - FormActions within the Tab are restyled as links
2212
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2213
	 *
2214
	 * @return FieldList The available actions for this page.
2215
	 */
2216
	public function getCMSActions() {
2217
		// Get status of page
2218
		$isOnDraft = $this->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\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...
2219
		$isPublished = $this->isPublished();
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\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...
2220
		$stagesDiffer = $this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SilverStripe\CMS\Model\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...
2221
2222
		// Check permissions
2223
		$canPublish = $this->canPublish();
2224
		$canUnpublish = $this->canUnpublish();
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\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...
2225
		$canEdit = $this->canEdit();
2226
2227
		// Major actions appear as buttons immediately visible as page actions.
2228
		$majorActions = CompositeField::create()->setName('MajorActions');
2229
		$majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2230
2231
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2232
		$rootTabSet = new TabSet('ActionMenus');
2233
		$moreOptions = new Tab(
2234
			'MoreOptions',
2235
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2236
		);
2237
		$rootTabSet->push($moreOptions);
2238
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2239
2240
		// Render page information into the "more-options" drop-up, on the top.
2241
		$liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2242
		$infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2243
		$moreOptions->push(
2244
			new LiteralField('Information',
2245
				$this->customise(array(
2246
					'Live' => $liveRecord,
2247
					'ExistsOnLive' => $isPublished
2248
				))->renderWith($infoTemplate)
2249
			)
2250
		);
2251
2252
		// Add to campaign option if not-archived and has publish permission
2253
		if (($isPublished || $isOnDraft) && $canPublish) {
2254
			$moreOptions->push(
2255
				AddToCampaignHandler_FormAction::create()
2256
					->removeExtraClass('btn-primary')
2257
					->addExtraClass('btn-secondary')
2258
				);
2259
		}
2260
2261
		// "readonly"/viewing version that isn't the current version of the record
2262
		$stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2263
		/** @skipUpgrade */
2264
		if($stageRecord && $stageRecord->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...
2265
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2266
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2267
			$actions = new FieldList(array($majorActions, $rootTabSet));
2268
2269
			// getCMSActions() can be extended with updateCMSActions() on a extension
2270
			$this->extend('updateCMSActions', $actions);
2271
			return $actions;
2272
		}
2273
2274
		// "unpublish"
2275
		if($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2276
			$moreOptions->push(
2277
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2278
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2279
					->addExtraClass('btn-secondary')
2280
			);
2281
		}
2282
2283
		// "rollback"
2284
		if($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2285
			$moreOptions->push(
2286
				FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2287
					->setDescription(_t(
2288
						'SiteTree.BUTTONCANCELDRAFTDESC',
2289
						'Delete your draft and revert to the currently published page'
2290
					))
2291
					->addExtraClass('btn-secondary')
2292
			);
2293
		}
2294
2295
		// "restore"
2296
		if($canEdit && !$isOnDraft && $isPublished) {
2297
			$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2298
		}
2299
2300
		// Check if we can restore a deleted page
2301
		// Note: It would be nice to have a canRestore() permission at some point
2302
		if($canEdit && !$isOnDraft && !$isPublished) {
2303
			// Determine if we should force a restore to root (where once it was a subpage)
2304
			$restoreToRoot = $this->isParentArchived();
2305
2306
			// "restore"
2307
			$title = $restoreToRoot
2308
				? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2309
				: _t('CMSMain.RESTORE','Restore draft');
2310
			$description = $restoreToRoot
2311
				? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2312
				: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2313
			$majorActions->push(
2314
				FormAction::create('restore', $title)
2315
					->setDescription($description)
2316
					->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...
2317
					->setAttribute('data-icon', 'decline')
2318
			);
2319
		}
2320
2321
		// If a page is on any stage it can be archived
2322
		if (($isOnDraft || $isPublished) && $this->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\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...
2323
			$title = $isPublished
2324
				? _t('CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2325
				: _t('CMSMain.ARCHIVE', 'Archive');
2326
			$moreOptions->push(
2327
				FormAction::create('archive', $title)
2328
					->addExtraClass('delete btn btn-secondary')
2329
					->setDescription(_t(
2330
						'SiteTree.BUTTONDELETEDESC',
2331
						'Remove from draft/live and send to archive'
2332
					))
2333
			);
2334
		}
2335
2336
		// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2337
		if ($canEdit && $isOnDraft) {
2338
			$majorActions->push(
2339
				FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2340
					->addExtraClass('btn-secondary-outline font-icon-check-mark')
2341
					->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2342
					->setUseButtonTag(true)
2343
					->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2344
			);
2345
		}
2346
2347
		if($canPublish && $isOnDraft) {
2348
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2349
			$majorActions->push(
2350
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2351
					->addExtraClass('btn-secondary-outline font-icon-check-mark')
2352
					->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2353
					->setUseButtonTag(true)
2354
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2355
			);
2356
2357
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2358
			if($stagesDiffer) {
2359
				$publish->addExtraClass('btn-primary font-icon-rocket');
2360
				$publish->setTitle(_t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
2361
				$publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2362
			}
2363
		}
2364
2365
		$actions = new FieldList(array($majorActions, $rootTabSet));
2366
2367
		// Hook for extensions to add/remove actions.
2368
		$this->extend('updateCMSActions', $actions);
2369
2370
		return $actions;
2371
	}
2372
2373
	public function onAfterPublish() {
2374
		// Force live sort order to match stage sort order
2375
		DB::prepared_query('UPDATE "SiteTree_Live"
2376
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2377
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2378
			array($this->ParentID)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2379
		);
2380
		}
2381
2382
	/**
2383
	 * Update draft dependant pages
2384
	 */
2385
	public function onAfterRevertToLive() {
2386
		// Use an alias to get the updates made by $this->publish
2387
		/** @var SiteTree $stageSelf */
2388
		$stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2389
		$stageSelf->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\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...
2390
2391
		// Need to update pages linking to this one as no longer broken
2392
		foreach($stageSelf->DependentPages() as $page) {
2393
			/** @var SiteTree $page */
2394
			$page->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\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...
2395
		}
2396
	}
2397
2398
	/**
2399
	 * Determine if this page references a parent which is archived, and not available in stage
2400
	 *
2401
	 * @return bool True if there is an archived parent
2402
	 */
2403
	protected function isParentArchived() {
2404
		if($parentID = $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2405
			/** @var SiteTree $parentPage */
2406
			$parentPage = Versioned::get_latest_version(self::class, $parentID);
2407
			if(!$parentPage || !$parentPage->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\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...
2408
				return true;
2409
			}
2410
		}
2411
		return false;
2412
	}
2413
2414
	/**
2415
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2416
	 *
2417
	 * @return self
2418
	 */
2419
	public function doRestoreToStage() {
2420
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2421
2422
		// Ensure that the parent page is restored, otherwise restore to root
2423
		if($this->isParentArchived()) {
2424
			$this->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2425
		}
2426
2427
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2428
		// create an empty record
2429
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2430
			$conn = DB::get_conn();
2431
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing(self::class, true);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\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...
2432
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2433
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing(self::class, false);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\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...
2434
		}
2435
2436
		$oldReadingMode = Versioned::get_reading_mode();
2437
		Versioned::set_stage(Versioned::DRAFT);
2438
		$this->forceChange();
2439
		$this->write();
2440
2441
		/** @var SiteTree $result */
2442
		$result = DataObject::get_by_id(self::class, $this->ID);
2443
2444
		// Need to update pages linking to this one as no longer broken
2445
		foreach($result->DependentPages(false) as $page) {
2446
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2447
			$page->write();
2448
		}
2449
2450
		Versioned::set_reading_mode($oldReadingMode);
2451
2452
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2453
2454
		return $result;
2455
	}
2456
2457
	/**
2458
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2459
	 *
2460
	 * @return bool
2461
	 */
2462
	public function isNew() {
2463
		/**
2464
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2465
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2466
		 */
2467
		if(empty($this->ID)) return true;
2468
2469
		if(is_numeric($this->ID)) return false;
2470
2471
		return stripos($this->ID, 'new') === 0;
2472
	}
2473
2474
	/**
2475
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2476
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2477
	 * {@link SiteTree::$needs_permission}.
2478
	 *
2479
	 * @return array
2480
	 */
2481
	protected function getClassDropdown() {
2482
		$classes = self::page_type_classes();
2483
		$currentClass = null;
2484
2485
		$result = array();
2486
		foreach($classes as $class) {
2487
			$instance = singleton($class);
2488
2489
			// if the current page type is this the same as the class type always show the page type in the list
2490
			if ($this->ClassName != $instance->ClassName) {
2491
				if($instance instanceof HiddenClass) continue;
2492
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2493
			}
2494
2495
			if($perms = $instance->stat('need_permission')) {
2496
				if(!$this->can($perms)) continue;
2497
			}
2498
2499
			$pageTypeName = $instance->i18n_singular_name();
2500
2501
			$currentClass = $class;
2502
			$result[$class] = $pageTypeName;
2503
2504
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2505
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2506
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2507
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2508
				$result[$class] = $result[$class] .  " ({$class})";
2509
			}
2510
		}
2511
2512
		// sort alphabetically, and put current on top
2513
		asort($result);
2514
		if($currentClass) {
2515
			$currentPageTypeName = $result[$currentClass];
2516
			unset($result[$currentClass]);
2517
			$result = array_reverse($result);
2518
			$result[$currentClass] = $currentPageTypeName;
2519
			$result = array_reverse($result);
2520
		}
2521
2522
		return $result;
2523
	}
2524
2525
	/**
2526
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2527
	 *
2528
	 * @return string[]
2529
	 */
2530
	public function allowedChildren() {
2531
		$allowedChildren = array();
2532
		$candidates = $this->stat('allowed_children');
2533
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2534
			foreach($candidates as $candidate) {
2535
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2536
				// Otherwise, the class and all its subclasses are allowed.
2537
				if(substr($candidate,0,1) == '*') {
2538
					$allowedChildren[] = substr($candidate,1);
2539
				} else {
2540
					$subclasses = ClassInfo::subclassesFor($candidate);
2541
					foreach($subclasses as $subclass) {
2542
						if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2543
							continue;
2544
						}
2545
						$allowedChildren[] = $subclass;
2546
					}
2547
				}
2548
			}
2549
		}
2550
2551
		return $allowedChildren;
2552
	}
2553
2554
	/**
2555
	 * Returns the class name of the default class for children of this page.
2556
	 *
2557
	 * @return string
2558
	 */
2559
	public function defaultChild() {
2560
		$default = $this->stat('default_child');
2561
		$allowed = $this->allowedChildren();
2562
		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...
2563
			if(!$default || !in_array($default, $allowed)) {
2564
				$default = reset($allowed);
2565
			}
2566
			return $default;
2567
		}
2568
		return null;
2569
	}
2570
2571
	/**
2572
	 * Returns the class name of the default class for the parent of this page.
2573
	 *
2574
	 * @return string
2575
	 */
2576
	public function defaultParent() {
2577
		return $this->stat('default_parent');
2578
	}
2579
2580
	/**
2581
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2582
	 * Title field.
2583
	 *
2584
	 * @return string
2585
	 */
2586
	public function getMenuTitle(){
2587
		if($value = $this->getField("MenuTitle")) {
2588
			return $value;
2589
		} else {
2590
			return $this->getField("Title");
2591
		}
2592
	}
2593
2594
2595
	/**
2596
	 * Set the menu title for this page.
2597
	 *
2598
	 * @param string $value
2599
	 */
2600
	public function setMenuTitle($value) {
2601
		if($value == $this->getField("Title")) {
2602
			$this->setField("MenuTitle", null);
2603
		} else {
2604
			$this->setField("MenuTitle", $value);
2605
		}
2606
	}
2607
2608
	/**
2609
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2610
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2611
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2612
	 * the flags.
2613
	 *
2614
	 * Example (simple):
2615
	 *   "deletedonlive" => "Deleted"
2616
	 *
2617
	 * Example (with optional title attribute):
2618
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2619
	 *
2620
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2621
	 * @return array
2622
	 */
2623
	public function getStatusFlags($cached = true) {
2624
		if(!$this->_cache_statusFlags || !$cached) {
2625
			$flags = array();
2626
			if($this->isOnLiveOnly()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnLiveOnly does not exist on object<SilverStripe\CMS\Model\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...
2627
				$flags['removedfromdraft'] = array(
2628
					'text' => _t('SiteTree.ONLIVEONLYSHORT', 'On live only'),
2629
					'title' => _t('SiteTree.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2630
				);
2631
			} elseif ($this->isArchived()) {
0 ignored issues
show
Documentation Bug introduced by
The method isArchived does not exist on object<SilverStripe\CMS\Model\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...
2632
				$flags['archived'] = array(
2633
					'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2634
					'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2635
				);
2636
			} else if($this->isOnDraftOnly()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraftOnly does not exist on object<SilverStripe\CMS\Model\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...
2637
				$flags['addedtodraft'] = array(
2638
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2639
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2640
				);
2641
			} else if($this->isModifiedOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isModifiedOnDraft does not exist on object<SilverStripe\CMS\Model\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...
2642
				$flags['modified'] = array(
2643
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2644
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2645
				);
2646
			}
2647
2648
			$this->extend('updateStatusFlags', $flags);
2649
2650
			$this->_cache_statusFlags = $flags;
2651
		}
2652
2653
		return $this->_cache_statusFlags;
2654
	}
2655
2656
	/**
2657
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2658
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2659
	 * publication status.
2660
	 *
2661
	 * @return string An HTML string ready to be directly used in a template
2662
	 */
2663
	public function getTreeTitle() {
2664
		// Build the list of candidate children
2665
		$children = array();
2666
		$candidates = static::page_type_classes();
2667
		foreach($this->allowedChildren() as $childClass) {
2668
			if(!in_array($childClass, $candidates)) continue;
2669
			$child = singleton($childClass);
2670
			if($child->canCreate(null, array('Parent' => $this))) {
2671
				$children[$childClass] = $child->i18n_singular_name();
2672
			}
2673
		}
2674
		$flags = $this->getStatusFlags();
2675
		$treeTitle = sprintf(
2676
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2677
			Convert::raw2att(Convert::raw2json($children)),
2678
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2679
		);
2680
		foreach($flags as $class => $data) {
2681
			if(is_string($data)) $data = array('text' => $data);
2682
			$treeTitle .= sprintf(
2683
				"<span class=\"badge %s\"%s>%s</span>",
2684
				'status-' . Convert::raw2xml($class),
2685
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2686
				Convert::raw2xml($data['text'])
2687
			);
2688
		}
2689
2690
		return $treeTitle;
2691
	}
2692
2693
	/**
2694
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2695
	 * we're currently inside, etc.
2696
	 *
2697
	 * @param int $level
2698
	 * @return SiteTree
2699
	 */
2700
	public function Level($level) {
2701
		$parent = $this;
2702
		$stack = array($parent);
2703
		while(($parent = $parent->Parent()) && $parent->exists()) {
2704
			array_unshift($stack, $parent);
2705
		}
2706
2707
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2708
	}
2709
2710
	/**
2711
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2712
	 *
2713
	 * @return int
2714
	 */
2715
	public function getPageLevel() {
2716
		if($this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2717
			return 1 + $this->Parent()->getPageLevel();
2718
		}
2719
		return 1;
2720
	}
2721
2722
	/**
2723
	 * Find the controller name by our convention of {$ModelClass}Controller
2724
	 *
2725
	 * @return string
2726
	 */
2727
	public function getControllerName() {
2728
		//default controller for SiteTree objects
2729
		$controller = ContentController::class;
2730
2731
		//go through the ancestry for this class looking for
2732
		$ancestry = ClassInfo::ancestry(static::class);
2733
		// loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2734
		while ($class = array_pop($ancestry)) {
2735
			//we don't need to go any deeper than the SiteTree class
2736
			if ($class == SiteTree::class) {
2737
				break;
2738
			}
2739
			// If we have a class of "{$ClassName}Controller" then we found our controller
2740
			if (class_exists($candidate = sprintf('%sController', $class))) {
2741
				$controller = $candidate;
2742
				break;
2743
			} elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2744
				// Support the legacy underscored filename, but raise a deprecation notice
2745
				Deprecation::notice(
2746
					'5.0',
2747
					'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2748
					Deprecation::SCOPE_GLOBAL
2749
				);
2750
				$controller = $candidate;
2751
				break;
2752
			}
2753
		}
2754
2755
		return $controller;
2756
	}
2757
2758
	/**
2759
	 * Return the CSS classes to apply to this node in the CMS tree.
2760
	 *
2761
	 * @param string $numChildrenMethod
2762
	 * @return string
2763
	 */
2764
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2765
		$classes = sprintf('class-%s', static::class);
2766
		if($this->HasBrokenFile || $this->HasBrokenLink) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenFile does not exist on object<SilverStripe\CMS\Model\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<SilverStripe\CMS\Model\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...
2767
			$classes .= " BrokenLink";
2768
		}
2769
2770
		if(!$this->canAddChildren()) {
2771
			$classes .= " nochildren";
2772
		}
2773
2774
		if(!$this->canEdit() && !$this->canAddChildren()) {
2775
			if (!$this->canView()) {
2776
				$classes .= " disabled";
2777
			} else {
2778
				$classes .= " edit-disabled";
2779
			}
2780
		}
2781
2782
		if(!$this->ShowInMenus) {
2783
			$classes .= " notinmenu";
2784
		}
2785
2786
		//TODO: Add integration
2787
		/*
2788
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2789
			$classes .= " untranslated ";
2790
		*/
2791
		$classes .= $this->markingClasses($numChildrenMethod);
0 ignored issues
show
Documentation Bug introduced by
The method markingClasses does not exist on object<SilverStripe\CMS\Model\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...
2792
2793
		return $classes;
2794
	}
2795
2796
	/**
2797
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2798
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2799
	 */
2800
	static public function disableCMSFieldsExtensions() {
2801
		self::$runCMSFieldsExtensions = false;
2802
	}
2803
2804
	/**
2805
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2806
	 * disableCMSFieldsExtensions().
2807
	 */
2808
	static public function enableCMSFieldsExtensions() {
2809
		self::$runCMSFieldsExtensions = true;
2810
	}
2811
2812
	public function providePermissions() {
2813
		return array(
2814
			'SITETREE_GRANT_ACCESS' => array(
2815
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2816
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2817
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2818
				'sort' => 100
2819
			),
2820
			'SITETREE_VIEW_ALL' => array(
2821
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2822
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2823
				'sort' => -100,
2824
				'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')
2825
			),
2826
			'SITETREE_EDIT_ALL' => array(
2827
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2828
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2829
				'sort' => -50,
2830
				'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')
2831
			),
2832
			'SITETREE_REORGANISE' => array(
2833
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2834
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2835
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2836
				'sort' => 100
2837
			),
2838
			'VIEW_DRAFT_CONTENT' => array(
2839
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2840
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2841
				'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.'),
2842
				'sort' => 100
2843
			)
2844
		);
2845
	}
2846
2847
	/**
2848
	 * Return the translated Singular name.
2849
	 *
2850
	 * @return string
2851
	 */
2852
	public function i18n_singular_name() {
2853
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2854
		/** @skipUpgrade */
2855
		// @todo When we namespace translations, change 'SiteTree' to FQN of the class
2856
		$class = (static::class == 'Page' || static::class === self::class)
2857
			? 'SiteTree'
2858
			: static::class;
2859
		return _t($class.'.SINGULARNAME', $this->singular_name());
2860
	}
2861
2862
	/**
2863
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2864
	 * picks it up for the wrong folder.
2865
	 *
2866
	 * @return array
2867
	 */
2868
	public function provideI18nEntities() {
2869
		$entities = parent::provideI18nEntities();
2870
2871
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2872
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2873
2874
		$entities[static::class . '.DESCRIPTION'] = array(
2875
			$this->stat('description'),
2876
			'Description of the page type (shown in the "add page" dialog)'
2877
		);
2878
2879
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2880
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2881
2882
		return $entities;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $entities; (array<*,null|array>) is incompatible with the return type of the parent method SilverStripe\ORM\DataObject::provideI18nEntities of type array<*,string[]>.

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...
2883
	}
2884
2885
	/**
2886
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2887
	 *
2888
	 * @return string
2889
	 */
2890
	public function getParentType() {
2891
		return $this->ParentID == 0 ? 'root' : 'subpage';
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\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...
2892
	}
2893
2894
	/**
2895
	 * Clear the permissions cache for SiteTree
2896
	 */
2897
	public static function reset() {
2898
		self::$cache_permissions = array();
2899
	}
2900
2901
	static public function on_db_reset() {
2902
		self::$cache_permissions = array();
2903
	}
2904
2905
}
2906