Completed
Push — master ( 36de85...554bbc )
by Daniel
10s
created

SiteTree::i18n_description()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
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\ContentNegotiator;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\RequestHandler;
17
use SilverStripe\Core\ClassInfo;
18
use SilverStripe\Core\Config\Config;
19
use SilverStripe\Core\Convert;
20
use SilverStripe\Dev\Deprecation;
21
use SilverStripe\Forms\CheckboxField;
22
use SilverStripe\Forms\CompositeField;
23
use SilverStripe\Forms\DropdownField;
24
use SilverStripe\Forms\FieldGroup;
25
use SilverStripe\Forms\FieldList;
26
use SilverStripe\Forms\FormAction;
27
use SilverStripe\Forms\FormField;
28
use SilverStripe\Forms\GridField\GridField;
29
use SilverStripe\Forms\GridField\GridFieldDataColumns;
30
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
31
use SilverStripe\Forms\ListboxField;
32
use SilverStripe\Forms\LiteralField;
33
use SilverStripe\Forms\OptionsetField;
34
use SilverStripe\Forms\Tab;
35
use SilverStripe\Forms\TabSet;
36
use SilverStripe\Forms\TextareaField;
37
use SilverStripe\Forms\TextField;
38
use SilverStripe\Forms\ToggleCompositeField;
39
use SilverStripe\Forms\TreeDropdownField;
40
use SilverStripe\i18n\i18n;
41
use SilverStripe\i18n\i18nEntityProvider;
42
use SilverStripe\ORM\ArrayList;
43
use SilverStripe\ORM\DataList;
44
use SilverStripe\ORM\DataObject;
45
use SilverStripe\ORM\DB;
46
use SilverStripe\ORM\HiddenClass;
47
use SilverStripe\ORM\Hierarchy\Hierarchy;
48
use SilverStripe\ORM\ManyManyList;
49
use SilverStripe\ORM\ValidationResult;
50
use SilverStripe\ORM\Versioning\Versioned;
51
use SilverStripe\Security\Group;
52
use SilverStripe\Security\Member;
53
use SilverStripe\Security\Permission;
54
use SilverStripe\Security\PermissionProvider;
55
use SilverStripe\SiteConfig\SiteConfig;
56
use SilverStripe\View\ArrayData;
57
use SilverStripe\View\Parsers\ShortcodeParser;
58
use SilverStripe\View\Parsers\URLSegmentFilter;
59
use SilverStripe\View\SSViewer;
60
use Subsite;
61
62
/**
63
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
64
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
65
 * draft and published states.
66
 *
67
 * <h2>URLs</h2>
68
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
69
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
70
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
71
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
72
 * {@link URLSegmentFilter::$default_allow_multibyte}.
73
 *
74
 * @property string URLSegment
75
 * @property string Title
76
 * @property string MenuTitle
77
 * @property string Content HTML content of the page.
78
 * @property string MetaDescription
79
 * @property string ExtraMeta
80
 * @property string ShowInMenus
81
 * @property string ShowInSearch
82
 * @property string Sort Integer value denoting the sort order.
83
 * @property string ReportClass
84
 * @property string CanViewType Type of restriction for viewing this object.
85
 * @property string CanEditType Type of restriction for editing this object.
86
 *
87
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
88
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
89
 * @method SiteTree Parent()
90
 *
91
 * @mixin Hierarchy
92
 * @mixin Versioned
93
 * @mixin SiteTreeLinkTracking
94
 */
95
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
96
97
	/**
98
	 * Indicates what kind of children this page type can have.
99
	 * This can be an array of allowed child classes, or the string "none" -
100
	 * indicating that this page type can't have children.
101
	 * If a classname is prefixed by "*", such as "*Page", then only that
102
	 * class is allowed - no subclasses. Otherwise, the class and all its
103
	 * subclasses are allowed.
104
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
105
	 *
106
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
107
	 *
108
	 * @config
109
	 * @var array
110
	 */
111
	private static $allowed_children = [
112
		self::class
113
	];
114
115
	/**
116
	 * The default child class for this page.
117
	 * Note: Value might be cached, see {@link $allowed_chilren}.
118
	 *
119
	 * @config
120
	 * @var string
121
	 */
122
	private static $default_child = "Page";
123
124
	/**
125
	 * Default value for SiteTree.ClassName enum
126
	 * {@see DBClassName::getDefault}
127
	 *
128
	 * @config
129
	 * @var string
130
	 */
131
	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...
132
133
	/**
134
	 * The default parent class for this page.
135
	 * Note: Value might be cached, see {@link $allowed_chilren}.
136
	 *
137
	 * @config
138
	 * @var string
139
	 */
140
	private static $default_parent = null;
141
142
	/**
143
	 * Controls whether a page can be in the root of the site tree.
144
	 * Note: Value might be cached, see {@link $allowed_chilren}.
145
	 *
146
	 * @config
147
	 * @var bool
148
	 */
149
	private static $can_be_root = true;
150
151
	/**
152
	 * List of permission codes a user can have to allow a user to create a page of this type.
153
	 * Note: Value might be cached, see {@link $allowed_chilren}.
154
	 *
155
	 * @config
156
	 * @var array
157
	 */
158
	private static $need_permission = null;
159
160
	/**
161
	 * If you extend a class, and don't want to be able to select the old class
162
	 * in the cms, set this to the old class name. Eg, if you extended Product
163
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
164
	 *
165
	 * @config
166
	 * @var string
167
	 */
168
	private static $hide_ancestor = null;
169
170
	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...
171
		"URLSegment" => "Varchar(255)",
172
		"Title" => "Varchar(255)",
173
		"MenuTitle" => "Varchar(100)",
174
		"Content" => "HTMLText",
175
		"MetaDescription" => "Text",
176
		"ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
177
		"ShowInMenus" => "Boolean",
178
		"ShowInSearch" => "Boolean",
179
		"Sort" => "Int",
180
		"HasBrokenFile" => "Boolean",
181
		"HasBrokenLink" => "Boolean",
182
		"ReportClass" => "Varchar",
183
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
184
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
185
	);
186
187
	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...
188
		"URLSegment" => true,
189
	);
190
191
	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...
192
		"ViewerGroups" => Group::class,
193
		"EditorGroups" => Group::class,
194
	);
195
196
	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...
197
		"VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
198
	);
199
200
	private static $owned_by = array(
201
		"VirtualPages"
202
	);
203
204
	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...
205
		"Breadcrumbs" => "HTMLFragment",
206
		"LastEdited" => "Datetime",
207
		"Created" => "Datetime",
208
		'Link' => 'Text',
209
		'RelativeLink' => 'Text',
210
		'AbsoluteLink' => 'Text',
211
		'CMSEditLink' => 'Text',
212
		'TreeTitle' => 'HTMLFragment',
213
		'MetaTags' => 'HTMLFragment',
214
	);
215
216
	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...
217
		"ShowInMenus" => 1,
218
		"ShowInSearch" => 1,
219
		"CanViewType" => "Inherit",
220
		"CanEditType" => "Inherit"
221
	);
222
223
	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...
224
225
	private static $versioning = array(
226
		"Stage",  "Live"
227
	);
228
229
	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...
230
231
	/**
232
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
233
	 * @var boolean
234
	 * @config
235
	 */
236
	private static $can_create = true;
237
238
	/**
239
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
240
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
241
	 *
242
	 * @see CMSMain::generateTreeStylingCSS()
243
	 * @config
244
	 * @var string
245
	 */
246
	private static $icon = null;
247
248
	private static $extensions = [
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...
249
		Hierarchy::class,
250
		Versioned::class,
251
		SiteTreeLinkTracking::class,
252
	];
253
254
	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...
255
		'Title',
256
		'Content',
257
	);
258
259
	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...
260
		'URLSegment' => 'URL'
261
	);
262
263
	/**
264
	 * @config
265
	 */
266
	private static $nested_urls = true;
267
268
	/**
269
	 * @config
270
	*/
271
	private static $create_default_pages = true;
272
273
	/**
274
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
275
	 */
276
	private static $runCMSFieldsExtensions = true;
277
278
	/**
279
	 * Cache for canView/Edit/Publish/Delete permissions.
280
	 * Keyed by permission type (e.g. 'edit'), with an array
281
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
282
	 * See {@link batch_permission_check()} for details.
283
	 */
284
	private static $cache_permissions = array();
285
286
	/**
287
	 * @config
288
	 * @var boolean
289
	 */
290
	private static $enforce_strict_hierarchy = true;
291
292
	/**
293
	 * The value used for the meta generator tag. Leave blank to omit the tag.
294
	 *
295
	 * @config
296
	 * @var string
297
	 */
298
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
299
300
	protected $_cache_statusFlags = null;
301
302
	/**
303
	 * Plural form for SiteTree / Page classes. Not inherited by subclasses.
304
	 *
305
	 * @config
306
	 * @var string
307
	 */
308
	private static $base_plural_name = 'Pages';
309
310
	/**
311
	 * Plural form for SiteTree / Page classes. Not inherited by subclasses.
312
	 *
313
	 * @config
314
	 * @var string
315
	 */
316
	private static $base_singular_name = 'Page';
317
318
	/**
319
	 * Description of the class functionality, typically shown to a user
320
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
321
	 *
322
	 * @see SiteTree::description()
323
	 * @see SiteTree::i18n_description()
324
	 *
325
	 * @config
326
	 * @var string
327
	 */
328
	private static $description = null;
329
330
	/**
331
	 * Description for Page and SiteTree classes, but not inherited by subclasses.
332
	 * override SiteTree::$description in subclasses instead.
333
	 *
334
	 * @see SiteTree::description()
335
	 * @see SiteTree::i18n_description()
336
	 *
337
	 * @config
338
	 * @var string
339
	 */
340
	private static $base_description = 'Generic content page';
341
342
	/**
343
	 * Fetches the {@link SiteTree} object that maps to a link.
344
	 *
345
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
346
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
347
	 *
348
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
349
	 * by a extension attached to {@link SiteTree}
350
	 *
351
	 * @param string $link  The link of the page to search for
352
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
353
	 * @return SiteTree
354
	 */
