Completed
Pull Request — master (#1722)
by Robbie
02:13
created

SiteTree::getFrontendControllerName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

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

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

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

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

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

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

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

Loading history...
2906
	}
2907
2908
	/**
2909
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2910
	 *
2911
	 * @return string
2912
	 */
2913
	public function getParentType() {
2914
		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...
2915
	}
2916
2917
	/**
2918
	 * Clear the permissions cache for SiteTree
2919
	 */
2920
	public static function reset() {
2921
		self::$cache_permissions = array();
2922
	}
2923
2924
	static public function on_db_reset() {
2925
		self::$cache_permissions = array();
2926
	}
2927
2928
}
2929