355
	public static function get_by_link($link, $cache = true) {
356
		if(trim($link, '/')) {
357
			$link = trim(Director::makeRelative($link), '/');
358
		} else {
359
			$link = RootURLController::get_homepage_link();
360
		}
361
362
		$parts = preg_split('|/+|', $link);
363
364
		// Grab the initial root level page to traverse down from.
365
		$URLSegment = array_shift($parts);
366
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
367
		if(self::config()->nested_urls) {
368
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
369
		}
370
		/** @var SiteTree $sitetree */
371
		$sitetree = DataObject::get_one(self::class, $conditions, $cache);
372
373
		/// Fall back on a unique URLSegment for b/c.
374
		if(	!$sitetree
375
			&& self::config()->nested_urls
376
			&& $sitetree = DataObject::get_one(self::class, array(
377
				'"SiteTree"."URLSegment"' => $URLSegment
378
			), $cache)
379
		) {
380
			return $sitetree;
381
		}
382
383
		// Attempt to grab an alternative page from extensions.
384
		if(!$sitetree) {
385
			$parentID = self::config()->nested_urls ? 0 : null;
386
387 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...
388
				foreach($alternatives as $alternative) {
389
					if($alternative) {
390
						$sitetree = $alternative;
391
			}
392
				}
393
			}
394
395
			if(!$sitetree) {
396
				return null;
397
		}
398
		}
399
400
		// Check if we have any more URL parts to parse.
401
		if(!self::config()->nested_urls || !count($parts)) {
402
			return $sitetree;
403
		}
404
405
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
406
		foreach($parts as $segment) {
407
			$next = DataObject::get_one(self::class, array(
408
					'"SiteTree"."URLSegment"' => $segment,
409
					'"SiteTree"."ParentID"' => $sitetree->ID
410
				),
411
				$cache
412
			);
413
414
			if(!$next) {
415
				$parentID = (int) $sitetree->ID;
416
417 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...
418
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
419
				}
420
421
				if(!$next) {
422
					return null;
423
				}
424
			}
425
426
			$sitetree->destroy();
427
			$sitetree = $next;
428
		}
429
430
		return $sitetree;
431
	}
432
433
	/**
434
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
435
	 *
436
	 * @return array
437
	 */
438
	public static function page_type_classes() {
439
		$classes = ClassInfo::getValidSubClasses();
440
441
		$baseClassIndex = array_search(self::class, $classes);
442
		if($baseClassIndex !== false) {
443
			unset($classes[$baseClassIndex]);
444
		}
445
446
		$kill_ancestors = array();
447
448
		// figure out if there are any classes we don't want to appear
449
		foreach($classes as $class) {
450
			$instance = singleton($class);
451
452
			// do any of the progeny want to hide an ancestor?
453
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
454
				// note for killing later
455
				$kill_ancestors[] = $ancestor_to_hide;
456
			}
457
		}
458
459
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
460
		// requirements
461
		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...
462
			$kill_ancestors = array_unique($kill_ancestors);
463
			foreach($kill_ancestors as $mark) {
464
				// unset from $classes
465
				$idx = array_search($mark, $classes, true);
466
				if ($idx !== false) {
467
					unset($classes[$idx]);
468
				}
469
			}
470
		}
471
472
		return $classes;
473
	}
474
475
	/**
476
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
477
	 *
478
	 * @param array      $arguments
479
	 * @param string     $content
480
	 * @param ShortcodeParser $parser
481
	 * @return string
482
	 */
483
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
484
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) {
485
			return null;
486
		}
487
488
		/** @var SiteTree $page */
489
		if (
490
			   !($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
491
			&& !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
492
		) {
493
			 return null; // There were no suitable matches at all.
494
		}
495
496
		/** @var SiteTree $page */
497
		$link = Convert::raw2att($page->Link());
498
499
		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...
500
			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...
501
		} else {
502
			return $link;
503
		}
504
	}
505
506
	/**
507
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
508
	 *
509
	 * @param string $action Optional controller action (method).
510
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
511
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
512
	 *                       append GET parameters.
513
	 * @return string
514
	 */
515
	public function Link($action = null) {
516
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
517
	}
518
519
	/**
520
	 * Get the absolute URL for this page, including protocol and host.
521
	 *
522
	 * @param string $action See {@link Link()}
523
	 * @return string
524
	 */
525
	public function AbsoluteLink($action = null) {
526
		if($this->hasMethod('alternateAbsoluteLink')) {
527
			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...
528
		} else {
529
			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 529 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...
530
		}
531
	}
532
533
	/**
534
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
535
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
536
	 *
537
	 * @param string $action See {@link Link()}
538
	 * @return string
539
	 */
540
	public function PreviewLink($action = null) {
541
		if($this->hasMethod('alternatePreviewLink')) {
542
			Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
543
			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...
544
		}
545
546
		$link = $this->AbsoluteLink($action);
547
		$this->extend('updatePreviewLink', $link, $action);
548
		return $link;
549
	}
550
551
	public function getMimeType() {
552
		return 'text/html';
553
	}
554
555
	/**
556
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
557
	 *
558
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
559
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
560
	 * and returned in its full form.
561
	 *
562
	 * @uses RootURLController::get_homepage_link()
563
	 *
564
	 * @param string $action See {@link Link()}
565
	 * @return string
566
	 */
567
	public function RelativeLink($action = null) {
568
		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...
569
			$parent = $this->Parent();
570
			// If page is removed select parent from version history (for archive page view)
571
			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...
572
				$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...
573
			}
574
			$base = $parent->RelativeLink($this->URLSegment);
575
		} 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...
576
			// Unset base for root-level homepages.
577
			// Note: Homepages with action parameters (or $action === true)
578
			// need to retain their URLSegment.
579
			$base = null;
580
		} else {
581
			$base = $this->URLSegment;
582
		}
583
584
		$this->extend('updateRelativeLink', $base, $action);
585
586
		// Legacy support: If $action === true, retain URLSegment for homepages,
587
		// but don't append any action
588
		if($action === true) $action = null;
589
590
		return Controller::join_links($base, '/', $action);
591
	}
592
593
	/**
594
	 * Get the absolute URL for this page on the Live site.
595
	 *
596
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
597
	 * @return string
598
	 */
599
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
600
		$oldReadingMode = Versioned::get_reading_mode();
601
		Versioned::set_stage(Versioned::LIVE);
602
		/** @var SiteTree $live */
603
		$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...
604
			'"SiteTree"."ID"' => $this->ID
605
		));
606
		if($live) {
607
			$link = $live->AbsoluteLink();
608
			if($includeStageEqualsLive) {
609
				$link = Controller::join_links($link, '?stage=Live');
610
			}
611
		} else {
612
			$link = null;
613
		}
614
615
		Versioned::set_reading_mode($oldReadingMode);
616
		return $link;
617
	}
618
619
	/**
620
	 * Generates a link to edit this page in the CMS.
621
	 *
622
	 * @return string
623
	 */
624
	public function CMSEditLink() {
625
		$link = Controller::join_links(
626
			CMSPageEditController::singleton()->Link('show'),
627
			$this->ID
628
		);
629
		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 629 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...
630
	}
631
632
633
	/**
634
	 * Return a CSS identifier generated from this page's link.
635
	 *
636
	 * @return string The URL segment
637
	 */
638
	public function ElementName() {
639
		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...
640
	}
641
642
	/**
643
	 * Returns true if this is the currently active page being used to handle this request.
644
	 *
645
	 * @return bool
646
	 */
647
	public function isCurrent() {
648
		$currentPage = Director::get_current_page();
649
		if ($currentPage instanceof ContentController) {
650
			$currentPage = $currentPage->data();
651
	}
652
		if($currentPage instanceof SiteTree) {
653
			return $currentPage === $this || $currentPage->ID === $this->ID;
654
		}
655
		return false;
656
	}
657
658
	/**
659
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
660
	 * currently being viewed).
661
	 *
662
	 * @return bool
663
	 */
664
	public function isSection() {
665
		return $this->isCurrent() || (
666
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
667
		);
668
	}
669
670
	/**
671
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
672
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
673
	 * to external users.
674
	 *
675
	 * @return bool
676
	 */
677
	public function isOrphaned() {
678
		// Always false for root pages
679
		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...
680
			return false;
681
		}
682
683
		// Parent must exist and not be an orphan itself
684
		$parent = $this->Parent();
685
		return !$parent || !$parent->exists() || $parent->isOrphaned();
686
	}
687
688
	/**
689
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
690
	 *
691
	 * @return string
692
	 */
693
	public function LinkOrCurrent() {
694
		return $this->isCurrent() ? 'current' : 'link';
695
	}
696
697
	/**
698
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
699
	 *
700
	 * @return string
701
	 */
702
	public function LinkOrSection() {
703
		return $this->isSection() ? 'section' : 'link';
704
	}
705
706
	/**
707
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
708
	 * but in the current section.
709
	 *
710
	 * @return string
711
	 */
712
	public function LinkingMode() {
713
		if($this->isCurrent()) {
714
			return 'current';
715
		} elseif($this->isSection()) {
716
			return 'section';
717
		} else {
718
			return 'link';
719
		}
720
	}
721
722
	/**
723
	 * Check if this page is in the given current section.
724
	 *
725
	 * @param string $sectionName Name of the section to check
726
	 * @return bool True if we are in the given section
727
	 */
728
	public function InSection($sectionName) {
729
		$page = Director::get_current_page();
730
		while($page && $page->exists()) {
731
			if($sectionName == $page->URLSegment) {
732
				return true;
733
			}
734
			$page = $page->Parent();
735
		}
736
		return false;
737
	}
738
739
	/**
740
	 * Reset Sort on duped page
741
	 *
742
	 * @param SiteTree $original
743
	 * @param bool $doWrite
744
	 */
745
	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...
746
		$this->Sort = 0;
747
	}
748
749
	/**
750
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
751
	 *
752
	 * @return static The duplicated object
753
	 */
754
	public function duplicateWithChildren() {
755
		/** @var SiteTree $clone */
756
		$clone = $this->duplicate();
757
		$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...
758
759
		if($children) {
760
			/** @var SiteTree $child */
761
			$sort = 0;
762
			foreach($children as $child) {
763
				$childClone = $child->duplicateWithChildren();
764
				$childClone->ParentID = $clone->ID;
765
				//retain sort order by manually setting sort values
766
				$childClone->Sort = ++$sort;
767
				$childClone->write();
768
			}
769
		}
770
771
		return $clone;
772
	}
773
774
	/**
775
	 * Duplicate this node and its children as a child of the node with the given ID
776
	 *
777
	 * @param int $id ID of the new node's new parent
778
	 */
779
	public function duplicateAsChild($id) {
780
		/** @var SiteTree $newSiteTree */
781
		$newSiteTree = $this->duplicate();
782
		$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...
783
		$newSiteTree->Sort = 0;
784
		$newSiteTree->write();
785
	}
786
787
	/**
788
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
789
	 *
790
	 * @param int $maxDepth The maximum depth to traverse.
791
	 * @param boolean $unlinked Whether to link page titles.
792
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
793
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
794
	 * @return string The breadcrumb trail.
795
	 */
796
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
797
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
798
		$template = new SSViewer('BreadcrumbsTemplate');
799
		return $template->process($this->customise(new ArrayData(array(
800
			"Pages" => $pages,
801
			"Unlinked" => $unlinked
802
		))));
803
	}
804
805
806
	/**
807
	 * Returns a list of breadcrumbs for the current page.
808
	 *
809
	 * @param int $maxDepth The maximum depth to traverse.
810
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
811
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
812
	 *
813
	 * @return ArrayList
814
	*/
815
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
816
		$page = $this;
817
		$pages = array();
818
819
		while(
820
			$page
821
			&& $page->exists()
822
 			&& (!$maxDepth || count($pages) < $maxDepth)
823
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
824
 		) {
825
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
826
				$pages[] = $page;
827
			}
828
829
			$page = $page->Parent();
830
		}
831
832
		return new ArrayList(array_reverse($pages));
833
	}
834
835
836
	/**
837
	 * Make this page a child of another page.
838
	 *
839
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
840
	 *
841
	 * @param SiteTree|int $item Either the parent object, or the parent ID
842
	 */
843
	public function setParent($item) {
844
		if(is_object($item)) {
845
			if (!$item->exists()) $item->write();
846
			$this->setField("ParentID", $item->ID);
847
		} else {
848
			$this->setField("ParentID", $item);
849
		}
850
	}
851
852
	/**
853
	 * Get the parent of this page.
854
	 *
855
	 * @return SiteTree Parent of this page
856
	 */
857
	public function getParent() {
858
		if ($parentID = $this->getField("ParentID")) {
859
			return DataObject::get_by_id(self::class, $parentID);
860
		}
861
		return null;
862
	}
863
864
	/**
865
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
866
	 *
867
	 * @param int $level The maximum amount of levels to traverse.
868
	 * @param string $separator Seperating string
869
	 * @return string The resulting string
870
	 */
871
	public function NestedTitle($level = 2, $separator = " - ") {
872
		$item = $this;
873
		$parts = [];
874
		while($item && $level > 0) {
875
			$parts[] = $item->Title;
876
			$item = $item->getParent();
877
			$level--;
878
		}
879
		return implode($separator, array_reverse($parts));
880
	}
881
882
	/**
883
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
884
	 * the security model for an application.
885
	 *
886
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
887
	 * - Checks for existence of a method named "can<$perm>()" on the object
888
	 * - Calls decorators and only returns for FALSE "vetoes"
889
	 * - Falls back to {@link Permission::check()}
890
	 * - Does NOT check for many-many relations named "Can<$perm>"
891
	 *
892
	 * @uses DataObjectDecorator->can()
893
	 *
894
	 * @param string $perm The permission to be checked, such as 'View'
895
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
896
	 * @param array $context Context argument for canCreate()
897
	 * @return bool True if the the member is allowed to do the given action
898
	 */
899
	public function can($perm, $member = null, $context = array()) {
900 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...
901
			$member = Member::currentUserID();
902
		}
903
904
		if($member && Permission::checkMember($member, "ADMIN")) return true;
905
906
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
907
			$method = 'can' . ucfirst($perm);
908
			return $this->$method($member);
909
		}
910
911
		$results = $this->extend('can', $member);
912
		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...
913
914
		return ($member && Permission::checkMember($member, $perm));
915
	}
916
917
	/**
918
	 * This function should return true if the current user can add children to this page. It can be overloaded to
919
	 * customise the security model for an application.
920
	 *
921
	 * Denies permission if any of the following conditions is true:
922
	 * - alternateCanAddChildren() on a extension returns false
923
	 * - canEdit() is not granted
924
	 * - There are no classes defined in {@link $allowed_children}
925
	 *
926
	 * @uses SiteTreeExtension->canAddChildren()
927
	 * @uses canEdit()
928
	 * @uses $allowed_children
929
	 *
930
	 * @param Member|int $member
931
	 * @return bool True if the current user can add children
932
	 */
933
	public function canAddChildren($member = null) {
934
		// Disable adding children to archived pages
935
		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...
936
			return false;
937
		}
938
939 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...
940
			$member = Member::currentUserID();
941
		}
942
943
		// Standard mechanism for accepting permission changes from extensions
944
		$extended = $this->extendedCan('canAddChildren', $member);
945
		if($extended !== null) {
946
			return $extended;
947
		}
948
949
		// Default permissions
950
		if($member && Permission::checkMember($member, "ADMIN")) {
951
			return true;
952
		}
953
954
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
955
	}
956
957
	/**
958
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
959
	 * security model for an application.
960
	 *
961
	 * Denies permission if any of the following conditions is true:
962
	 * - canView() on any extension returns false
963
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
964
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
965
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
966
	 *
967
	 * @uses DataExtension->canView()
968
	 * @uses ViewerGroups()
969
	 *
970
	 * @param Member|int $member
971
	 * @return bool True if the current user can view this page
972
	 */
973
	public function canView($member = null) {
974 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...
975
			$member = Member::currentUserID();
976
		}
977
978
		// Standard mechanism for accepting permission changes from extensions
979
		$extended = $this->extendedCan('canView', $member);
980
		if($extended !== null) {
981
			return $extended;
982
		}
983
984
		// admin override
985
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
986
			return true;
987
		}
988
989
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
990
		if($this->isOrphaned()) {
991
			return false;
992
		}
993
994
		// check for empty spec
995
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') {
996
			return true;
997
		}
998
999
		// check for inherit
1000
		if($this->CanViewType == 'Inherit') {
1001
			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...
1002
			else return $this->getSiteConfig()->canViewPages($member);
1003
		}
1004
1005
		// check for any logged-in users
1006
		if($this->CanViewType == 'LoggedInUsers' && $member) {
1007
			return true;
1008
		}
1009
1010
		// check for specific groups
1011
		if($member && is_numeric($member)) {
1012
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
1013
		}
1014
		if(
1015
			$this->CanViewType == 'OnlyTheseUsers'
1016
			&& $member
1017
			&& $member->inGroups($this->ViewerGroups())
1018
		) return true;
1019
1020
		return false;
1021
	}
1022
1023
	/**
1024
	 * Check if this page can be published
1025
	 *
1026
	 * @param Member $member
1027
	 * @return bool
1028
	 */
1029
	public function canPublish($member = null) {
1030
		if(!$member) {
1031
			$member = Member::currentUser();
1032
		}
1033
1034
		// Check extension
1035
		$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...
1036
		if($extended !== null) {
1037
			return $extended;
1038
		}
1039
1040
		if(Permission::checkMember($member, "ADMIN")) {
1041
			return true;
1042
		}
1043
1044
		// Default to relying on edit permission
1045
		return $this->canEdit($member);
1046
	}
1047
1048
	/**
1049
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
1050
	 * security model for an application.
1051
	 *
1052
	 * Denies permission if any of the following conditions is true:
1053
	 * - canDelete() returns false on any extension
1054
	 * - canEdit() returns false
1055
	 * - any descendant page returns false for canDelete()
1056
	 *
1057
	 * @uses canDelete()
1058
	 * @uses SiteTreeExtension->canDelete()
1059
	 * @uses canEdit()
1060
	 *
1061
	 * @param Member $member
1062
	 * @return bool True if the current user can delete this page
1063
	 */
1064
	public function canDelete($member = null) {
1065 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...
1066
		else if(is_numeric($member)) $memberID = $member;
1067
		else $memberID = Member::currentUserID();
1068
1069
		// Standard mechanism for accepting permission changes from extensions
1070
		$extended = $this->extendedCan('canDelete', $memberID);
1071
		if($extended !== null) {
1072
			return $extended;
1073
		}
1074
1075
		// Default permission check
1076
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1077
			return true;
1078
		}
1079
1080
		// Regular canEdit logic is handled by can_edit_multiple
1081
		$results = self::can_delete_multiple(array($this->ID), $memberID);
1082
1083
		// If this page no longer exists in stage/live results won't contain the page.
1084
		// Fail-over to false
1085
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
1086
	}
1087
1088
	/**
1089
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
1090
	 * can be overloaded to customise the security model for an application.
1091
	 *
1092
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1093
	 * create beneath a parent is based on the ability to edit that parent page.
1094
	 *
1095
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1096
	 *
1097
	 * @uses $can_create
1098
	 * @uses DataExtension->canCreate()
1099
	 *
1100
	 * @param Member $member
1101
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
1102
	 *                       If a parent page is known, it will be checked for validity.
1103
	 *                       If omitted, it will be assumed this is to be created as a top level page.
1104
	 * @return bool True if the current user can create pages on this class.
1105
	 */
1106
	public function canCreate($member = null, $context = array()) {
1107 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...
1108
			$member = Member::currentUserID();
1109
		}
1110
1111
		// Check parent (custom canCreate option for SiteTree)
1112
		// Block children not allowed for this parent type
1113
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1114
		if($parent && !in_array(static::class, $parent->allowedChildren())) {
1115
			return false;
1116
		}
1117
1118
		// Standard mechanism for accepting permission changes from extensions
1119
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1120
		if($extended !== null) {
1121
			return $extended;
1122
		}
1123
1124
		// Check permission
1125
		if($member && Permission::checkMember($member, "ADMIN")) {
1126
			return true;
1127
		}
1128
1129
		// Fall over to inherited permissions
1130
		if($parent && $parent->exists()) {
1131
			return $parent->canAddChildren($member);
1132
		} else {
1133
			// This doesn't necessarily mean we are creating a root page, but that
1134
			// we don't know if there is a parent, so default to this permission
1135
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1136
		}
1137
	}
1138
1139
	/**
1140
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1141
	 * security model for an application.
1142
	 *
1143
	 * Denies permission if any of the following conditions is true:
1144
	 * - canEdit() on any extension returns false
1145
	 * - canView() return false
1146
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1147
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1148
	 *   CMS_Access_CMSMAIN permission code
1149
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1150
	 *
1151
	 * @uses canView()
1152
	 * @uses EditorGroups()
1153
	 * @uses DataExtension->canEdit()
1154
	 *
1155
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1156
	 *                       unit tests)
1157
	 * @return bool True if the current user can edit this page
1158
	 */
1159
	public function canEdit($member = null) {
1160 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...
1161
		else if(is_numeric($member)) $memberID = $member;
1162
		else $memberID = Member::currentUserID();
1163
1164
		// Standard mechanism for accepting permission changes from extensions
1165
		$extended = $this->extendedCan('canEdit', $memberID);
1166
		if($extended !== null) {
1167
			return $extended;
1168
		}
1169
1170
		// Default permissions
1171
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1172
			return true;
1173
		}
1174
1175
		if($this->ID) {
1176
			// Regular canEdit logic is handled by can_edit_multiple
1177
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1178
1179
			// If this page no longer exists in stage/live results won't contain the page.
1180
			// Fail-over to false
1181
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1182
1183
		// Default for unsaved pages
1184
		} else {
1185
			return $this->getSiteConfig()->canEditPages($member);
1186
		}
1187
	}
1188
1189
	/**
1190
	 * Stub method to get the site config, unless the current class can provide an alternate.
1191
	 *
1192
	 * @return SiteConfig
1193
	 */
1194
	public function getSiteConfig() {
1195
		$configs = $this->invokeWithExtensions('alternateSiteConfig');
1196
		foreach(array_filter($configs) as $config) {
1197
			return $config;
1198
		}
1199
1200
		return SiteConfig::current_site_config();
1201
	}
1202
1203
	/**
1204
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1205
	 * can_(perm)_multiple method for efficiency.
1206
	 *
1207
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1208
	 * @param array           $ids           An array of page IDs
1209
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1210
	 *                                       to 'SiteTree::can_(permission)_multiple'
1211
	 */
1212
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1213
		if(!$batchCallback) {
1214
			$batchCallback = self::class . "::can_{$permission}_multiple";
1215
		}
1216
1217
		if(is_callable($batchCallback)) {
1218
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1219
		} else {
1220
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1221
				. "with callback '$batchCallback'", E_USER_WARNING);
1222
		}
1223
	}
1224
1225
	/**
1226
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1227
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1228
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1229
	 * efficiently.
1230
	 *
1231
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1232
	 * property to FALSE.
1233
	 *
1234
	 * @param array  $ids              Of {@link SiteTree} IDs
1235
	 * @param int    $memberID         Member ID
1236
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1237
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1238
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1239
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1240
	 * @param bool   $useCached
1241
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1242
	 */
1243
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1244
												  $globalPermission = null, $useCached = true) {
1245
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1246
1247
		// Sanitise the IDs
1248
		$ids = array_filter($ids, 'is_numeric');
1249
1250
		// This is the name used on the permission cache
1251
		// converts something like 'CanEditType' to 'edit'.
1252
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1253
1254
		// Default result: nothing editable
1255
		$result = array_fill_keys($ids, false);
1256
		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...
1257
1258
			// Look in the cache for values
1259
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1260
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1261
1262
				// If we can't find everything in the cache, then look up the remainder separately
1263
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1264
				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...
1265
					$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 1245 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...
1266
				}
1267
				return $cachedValues;
1268
			}
1269
1270
			// If a member doesn't have a certain permission then they can't edit anything
1271
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1272
				return $result;
1273
			}
1274
1275
			// Placeholder for parameterised ID list
1276
			$idPlaceholders = DB::placeholders($ids);
1277
1278
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1279
			// be enabled
1280
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1281
1282
			// Get the groups that the given member belongs to
1283
			/** @var Member $member */
1284
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1285
			$groupIDs = $member->Groups()->column("ID");
1286
			$SQL_groupList = implode(", ", $groupIDs);
1287
			if (!$SQL_groupList) {
1288
				$SQL_groupList = '0';
1289
			}
1290
1291
			$combinedStageResult = array();
1292
1293
			foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1294
				// Start by filling the array with the pages that actually exist
1295
				/** @skipUpgrade */
1296
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1297
1298
				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...
1299
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1300
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1301
				} else {
1302
					$stageIds = array();
1303
				}
1304
				$result = array_fill_keys($stageIds, false);
1305
1306
				// Get the uninherited permissions
1307
				$uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1308
					->where(array(
1309
						"(\"$typeField\" = 'LoggedInUsers' OR
1310
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1311
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1312
						=> $ids
1313
					))
1314
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1315
1316
				if($uninheritedPermissions) {
1317
					// Set all the relevant items in $result to true
1318
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1319
				}
1320
1321
				// Get permissions that are inherited
1322
				$potentiallyInherited = Versioned::get_by_stage(
1323
					"SilverStripe\\CMS\\Model\\SiteTree",
1324
					$stage,
1325
					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...
1326
				);
1327
1328
				if($potentiallyInherited) {
1329
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1330
					// then see which ones the user has permission on
1331
					$groupedByParent = array();
1332
					foreach($potentiallyInherited as $item) {
1333
						/** @var SiteTree $item */
1334
						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...
1335
							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...
1336
							$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...
1337
						} else {
1338
							// Might return different site config based on record context, e.g. when subsites module
1339
							// is used
1340
							$siteConfig = $item->getSiteConfig();
1341
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1342
						}
1343
					}
1344
1345
					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...
1346
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1347
						if($actuallyInherited) {
1348
							$parentIDs = array_keys(array_filter($actuallyInherited));
1349
							foreach($parentIDs as $parentID) {
1350
								// Set all the relevant items in $result to true
1351
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1352
							}
1353
						}
1354
					}
1355
				}
1356
1357
				$combinedStageResult = $combinedStageResult + $result;
1358
1359
			}
1360
		}
1361
1362
		if(isset($combinedStageResult)) {
1363
			// Cache the results
1364
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1365
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1366
			return $combinedStageResult;
1367
		} else {
1368
			return array();
1369
		}
1370
	}
1371
1372
	/**
1373
	 * Get the 'can edit' information for a number of SiteTree pages.
1374
	 *
1375
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1376
	 * @param int   $memberID  ID of member
1377
	 * @param bool  $useCached Return values from the permission cache if they exist
1378
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1379
	 *                         edited
1380
	 */
1381
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1382
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1383
	}
1384
1385
	/**
1386
	 * Get the 'can edit' information for a number of SiteTree pages.
1387
	 *
1388
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1389
	 * @param int   $memberID  ID of member
1390
	 * @param bool  $useCached Return values from the permission cache if they exist
1391
	 * @return array
1392
	 */
1393
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1394
		$deletable = array();
1395
		$result = array_fill_keys($ids, false);
1396
		$cacheKey = "delete-$memberID";
1397
1398
		// Look in the cache for values
1399
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1400
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1401
1402
			// If we can't find everything in the cache, then look up the remainder separately
1403
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1404
			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...
1405
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1406
					+ $cachedValues;
1407
			}
1408
			return $cachedValues;
1409
		}
1410
1411
		// You can only delete pages that you can edit
1412
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1413
		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...
1414
1415
			// You can only delete pages whose children you can delete
1416
			$editablePlaceholders = DB::placeholders($editableIDs);
1417
			$childRecords = SiteTree::get()->where(array(
1418
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1419
			));
1420
			if($childRecords) {
1421
				$children = $childRecords->map("ID", "ParentID");
1422
1423
				// Find out the children that can be deleted
1424
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1425
1426
				// Get a list of all the parents that have no undeletable children
1427
				$deletableParents = array_fill_keys($editableIDs, true);
1428
				foreach($deletableChildren as $id => $canDelete) {
1429
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1430
				}
1431
1432
				// Use that to filter the list of deletable parents that have children
1433
				$deletableParents = array_keys($deletableParents);
1434
1435
				// Also get the $ids that don't have children
1436
				$parents = array_unique($children->values());
1437
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1438
1439
				// Combine the two
1440
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1441
1442
			} else {
1443
				$deletable = $editableIDs;
1444
			}
1445
		}
1446
1447
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1448
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1449
	}
1450
1451
	/**
1452
	 * Collate selected descendants of this page.
1453
	 *
1454
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1455
	 * $collator array.
1456
	 *
1457
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1458
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1459
	 * @return bool
1460
	 */
1461
	public function collateDescendants($condition, &$collator) {
1462
		$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...
1463
		if($children) {
1464
			foreach($children as $item) {
1465
1466
				if(eval("return $condition;")) {
1467
					$collator[] = $item;
1468
				}
1469
				/** @var SiteTree $item */
1470
				$item->collateDescendants($condition, $collator);
1471
			}
1472
			return true;
1473
		}
1474
		return false;
1475
	}
1476
1477
	/**
1478
	 * Return the title, description, keywords and language metatags.
1479
	 *
1480
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1481
	 *
1482
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1483
	 * @return string The XHTML metatags
1484
	 */
1485
	public function MetaTags($includeTitle = true) {
1486
		$tags = array();
1487
		if($includeTitle && strtolower($includeTitle) != 'false') {
1488
			$tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1489
		}
1490
1491
		$generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1492
		if (!empty($generator)) {
1493
			$tags[] = FormField::create_tag('meta', array(
1494
				'name' => 'generator',
1495
				'content' => $generator,
1496
			));
1497
		}
1498
1499
		$charset = ContentNegotiator::config()->get('encoding');
1500
		$tags[] = FormField::create_tag('meta', array(
1501
			'http-equiv' => 'Content-Type',
1502
			'content' => 'text/html; charset=' . $charset,
1503
		));
1504
		if($this->MetaDescription) {
1505
			$tags[] = FormField::create_tag('meta', array(
1506
				'name' => 'description',
1507
				'content' => $this->MetaDescription,
1508
			));
1509
		}
1510
1511
		if(Permission::check('CMS_ACCESS_CMSMain')
1512
			&& !$this instanceof ErrorPage
1513
			&& $this->ID > 0
1514
		) {
1515
			$tags[] = FormField::create_tag('meta', array(
1516
				'name' => 'x-page-id',
1517
				'content' => $this->obj('ID')->forTemplate(),
1518
			));
1519
			$tags[] = FormField::create_tag('meta', array(
1520
				'name' => 'x-cms-edit-link',
1521
				'content' => $this->obj('CMSEditLink')->forTemplate(),
1522
			));
1523
		}
1524
1525
		$tags = implode("\n", $tags);
1526
		if($this->ExtraMeta) {
1527
			$tags .= $this->obj('ExtraMeta')->forTemplate();
1528
		}
1529
1530
		$this->extend('MetaTags', $tags);
1531
1532
		return $tags;
1533
	}
1534
1535
	/**
1536
	 * Returns the object that contains the content that a user would associate with this page.
1537
	 *
1538
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1539
	 * return the page that is linked to.
1540
	 *
1541
	 * @return $this
1542
	 */
1543
	public function ContentSource() {
1544
		return $this;
1545
	}
1546
1547
	/**
1548
	 * Add default records to database.
1549
	 *
1550
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1551
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1552
	 */
1553
	public function requireDefaultRecords() {
1554
		parent::requireDefaultRecords();
1555
1556
		// default pages
1557
		if(static::class == self::class && $this->config()->create_default_pages) {
1558
			if(!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1559
				$homepage = new Page();
1560
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1561
				$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>');
1562
				$homepage->URLSegment = RootURLController::config()->default_homepage_link;
1563
				$homepage->Sort = 1;
1564
				$homepage->write();
1565
				$homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1566
				$homepage->flushCache();
1567
				DB::alteration_message('Home page created', 'created');
1568
			}
1569
1570
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1571
				$aboutus = new Page();
1572
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1573
				$aboutus->Content = _t(
1574
					'SiteTree.DEFAULTABOUTCONTENT',
1575
					'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1576
				);
1577
				$aboutus->Sort = 2;
1578
				$aboutus->write();
1579
				$aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1580
				$aboutus->flushCache();
1581
				DB::alteration_message('About Us page created', 'created');
1582
1583
				$contactus = new Page();
1584
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1585
				$contactus->Content = _t(
1586
					'SiteTree.DEFAULTCONTACTCONTENT',
1587
					'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1588
				);
1589
				$contactus->Sort = 3;
1590
				$contactus->write();
1591
				$contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1592
				$contactus->flushCache();
1593
				DB::alteration_message('Contact Us page created', 'created');
1594
			}
1595
		}
1596
	}
1597
1598
	protected function onBeforeWrite() {
1599
		parent::onBeforeWrite();
1600
1601
		// If Sort hasn't been set, make this page come after it's siblings
1602
		if(!$this->Sort) {
1603
			$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...
1604
			$this->Sort = DB::prepared_query(
1605
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1606
				array($parentID)
1607
			)->value();
1608
		}
1609
1610
		// If there is no URLSegment set, generate one from Title
1611
		$defaultSegment = $this->generateURLSegment(_t(
1612
			'CMSMain.NEWPAGE',
1613
			'New {pagetype}',
1614
			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...
1615
		));
1616
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1617
			$this->URLSegment = $this->generateURLSegment($this->Title);
1618
		} else if($this->isChanged('URLSegment', 2)) {
1619
			// Do a strict check on change level, to avoid double encoding caused by
1620
			// bogus changes through forceChange()
1621
			$filter = URLSegmentFilter::create();
1622
			$this->URLSegment = $filter->filter($this->URLSegment);
1623
			// If after sanitising there is no URLSegment, give it a reasonable default
1624
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1625
		}
1626
1627
		// Ensure that this object has a non-conflicting URLSegment value.
1628
		$count = 2;
1629
		while(!$this->validURLSegment()) {
1630
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1631
			$count++;
1632
		}
1633
1634
		$this->syncLinkTracking();
1635
1636
		// Check to see if we've only altered fields that shouldn't affect versioning
1637
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1638
		$changedFields = array_keys($this->getChangedFields(true, 2));
1639
1640
		// This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1641
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1642
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1643
1644
		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...
1645
			// This will have the affect of preserving the versioning
1646
			$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...
1647
		}
1648
	}
1649
1650
	/**
1651
	 * Trigger synchronisation of link tracking
1652
	 *
1653
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1654
	 */
1655
	public function syncLinkTracking() {
1656
		$this->extend('augmentSyncLinkTracking');
1657
	}
1658
1659
	public function onBeforeDelete() {
1660
		parent::onBeforeDelete();
1661
1662
		// If deleting this page, delete all its children.
1663
		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...
1664
			foreach($children as $child) {
1665
				/** @var SiteTree $child */
1666
				$child->delete();
1667
			}
1668
		}
1669
	}
1670
1671
	public function onAfterDelete() {
1672
		// Need to flush cache to avoid outdated versionnumber references
1673
		$this->flushCache();
1674
1675
		// Need to mark pages depending to this one as broken
1676
		$dependentPages = $this->DependentPages();
1677
		if($dependentPages) foreach($dependentPages as $page) {
1678
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1679
			$page->write();
1680
		}
1681
1682
		parent::onAfterDelete();
1683
	}
1684
1685
	public function flushCache($persistent = true) {
1686
		parent::flushCache($persistent);
1687
		$this->_cache_statusFlags = null;
1688
	}
1689
1690
	public function validate() {
1691
		$result = parent::validate();
1692
1693
		// Allowed children validation
1694
		$parent = $this->getParent();
1695
		if($parent && $parent->exists()) {
1696
			// No need to check for subclasses or instanceof, as allowedChildren() already
1697
			// deconstructs any inheritance trees already.
1698
			$allowed = $parent->allowedChildren();
1699
			$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...
1700
				? $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...
1701
				: $this;
1702
			if(!in_array($subject->ClassName, $allowed)) {
1703
				$result->addError(
1704
					_t(
1705
						'SiteTree.PageTypeNotAllowed',
1706
						'Page type "{type}" not allowed as child of this parent page',
1707
						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...
1708
					),
1709
					ValidationResult::TYPE_ERROR,
1710
					'ALLOWED_CHILDREN'
1711
				);
1712
			}
1713
		}
1714
1715
		// "Can be root" validation
1716
		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...
1717
			$result->addError(
1718
				_t(
1719
					'SiteTree.PageTypNotAllowedOnRoot',
1720
					'Page type "{type}" is not allowed on the root level',
1721
					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...
1722
				),
1723
				ValidationResult::TYPE_ERROR,
1724
				'CAN_BE_ROOT'
1725
			);
1726
		}
1727
1728
		return $result;
1729
	}
1730
1731
	/**
1732
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1733
	 * checks for:
1734
	 *  - A page with the same URLSegment that has a conflict
1735
	 *  - Conflicts with actions on the parent page
1736
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1737
	 *
1738
	 * @return bool
1739
	 */
1740
	public function validURLSegment() {
1741
		if(self::config()->nested_urls && $parent = $this->Parent()) {
1742
			if($controller = ModelAsController::controller_for($parent)) {
1743
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1744
			}
1745
		}
1746
1747
		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...
1748
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, RequestHandler::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\Control\RequestHandler::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1749
				return false;
1750
			}
1751
		}
1752
1753
		// Filters by url, id, and parent
1754
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1755
		if($this->ID) {
1756
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1757
		}
1758
		if(self::config()->nested_urls) {
1759
			$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...
1760
		}
1761
1762
		// If any of the extensions return `0` consider the segment invalid
1763
		$extensionResponses = array_filter(
1764
			(array)$this->extend('augmentValidURLSegment'),
1765
			function($response) {return !is_null($response);}
1766
		);
1767
		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...
1768
			return min($extensionResponses);
1769
		}
1770
1771
		// Check existence
1772
		return !DataObject::get(self::class, $filter)->exists();
1773
	}
1774
1775
	/**
1776
	 * Generate a URL segment based on the title provided.
1777
	 *
1778
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1779
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1780
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1781
	 * scratch, or incrementally modify the generated URL.
1782
	 *
1783
	 * @param string $title Page title
1784
	 * @return string Generated url segment
1785
	 */
1786
	public function generateURLSegment($title){
1787
		$filter = URLSegmentFilter::create();
1788
		$t = $filter->filter($title);
1789
1790
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1791
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1792
1793
		// Hook for extensions
1794
		$this->extend('updateURLSegment', $t, $title);
1795
1796
		return $t;
1797
	}
1798
1799
	/**
1800
	 * Gets the URL segment for the latest draft version of this page.
1801
	 *
1802
	 * @return string
1803
	 */
1804
	public function getStageURLSegment() {
1805
		$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...
1806
			'"SiteTree"."ID"' => $this->ID
1807
		));
1808
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1809
	}
1810
1811
	/**
1812
	 * Gets the URL segment for the currently published version of this page.
1813
	 *
1814
	 * @return string
1815
	 */
1816
	public function getLiveURLSegment() {
1817
		$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...
1818
			'"SiteTree"."ID"' => $this->ID
1819
		));
1820
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1821
	}
1822
1823
	/**
1824
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1825
	 *
1826
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1827
	 * @return ArrayList
1828
	 */
1829
	public function DependentPages($includeVirtuals = true) {
1830
		if(class_exists('Subsite')) {
1831
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1832
			Subsite::disable_subsite_filter(true);
1833
		}
1834
1835
		// Content links
1836
		$items = new ArrayList();
1837
1838
		// We merge all into a regular SS_List, because DataList doesn't support merge
1839
		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...
1840
			$linkList = new ArrayList();
1841
			foreach($contentLinks as $item) {
1842
				$item->DependentLinkType = 'Content link';
1843
				$linkList->push($item);
1844
			}
1845
			$items->merge($linkList);
1846
		}
1847
1848
		// Virtual pages
1849
		if($includeVirtuals) {
1850
			$virtuals = $this->VirtualPages();
1851
			if($virtuals) {
1852
				$virtualList = new ArrayList();
1853
				foreach($virtuals as $item) {
1854
					$item->DependentLinkType = 'Virtual page';
1855
					$virtualList->push($item);
1856
				}
1857
				$items->merge($virtualList);
1858
			}
1859
		}
1860
1861
		// Redirector pages
1862
		$redirectors = RedirectorPage::get()->where(array(
1863
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1864
			'"RedirectorPage"."LinkToID"' => $this->ID
1865
		));
1866
		if($redirectors) {
1867
			$redirectorList = new ArrayList();
1868
			foreach($redirectors as $item) {
1869
				$item->DependentLinkType = 'Redirector page';
1870
				$redirectorList->push($item);
1871
			}
1872
			$items->merge($redirectorList);
1873
		}
1874
1875
		if(class_exists('Subsite')) {
1876
			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...
1877
		}
1878
1879
		return $items;
1880
	}
1881
1882
	/**
1883
	 * Return all virtual pages that link to this page.
1884
	 *
1885
	 * @return DataList
1886
	 */
1887
	public function VirtualPages() {
1888
		$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...
1889
1890
		// Disable subsite filter for these pages
1891
		if($pages instanceof DataList) {
1892
			return $pages->setDataQueryParam('Subsite.filter', false);
1893
		} else {
1894
			return $pages;
1895
		}
1896
	}
1897
1898
	/**
1899
	 * Returns a FieldList with which to create the main editing form.
1900
	 *
1901
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1902
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1903
	 *
1904
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1905
	 * e.g. access control.
1906
	 *
1907
	 * @return FieldList The fields to be displayed in the CMS
1908
	 */
1909
	public function getCMSFields() {
1910
		// Status / message
1911
		// Create a status message for multiple parents
1912
		if($this->ID && is_numeric($this->ID)) {
1913
			$linkedPages = $this->VirtualPages();
1914
1915
			$parentPageLinks = array();
1916
1917
			if($linkedPages->count() > 0) {
1918
				/** @var VirtualPage $linkedPage */
1919
				foreach($linkedPages as $linkedPage) {
1920
					$parentPage = $linkedPage->Parent();
1921
					if($parentPage && $parentPage->exists()) {
1922
						$link = Convert::raw2att($parentPage->CMSEditLink());
1923
						$title = Convert::raw2xml($parentPage->Title);
1924
						} else {
1925
						$link = CMSPageEditController::singleton()->Link('show');
1926
						$title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
1927
						}
1928
					$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1929
				}
1930
1931
				$lastParent = array_pop($parentPageLinks);
1932
				$parentList = "'$lastParent'";
1933
1934
				if(count($parentPageLinks)) {
1935
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1936
						. $parentList;
1937
				}
1938
1939
				$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...
1940
					'SiteTree.APPEARSVIRTUALPAGES',
1941
					"This content also appears on the virtual pages in the {title} sections.",
1942
					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...
1943
				);
1944
			}
1945
		}
1946
1947
		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...
1948
			$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...
1949
		}
1950
1951
		$dependentNote = '';
1952
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1953
1954
		// Create a table for showing pages linked to this one
1955
		$dependentPages = $this->DependentPages();
1956
		$dependentPagesCount = $dependentPages->count();
1957
		if($dependentPagesCount) {
1958
			$dependentColumns = array(
1959
				'Title' => $this->fieldLabel('Title'),
1960
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1961
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1962
			);
1963
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1964
1965
			$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>');
1966
			$dependentTable = GridField::create(
1967
				'DependentPages',
1968
				false,
1969
				$dependentPages
1970
			);
1971
			/** @var GridFieldDataColumns $dataColumns */
1972
			$dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1973
			$dataColumns
1974
				->setDisplayFields($dependentColumns)
1975
				->setFieldFormatting(array(
1976
					'Title' => function($value, &$item) {
1977
						return sprintf(
1978
							'<a href="admin/pages/edit/show/%d">%s</a>',
1979
							(int)$item->ID,
1980
							Convert::raw2xml($item->Title)
1981
						);
1982
					},
1983
					'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...
1984
						return sprintf(
1985
							'<a href="%s" target="_blank">%s</a>',
1986
							Convert::raw2xml($value),
1987
							Convert::raw2xml($value)
1988
						);
1989
					}
1990
				));
1991
		}
1992
1993
		$baseLink = Controller::join_links (
1994
			Director::absoluteBaseURL(),
1995
			(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...
1996
		);
1997
1998
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1999
			->setURLPrefix($baseLink)
2000
			->setDefaultURL($this->generateURLSegment(_t(
2001
				'CMSMain.NEWPAGE',
2002
				'New {pagetype}',
2003
				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...
2004
			)));
2005
		$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...
2006
			? $this->fieldLabel('LinkChangeNote')
2007
			: '';
2008
		if(!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
2009
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
2010
		}
2011
		$urlsegment->setHelpText($helpText);
2012
2013
		$fields = new FieldList(
2014
			$rootTab = new TabSet("Root",
2015
				$tabMain = new Tab('Main',
2016
					new TextField("Title", $this->fieldLabel('Title')),
2017
					$urlsegment,
2018
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
2019
					$htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
2020
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
2021
						array(
2022
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
2023
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
2024
						)
2025
					)->setHeadingLevel(4)
2026
				),
2027
				$tabDependent = new Tab('Dependent',
2028
					$dependentNote,
2029
					$dependentTable
2030
				)
2031
			)
2032
		);
2033
		$htmlField->addExtraClass('stacked');
2034
2035
		// Help text for MetaData on page content editor
2036
		$metaFieldDesc
2037
			->setRightTitle(
2038
				_t(
2039
					'SiteTree.METADESCHELP',
2040
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
2041
				)
2042
			)
2043
			->addExtraClass('help');
2044
		$metaFieldExtra
2045
			->setRightTitle(
2046
				_t(
2047
					'SiteTree.METAEXTRAHELP',
2048
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2049
				)
2050
			)
2051
			->addExtraClass('help');
2052
2053
		// Conditional dependent pages tab
2054
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2055
		else $fields->removeFieldFromTab('Root', 'Dependent');
2056
2057
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2058
2059
		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...
2060
			$obsoleteWarning = _t(
2061
				'SiteTree.OBSOLETECLASS',
2062
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2063
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2064
			);
2065
2066
			$fields->addFieldToTab(
2067
				"Root.Main",
2068
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2069
				"Title"
2070
			);
2071
		}
2072
2073
		if(file_exists(BASE_PATH . '/install.php')) {
2074
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2075
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2076
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2077
				. "</p>"), "Title");
2078
		}
2079
2080
		if(self::$runCMSFieldsExtensions) {
2081
			$this->extend('updateCMSFields', $fields);
2082
		}
2083
2084
		return $fields;
2085
	}
2086
2087
2088
	/**
2089
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2090
	 * for content-related fields.
2091
	 *
2092
	 * @return FieldList
2093
	 */
2094
	public function getSettingsFields() {
2095
		$groupsMap = array();
2096
		foreach(Group::get() as $group) {
2097
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2098
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2099
		}
2100
		asort($groupsMap);
2101
2102
		$fields = new FieldList(
2103
			$rootTab = new TabSet("Root",
2104
				$tabBehaviour = new Tab('Settings',
2105
					new DropdownField(
2106
						"ClassName",
2107
						$this->fieldLabel('ClassName'),
2108
						$this->getClassDropdown()
2109
					),
2110
					$parentTypeSelector = new CompositeField(
2111
						$parentType = new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2112
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2113
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2114
						)),
2115
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
2116
					),
2117
					$visibility = new FieldGroup(
2118
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2119
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2120
					),
2121
					$viewersOptionsField = new OptionsetField(
2122
						"CanViewType",
2123
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2124
					),
2125
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2126
						->setSource($groupsMap)
2127
						->setAttribute(
2128
							'data-placeholder',
2129
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2130
						),
2131
					$editorsOptionsField = new OptionsetField(
2132
						"CanEditType",
2133
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2134
					),
2135
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2136
						->setSource($groupsMap)
2137
						->setAttribute(
2138
							'data-placeholder',
2139
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2140
						)
2141
				)
2142
			)
2143
		);
2144
2145
		$parentType->addExtraClass('noborder');
2146
		$visibility->setTitle($this->fieldLabel('Visibility'));
2147
2148
2149
		// This filter ensures that the ParentID dropdown selection does not show this node,
2150
		// or its descendents, as this causes vanishing bugs
2151
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2152
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2153
2154
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2155
2156
		// Make page location fields read-only if the user doesn't have the appropriate permission
2157
		if(!Permission::check("SITETREE_REORGANISE")) {
2158
			$fields->makeFieldReadonly('ParentType');
2159
			if($this->getParentType() === 'root') {
2160
				$fields->removeByName('ParentID');
2161
			} else {
2162
				$fields->makeFieldReadonly('ParentID');
2163
			}
2164
		}
2165
2166
		$viewersOptionsSource = array();
2167
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2168
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2169
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2170
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2171
		$viewersOptionsField->setSource($viewersOptionsSource);
2172
2173
		$editorsOptionsSource = array();
2174
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2175
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2176
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2177
		$editorsOptionsField->setSource($editorsOptionsSource);
2178
2179
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2180
			$fields->makeFieldReadonly($viewersOptionsField);
2181
			if($this->CanViewType == 'OnlyTheseUsers') {
2182
				$fields->makeFieldReadonly($viewerGroupsField);
2183
			} else {
2184
				$fields->removeByName('ViewerGroups');
2185
			}
2186
2187
			$fields->makeFieldReadonly($editorsOptionsField);
2188
			if($this->CanEditType == 'OnlyTheseUsers') {
2189
				$fields->makeFieldReadonly($editorGroupsField);
2190
			} else {
2191
				$fields->removeByName('EditorGroups');
2192
			}
2193
		}
2194
2195
		if(self::$runCMSFieldsExtensions) {
2196
			$this->extend('updateSettingsFields', $fields);
2197
		}
2198
2199
		return $fields;
2200
	}
2201
2202
	/**
2203
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2204
	 * @return array
2205
	 */
2206
	public function fieldLabels($includerelations = true) {
2207
		$cacheKey = static::class . '_' . $includerelations;
2208
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2209
			$labels = parent::fieldLabels($includerelations);
2210
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2211
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2212
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2213
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2214
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2215
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2216
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2217
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2218
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2219
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2220
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2221
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2222
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2223
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2224
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2225
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2226
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2227
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2228
			$labels['LinkChangeNote'] = _t (
2229
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2230
			);
2231
2232
			if($includerelations){
2233
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2234
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2235
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2236
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2237
			}
2238
2239
			self::$_cache_field_labels[$cacheKey] = $labels;
2240
		}
2241
2242
		return self::$_cache_field_labels[$cacheKey];
2243
	}
2244
2245
	/**
2246
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2247
	 *
2248
	 * Frontend scripts and styles know how to handle the following FormFields:
2249
	 * - top-level FormActions appear as standalone buttons
2250
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2251
	 * - TabSet & Tabs appear as a drop ups
2252
	 * - FormActions within the Tab are restyled as links
2253
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2254
	 *
2255
	 * @return FieldList The available actions for this page.
2256
	 */
2257
	public function getCMSActions() {
2258
		// Get status of page
2259
		$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...
2260
		$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...
2261
		$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...
2262
2263
		// Check permissions
2264
		$canPublish = $this->canPublish();
2265
		$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...
2266
		$canEdit = $this->canEdit();
2267
2268
		// Major actions appear as buttons immediately visible as page actions.
2269
		$majorActions = CompositeField::create()->setName('MajorActions');
2270
		$majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2271
2272
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2273
		$rootTabSet = new TabSet('ActionMenus');
2274
		$moreOptions = new Tab(
2275
			'MoreOptions',
2276
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2277
		);
2278
		$rootTabSet->push($moreOptions);
2279
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2280
2281
		// Render page information into the "more-options" drop-up, on the top.
2282
		$liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2283
		$infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2284
		$moreOptions->push(
2285
			new LiteralField('Information',
2286
				$this->customise(array(
2287
					'Live' => $liveRecord,
2288
					'ExistsOnLive' => $isPublished
2289
				))->renderWith($infoTemplate)
2290
			)
2291
		);
2292
2293
		// Add to campaign option if not-archived and has publish permission
2294
		if (($isPublished || $isOnDraft) && $canPublish) {
2295
			$moreOptions->push(
2296
				AddToCampaignHandler_FormAction::create()
2297
					->removeExtraClass('btn-primary')
2298
					->addExtraClass('btn-secondary')
2299
				);
2300
		}
2301
2302
		// "readonly"/viewing version that isn't the current version of the record
2303
		$stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2304
		/** @skipUpgrade */
2305
		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...
2306
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2307
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2308
			$actions = new FieldList(array($majorActions, $rootTabSet));
2309
2310
			// getCMSActions() can be extended with updateCMSActions() on a extension
2311
			$this->extend('updateCMSActions', $actions);
2312
			return $actions;
2313
		}
2314
2315
		// "unpublish"
2316
		if($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2317
			$moreOptions->push(
2318
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2319
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2320
					->addExtraClass('btn-secondary')
2321
			);
2322
		}
2323
2324
		// "rollback"
2325
		if($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2326
			$moreOptions->push(
2327
				FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2328
					->setDescription(_t(
2329
						'SiteTree.BUTTONCANCELDRAFTDESC',
2330
						'Delete your draft and revert to the currently published page'
2331
					))
2332
					->addExtraClass('btn-secondary')
2333
			);
2334
		}
2335
2336
		// "restore"
2337
		if($canEdit && !$isOnDraft && $isPublished) {
2338
			$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2339
		}
2340
2341
		// Check if we can restore a deleted page
2342
		// Note: It would be nice to have a canRestore() permission at some point
2343
		if($canEdit && !$isOnDraft && !$isPublished) {
2344
			// Determine if we should force a restore to root (where once it was a subpage)
2345
			$restoreToRoot = $this->isParentArchived();
2346
2347
			// "restore"
2348
			$title = $restoreToRoot
2349
				? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2350
				: _t('CMSMain.RESTORE','Restore draft');
2351
			$description = $restoreToRoot
2352
				? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2353
				: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2354
			$majorActions->push(
2355
				FormAction::create('restore', $title)
2356
					->setDescription($description)
2357
					->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...
2358
					->setAttribute('data-icon', 'decline')
2359
			);
2360
		}
2361
2362
		// If a page is on any stage it can be archived
2363
		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...
2364
			$title = $isPublished
2365
				? _t('CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2366
				: _t('CMSMain.ARCHIVE', 'Archive');
2367
			$moreOptions->push(
2368
				FormAction::create('archive', $title)
2369
					->addExtraClass('delete btn btn-secondary')
2370
					->setDescription(_t(
2371
						'SiteTree.BUTTONDELETEDESC',
2372
						'Remove from draft/live and send to archive'
2373
					))
2374
			);
2375
		}
2376
2377
		// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2378
		if ($canEdit && $isOnDraft) {
2379
			$majorActions->push(
2380
				FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2381
					->addExtraClass('btn-secondary-outline font-icon-check-mark')
2382
					->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2383
					->setUseButtonTag(true)
2384
					->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2385
			);
2386
		}
2387
2388
		if($canPublish && $isOnDraft) {
2389
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2390
			$majorActions->push(
2391
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2392
					->addExtraClass('btn-secondary-outline font-icon-check-mark')
2393
					->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2394
					->setUseButtonTag(true)
2395
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2396
			);
2397
2398
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2399
			if($stagesDiffer) {
2400
				$publish->addExtraClass('btn-primary font-icon-rocket');
2401
				$publish->setTitle(_t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
2402
				$publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2403
			}
2404
		}
2405
2406
		$actions = new FieldList(array($majorActions, $rootTabSet));
2407
2408
		// Hook for extensions to add/remove actions.
2409
		$this->extend('updateCMSActions', $actions);
2410
2411
		return $actions;
2412
	}
2413
2414
	public function onAfterPublish() {
2415
		// Force live sort order to match stage sort order
2416
		DB::prepared_query('UPDATE "SiteTree_Live"
2417
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2418
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2419
			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...
2420
		);
2421
		}
2422
2423
	/**
2424
	 * Update draft dependant pages
2425
	 */
2426
	public function onAfterRevertToLive() {
2427
		// Use an alias to get the updates made by $this->publish
2428
		/** @var SiteTree $stageSelf */
2429
		$stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2430
		$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...
2431
2432
		// Need to update pages linking to this one as no longer broken
2433
		foreach($stageSelf->DependentPages() as $page) {
2434
			/** @var SiteTree $page */
2435
			$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...
2436
		}
2437
	}
2438
2439
	/**
2440
	 * Determine if this page references a parent which is archived, and not available in stage
2441
	 *
2442
	 * @return bool True if there is an archived parent
2443
	 */
2444
	protected function isParentArchived() {
2445
		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...
2446
			/** @var SiteTree $parentPage */
2447
			$parentPage = Versioned::get_latest_version(self::class, $parentID);
2448
			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...
2449
				return true;
2450
			}
2451
		}
2452
		return false;
2453
	}
2454
2455
	/**
2456
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2457
	 *
2458
	 * @return self
2459
	 */
2460
	public function doRestoreToStage() {
2461
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2462
2463
		// Ensure that the parent page is restored, otherwise restore to root
2464
		if($this->isParentArchived()) {
2465
			$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...
2466
		}
2467
2468
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2469
		// create an empty record
2470
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2471
			$conn = DB::get_conn();
2472
			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...
2473
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2474
			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...
2475
		}
2476
2477
		$oldReadingMode = Versioned::get_reading_mode();
2478
		Versioned::set_stage(Versioned::DRAFT);
2479
		$this->forceChange();
2480
		$this->write();
2481
2482
		/** @var SiteTree $result */
2483
		$result = DataObject::get_by_id(self::class, $this->ID);
2484
2485
		// Need to update pages linking to this one as no longer broken
2486
		foreach($result->DependentPages(false) as $page) {
2487
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2488
			$page->write();
2489
		}
2490
2491
		Versioned::set_reading_mode($oldReadingMode);
2492
2493
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2494
2495
		return $result;
2496
	}
2497
2498
	/**
2499
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2500
	 *
2501
	 * @return bool
2502
	 */
2503
	public function isNew() {
2504
		/**
2505
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2506
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2507
		 */
2508
		if(empty($this->ID)) return true;
2509
2510
		if(is_numeric($this->ID)) return false;
2511
2512
		return stripos($this->ID, 'new') === 0;
2513
	}
2514
2515
	/**
2516
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2517
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2518
	 * {@link SiteTree::$needs_permission}.
2519
	 *
2520
	 * @return array
2521
	 */
2522
	protected function getClassDropdown() {
2523
		$classes = self::page_type_classes();
2524
		$currentClass = null;
2525
2526
		$result = array();
2527
		foreach($classes as $class) {
2528
			$instance = singleton($class);
2529
2530
			// if the current page type is this the same as the class type always show the page type in the list
2531
			if ($this->ClassName != $instance->ClassName) {
2532
				if($instance instanceof HiddenClass) continue;
2533
				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...
2534
			}
2535
2536
			if($perms = $instance->stat('need_permission')) {
2537
				if(!$this->can($perms)) continue;
2538
			}
2539
2540
			$pageTypeName = $instance->i18n_singular_name();
2541
2542
			$currentClass = $class;
2543
			$result[$class] = $pageTypeName;
2544
2545
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2546
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2547
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2548
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2549
				$result[$class] = $result[$class] .  " ({$class})";
2550
			}
2551
		}
2552
2553
		// sort alphabetically, and put current on top
2554
		asort($result);
2555
		if($currentClass) {
2556
			$currentPageTypeName = $result[$currentClass];
2557
			unset($result[$currentClass]);
2558
			$result = array_reverse($result);
2559
			$result[$currentClass] = $currentPageTypeName;
2560
			$result = array_reverse($result);
2561
		}
2562
2563
		return $result;
2564
	}
2565
2566
	/**
2567
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2568
	 *
2569
	 * @return string[]
2570
	 */
2571
	public function allowedChildren() {
2572
		$allowedChildren = array();
2573
		$candidates = $this->stat('allowed_children');
2574
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2575
			foreach($candidates as $candidate) {
2576
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2577
				// Otherwise, the class and all its subclasses are allowed.
2578
				if(substr($candidate,0,1) == '*') {
2579
					$allowedChildren[] = substr($candidate,1);
2580
				} elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2581
					foreach($subclasses as $subclass) {
2582
						if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2583
							continue;
2584
						}
2585
						$allowedChildren[] = $subclass;
2586
					}
2587
				}
2588
			}
2589
		}
2590
2591
		return $allowedChildren;
2592
	}
2593
2594
	/**
2595
	 * Returns the class name of the default class for children of this page.
2596
	 *
2597
	 * @return string
2598
	 */
2599
	public function defaultChild() {
2600
		$default = $this->stat('default_child');
2601
		$allowed = $this->allowedChildren();
2602
		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...
2603
			if(!$default || !in_array($default, $allowed)) {
2604
				$default = reset($allowed);
2605
			}
2606
			return $default;
2607
		}
2608
		return null;
2609
	}
2610
2611
	/**
2612
	 * Returns the class name of the default class for the parent of this page.
2613
	 *
2614
	 * @return string
2615
	 */
2616
	public function defaultParent() {
2617
		return $this->stat('default_parent');
2618
	}
2619
2620
	/**
2621
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2622
	 * Title field.
2623
	 *
2624
	 * @return string
2625
	 */
2626
	public function getMenuTitle(){
2627
		if($value = $this->getField("MenuTitle")) {
2628
			return $value;
2629
		} else {
2630
			return $this->getField("Title");
2631
		}
2632
	}
2633
2634
2635
	/**
2636
	 * Set the menu title for this page.
2637
	 *
2638
	 * @param string $value
2639
	 */
2640
	public function setMenuTitle($value) {
2641
		if($value == $this->getField("Title")) {
2642
			$this->setField("MenuTitle", null);
2643
		} else {
2644
			$this->setField("MenuTitle", $value);
2645
		}
2646
	}
2647
2648
	/**
2649
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2650
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2651
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2652
	 * the flags.
2653
	 *
2654
	 * Example (simple):
2655
	 *   "deletedonlive" => "Deleted"
2656
	 *
2657
	 * Example (with optional title attribute):
2658
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2659
	 *
2660
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2661
	 * @return array
2662
	 */
2663
	public function getStatusFlags($cached = true) {
2664
		if(!$this->_cache_statusFlags || !$cached) {
2665
			$flags = array();
2666
			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...
2667
				$flags['removedfromdraft'] = array(
2668
					'text' => _t('SiteTree.ONLIVEONLYSHORT', 'On live only'),
2669
					'title' => _t('SiteTree.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2670
				);
2671
			} 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...
2672
				$flags['archived'] = array(
2673
					'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2674
					'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2675
				);
2676
			} 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...
2677
				$flags['addedtodraft'] = array(
2678
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2679
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2680
				);
2681
			} 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...
2682
				$flags['modified'] = array(
2683
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2684
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2685
				);
2686
			}
2687
2688
			$this->extend('updateStatusFlags', $flags);
2689
2690
			$this->_cache_statusFlags = $flags;
2691
		}
2692
2693
		return $this->_cache_statusFlags;
2694
	}
2695
2696
	/**
2697
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2698
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2699
	 * publication status.
2700
	 *
2701
	 * @return string An HTML string ready to be directly used in a template
2702
	 */
2703
	public function getTreeTitle() {
2704
		// Build the list of candidate children
2705
		$children = array();
2706
		$candidates = static::page_type_classes();
2707
		foreach($this->allowedChildren() as $childClass) {
2708
			if(!in_array($childClass, $candidates)) continue;
2709
			$child = singleton($childClass);
2710
			if($child->canCreate(null, array('Parent' => $this))) {
2711
				$children[$childClass] = $child->i18n_singular_name();
2712
			}
2713
		}
2714
		$flags = $this->getStatusFlags();
2715
		$treeTitle = sprintf(
2716
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2717
			Convert::raw2att(Convert::raw2json($children)),
2718
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2719
		);
2720
		foreach($flags as $class => $data) {
2721
			if(is_string($data)) $data = array('text' => $data);
2722
			$treeTitle .= sprintf(
2723
				"<span class=\"badge %s\"%s>%s</span>",
2724
				'status-' . Convert::raw2xml($class),
2725
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2726
				Convert::raw2xml($data['text'])
2727
			);
2728
		}
2729
2730
		return $treeTitle;
2731
	}
2732
2733
	/**
2734
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2735
	 * we're currently inside, etc.
2736
	 *
2737
	 * @param int $level
2738
	 * @return SiteTree
2739
	 */
2740
	public function Level($level) {
2741
		$parent = $this;
2742
		$stack = array($parent);
2743
		while(($parent = $parent->Parent()) && $parent->exists()) {
2744
			array_unshift($stack, $parent);
2745
		}
2746
2747
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2748
	}
2749
2750
	/**
2751
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2752
	 *
2753
	 * @return int
2754
	 */
2755
	public function getPageLevel() {
2756
		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...
2757
			return 1 + $this->Parent()->getPageLevel();
2758
		}
2759
		return 1;
2760
	}
2761
2762
	/**
2763
	 * Find the controller name by our convention of {$ModelClass}Controller
2764
	 *
2765
	 * @return string
2766
	 */
2767
	public function getControllerName() {
2768
		//default controller for SiteTree objects
2769
		$controller = ContentController::class;
2770
2771
		//go through the ancestry for this class looking for
2772
		$ancestry = ClassInfo::ancestry(static::class);
2773
		// loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2774
		while ($class = array_pop($ancestry)) {
2775
			//we don't need to go any deeper than the SiteTree class
2776
			if ($class == SiteTree::class) {
2777
				break;
2778
			}
2779
			// If we have a class of "{$ClassName}Controller" then we found our controller
2780
			if (class_exists($candidate = sprintf('%sController', $class))) {
2781
				$controller = $candidate;
2782
				break;
2783
			} elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2784
				// Support the legacy underscored filename, but raise a deprecation notice
2785
				Deprecation::notice(
2786
					'5.0',
2787
					'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2788
					Deprecation::SCOPE_GLOBAL
2789
				);
2790
				$controller = $candidate;
2791
				break;
2792
			}
2793
		}
2794
2795
		return $controller;
2796
	}
2797
2798
	/**
2799
	 * Return the CSS classes to apply to this node in the CMS tree.
2800
	 *
2801
	 * @param string $numChildrenMethod
2802
	 * @return string
2803
	 */
2804
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2805
		$classes = sprintf('class-%s', static::class);
2806
		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...
2807
			$classes .= " BrokenLink";
2808
		}
2809
2810
		if(!$this->canAddChildren()) {
2811
			$classes .= " nochildren";
2812
		}
2813
2814
		if(!$this->canEdit() && !$this->canAddChildren()) {
2815
			if (!$this->canView()) {
2816
				$classes .= " disabled";
2817
			} else {
2818
				$classes .= " edit-disabled";
2819
			}
2820
		}
2821
2822
		if(!$this->ShowInMenus) {
2823
			$classes .= " notinmenu";
2824
		}
2825
2826
		//TODO: Add integration
2827
		/*
2828
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2829
			$classes .= " untranslated ";
2830
		*/
2831
		$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...
2832
2833
		return $classes;
2834
	}
2835
2836
	/**
2837
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2838
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2839
	 */
2840
	static public function disableCMSFieldsExtensions() {
2841
		self::$runCMSFieldsExtensions = false;
2842
	}
2843
2844
	/**
2845
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2846
	 * disableCMSFieldsExtensions().
2847
	 */
2848
	static public function enableCMSFieldsExtensions() {
2849
		self::$runCMSFieldsExtensions = true;
2850
	}
2851
2852
	public function providePermissions() {
2853
		return array(
2854
			'SITETREE_GRANT_ACCESS' => array(
2855
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2856
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2857
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2858
				'sort' => 100
2859
			),
2860
			'SITETREE_VIEW_ALL' => array(
2861
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2862
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2863
				'sort' => -100,
2864
				'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')
2865
			),
2866
			'SITETREE_EDIT_ALL' => array(
2867
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2868
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2869
				'sort' => -50,
2870
				'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')
2871
			),
2872
			'SITETREE_REORGANISE' => array(
2873
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2874
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2875
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2876
				'sort' => 100
2877
			),
2878
			'VIEW_DRAFT_CONTENT' => array(
2879
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2880
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2881
				'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.'),
2882
				'sort' => 100
2883
			)
2884
		);
2885
	}
2886
2887
	/**
2888
	 * Default singular name for page / sitetree
2889
	 *
2890
	 * @return string
2891
	 */
2892 View Code Duplication
	public function singular_name() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2893
		$base = in_array(static::class, [Page::class, self::class]);
2894
		if ($base) {
2895
			return $this->stat('base_singular_name');
2896
		}
2897
		return parent::singular_name();
2898
	}
2899
2900
	/**
2901
	 * Default plural name for page / sitetree
2902
	 *
2903
	 * @return string
2904
	 */
2905 View Code Duplication
	public function plural_name() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2906
		$base = in_array(static::class, [Page::class, self::class]);
2907
		if ($base) {
2908
			return $this->stat('base_plural_name');
2909
		}
2910
		return parent::plural_name();
2911
	}
2912
2913
	/**
2914
	 * Get description for this page
2915
	 *
2916
	 * @return string|null
2917
	 */
2918
	public function description() {
2919
		$base = in_array(static::class, [Page::class, self::class]);
2920
		if ($base) {
2921
			return $this->stat('base_description');
2922
		}
2923
		return $this->stat('description');
2924
	}
2925
2926
	/**
2927
	 * Get localised description for this page
2928
	 *
2929
	 * @return string|null
2930
	 */
2931
	public function i18n_description() {
2932
		$description = $this->description();
2933
		if ($description) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $description 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...
2934
			return _t(static::class.'.DESCRIPTION', $description);
2935
		}
2936
		return null;
2937
	}
2938
2939
	/**
2940
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2941
	 * picks it up for the wrong folder.
2942
	 *
2943
	 * @return array
2944
	 */
2945
	public function provideI18nEntities() {
2946
		$entities = parent::provideI18nEntities();
2947
2948
		// Add optional description
2949
		$description = $this->description();
2950
		if ($description) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $description 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...
2951
			$entities[static::class . '.DESCRIPTION'] = $description;
2952
		}
2953
		return $entities;
0 ignored issues
show
Best Practice introduced by
The expression return $entities; seems to be an array, but some of its elements' types (string) are 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 new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2954
	}
2955
2956
	/**
2957
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2958
	 *
2959
	 * @return string
2960
	 */
2961
	public function getParentType() {
2962
		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...
2963
	}
2964
2965
	/**
2966
	 * Clear the permissions cache for SiteTree
2967
	 */
2968
	public static function reset() {
2969
		self::$cache_permissions = array();
2970
	}
2971
2972
	public static function on_db_reset() {
2973
		self::$cache_permissions = array();
2974
	}
2975
}
2976