Completed
Pull Request — master (#1605)
by Loz
03:02
created

SiteTree::getAssociatedController()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 1
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\Core\Injector\Injector;
19
use SilverStripe\Dev\Deprecation;
20
use SilverStripe\Forms\CheckboxField;
21
use SilverStripe\Forms\CompositeField;
22
use SilverStripe\Forms\DropdownField;
23
use SilverStripe\Forms\FieldGroup;
24
use SilverStripe\Forms\FieldList;
25
use SilverStripe\Forms\FormAction;
26
use SilverStripe\Forms\FormField;
27
use SilverStripe\Forms\GridField\GridField;
28
use SilverStripe\Forms\GridField\GridFieldDataColumns;
29
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
30
use SilverStripe\Forms\ListboxField;
31
use SilverStripe\Forms\LiteralField;
32
use SilverStripe\Forms\OptionsetField;
33
use SilverStripe\Forms\Tab;
34
use SilverStripe\Forms\TabSet;
35
use SilverStripe\Forms\TextareaField;
36
use SilverStripe\Forms\TextField;
37
use SilverStripe\Forms\ToggleCompositeField;
38
use SilverStripe\Forms\TreeDropdownField;
39
use SilverStripe\i18n\i18n;
40
use SilverStripe\i18n\i18nEntityProvider;
41
use SilverStripe\ORM\ArrayList;
42
use SilverStripe\ORM\DataList;
43
use SilverStripe\ORM\DataObject;
44
use SilverStripe\ORM\DB;
45
use SilverStripe\ORM\HiddenClass;
46
use SilverStripe\ORM\Hierarchy\Hierarchy;
47
use SilverStripe\ORM\ManyManyList;
48
use SilverStripe\ORM\Versioning\Versioned;
49
use SilverStripe\Security\Group;
50
use SilverStripe\Security\Member;
51
use SilverStripe\Security\Permission;
52
use SilverStripe\Security\PermissionProvider;
53
use SilverStripe\SiteConfig\SiteConfig;
54
use SilverStripe\View\ArrayData;
55
use SilverStripe\View\Parsers\ShortcodeParser;
56
use SilverStripe\View\Parsers\URLSegmentFilter;
57
use SilverStripe\View\SSViewer;
58
use Subsite;
59
60
/**
61
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
62
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
63
 * draft and published states.
64
 *
65
 * <h2>URLs</h2>
66
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
67
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
68
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
69
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
70
 * {@link URLSegmentFilter::$default_allow_multibyte}.
71
 *
72
 * @property string URLSegment
73
 * @property string Title
74
 * @property string MenuTitle
75
 * @property string Content HTML content of the page.
76
 * @property string MetaDescription
77
 * @property string ExtraMeta
78
 * @property string ShowInMenus
79
 * @property string ShowInSearch
80
 * @property string Sort Integer value denoting the sort order.
81
 * @property string ReportClass
82
 * @property string CanViewType Type of restriction for viewing this object.
83
 * @property string CanEditType Type of restriction for editing this object.
84
 *
85
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
86
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
87
 * @method SiteTree Parent()
88
 *
89
 * @mixin Hierarchy
90
 * @mixin Versioned
91
 * @mixin SiteTreeLinkTracking
92
 */
93
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
94
95
	/**
96
	 * Indicates what kind of children this page type can have.
97
	 * This can be an array of allowed child classes, or the string "none" -
98
	 * indicating that this page type can't have children.
99
	 * If a classname is prefixed by "*", such as "*Page", then only that
100
	 * class is allowed - no subclasses. Otherwise, the class and all its
101
	 * subclasses are allowed.
102
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
103
	 *
104
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
105
	 *
106
	 * @config
107
	 * @var array
108
	 */
109
	private static $allowed_children = array("SilverStripe\\CMS\\Model\\SiteTree");
110
111
	/**
112
	 * The default child class for this page.
113
	 * Note: Value might be cached, see {@link $allowed_chilren}.
114
	 *
115
	 * @config
116
	 * @var string
117
	 */
118
	private static $default_child = "Page";
119
120
	/**
121
	 * Default value for SiteTree.ClassName enum
122
	 * {@see DBClassName::getDefault}
123
	 *
124
	 * @config
125
	 * @var string
126
	 */
127
	private static $default_classname = "Page";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
128
129
	/**
130
	 * The default parent class for this page.
131
	 * Note: Value might be cached, see {@link $allowed_chilren}.
132
	 *
133
	 * @config
134
	 * @var string
135
	 */
136
	private static $default_parent = null;
137
138
	/**
139
	 * Controls whether a page can be in the root of the site tree.
140
	 * Note: Value might be cached, see {@link $allowed_chilren}.
141
	 *
142
	 * @config
143
	 * @var bool
144
	 */
145
	private static $can_be_root = true;
146
147
	/**
148
	 * List of permission codes a user can have to allow a user to create a page of this type.
149
	 * Note: Value might be cached, see {@link $allowed_chilren}.
150
	 *
151
	 * @config
152
	 * @var array
153
	 */
154
	private static $need_permission = null;
155
156
	/**
157
	 * If you extend a class, and don't want to be able to select the old class
158
	 * in the cms, set this to the old class name. Eg, if you extended Product
159
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
160
	 *
161
	 * @config
162
	 * @var string
163
	 */
164
	private static $hide_ancestor = null;
165
166
	private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
167
		"URLSegment" => "Varchar(255)",
168
		"Title" => "Varchar(255)",
169
		"MenuTitle" => "Varchar(100)",
170
		"Content" => "HTMLText",
171
		"MetaDescription" => "Text",
172
		"ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
173
		"ShowInMenus" => "Boolean",
174
		"ShowInSearch" => "Boolean",
175
		"Sort" => "Int",
176
		"HasBrokenFile" => "Boolean",
177
		"HasBrokenLink" => "Boolean",
178
		"ReportClass" => "Varchar",
179
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
180
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
181
	);
182
183
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
184
		"URLSegment" => true,
185
	);
186
187
	private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
188
		"ViewerGroups" => "SilverStripe\\Security\\Group",
189
		"EditorGroups" => "SilverStripe\\Security\\Group",
190
	);
191
192
	private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
193
		"VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
194
	);
195
196
	private static $owned_by = array(
197
		"VirtualPages"
198
	);
199
200
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
201
		"Breadcrumbs" => "HTMLFragment",
202
		"LastEdited" => "Datetime",
203
		"Created" => "Datetime",
204
		'Link' => 'Text',
205
		'RelativeLink' => 'Text',
206
		'AbsoluteLink' => 'Text',
207
		'CMSEditLink' => 'Text',
208
		'TreeTitle' => 'HTMLFragment',
209
		'MetaTags' => 'HTMLFragment',
210
	);
211
212
	private static $defaults = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
213
		"ShowInMenus" => 1,
214
		"ShowInSearch" => 1,
215
		"CanViewType" => "Inherit",
216
		"CanEditType" => "Inherit"
217
	);
218
219
	private static $table_name = 'SiteTree';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
220
221
	private static $versioning = array(
222
		"Stage",  "Live"
223
	);
224
225
	private static $default_sort = "\"Sort\"";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
226
227
	/**
228
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
229
	 * @var boolean
230
	 * @config
231
	 */
232
	private static $can_create = true;
233
234
	/**
235
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
236
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
237
	 *
238
	 * @see CMSMain::generateTreeStylingCSS()
239
	 * @config
240
	 * @var string
241
	 */
242
	private static $icon = null;
243
244
	/**
245
	 * @config
246
	 * @var string Description of the class functionality, typically shown to a user
247
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
248
	 */
249
	private static $description = 'Generic content page';
250
251
	private static $extensions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
252
		'SilverStripe\\ORM\\Hierarchy\\Hierarchy',
253
		'SilverStripe\\ORM\\Versioning\\Versioned',
254
		"SilverStripe\\CMS\\Model\\SiteTreeLinkTracking"
255
	);
256
257
	private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
258
		'Title',
259
		'Content',
260
	);
261
262
	private static $field_labels = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
263
		'URLSegment' => 'URL'
264
	);
265
266
	/**
267
	 * @config
268
	 */
269
	private static $nested_urls = true;
270
271
	/**
272
	 * @config
273
	*/
274
	private static $create_default_pages = true;
275
276
	/**
277
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
278
	 */
279
	private static $runCMSFieldsExtensions = true;
280
281
	/**
282
	 * Cache for canView/Edit/Publish/Delete permissions.
283
	 * Keyed by permission type (e.g. 'edit'), with an array
284
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
285
	 * See {@link batch_permission_check()} for details.
286
	 */
287
	private static $cache_permissions = array();
288
289
	/**
290
	 * @config
291
	 * @var boolean
292
	 */
293
	private static $enforce_strict_hierarchy = true;
294
295
	/**
296
	 * The value used for the meta generator tag. Leave blank to omit the tag.
297
	 *
298
	 * @config
299
	 * @var string
300
	 */
301
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
302
303
	protected $_cache_statusFlags = null;
304
305
	/**
306
	 * Fetches the {@link SiteTree} object that maps to a link.
307
	 *
308
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
309
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
310
	 *
311
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
312
	 * by a extension attached to {@link SiteTree}
313
	 *
314
	 * @param string $link  The link of the page to search for
315
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
316
	 * @return SiteTree
317
	 */
318
	static public function get_by_link($link, $cache = true) {
319
		if(trim($link, '/')) {
320
			$link = trim(Director::makeRelative($link), '/');
321
		} else {
322
			$link = RootURLController::get_homepage_link();
323
		}
324
325
		$parts = preg_split('|/+|', $link);
326
327
		// Grab the initial root level page to traverse down from.
328
		$URLSegment = array_shift($parts);
329
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
330
		if(self::config()->nested_urls) {
331
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
332
		}
333
		/** @var SiteTree $sitetree */
334
		$sitetree = DataObject::get_one(__CLASS__, $conditions, $cache);
335
336
		/// Fall back on a unique URLSegment for b/c.
337
		if(	!$sitetree
338
			&& self::config()->nested_urls
339
			&& $sitetree = DataObject::get_one(__CLASS__, array(
340
				'"SiteTree"."URLSegment"' => $URLSegment
341
			), $cache)
342
		) {
343
			return $sitetree;
344
		}
345
346
		// Attempt to grab an alternative page from extensions.
347
		if(!$sitetree) {
348
			$parentID = self::config()->nested_urls ? 0 : null;
349
350 View Code Duplication
			if($alternatives = static::singleton()->extend('alternateGetByLink', $URLSegment, $parentID)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
351
				foreach($alternatives as $alternative) {
352
					if($alternative) {
353
						$sitetree = $alternative;
354
			}
355
				}
356
			}
357
358
			if(!$sitetree) {
359
				return null;
360
		}
361
		}
362
363
		// Check if we have any more URL parts to parse.
364
		if(!self::config()->nested_urls || !count($parts)) {
365
			return $sitetree;
366
		}
367
368
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
369
		foreach($parts as $segment) {
370
			$next = DataObject::get_one('SilverStripe\\CMS\\Model\\SiteTree', array(
371
					'"SiteTree"."URLSegment"' => $segment,
372
					'"SiteTree"."ParentID"' => $sitetree->ID
373
				),
374
				$cache
375
			);
376
377
			if(!$next) {
378
				$parentID = (int) $sitetree->ID;
379
380 View Code Duplication
				if($alternatives = static::singleton()->extend('alternateGetByLink', $segment, $parentID)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
381
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
382
				}
383
384
				if(!$next) {
385
					return null;
386
				}
387
			}
388
389
			$sitetree->destroy();
390
			$sitetree = $next;
391
		}
392
393
		return $sitetree;
394
	}
395
396
	/**
397
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
398
	 *
399
	 * @return array
400
	 */
401
	public static function page_type_classes() {
402
		$classes = ClassInfo::getValidSubClasses();
403
404
		$baseClassIndex = array_search(__CLASS__, $classes);
405
		if($baseClassIndex !== false) {
406
			unset($classes[$baseClassIndex]);
407
		}
408
409
		$kill_ancestors = array();
410
411
		// figure out if there are any classes we don't want to appear
412
		foreach($classes as $class) {
413
			$instance = singleton($class);
414
415
			// do any of the progeny want to hide an ancestor?
416
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
417
				// note for killing later
418
				$kill_ancestors[] = $ancestor_to_hide;
419
			}
420
		}
421
422
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
423
		// requirements
424
		if($kill_ancestors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $kill_ancestors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
425
			$kill_ancestors = array_unique($kill_ancestors);
426
			foreach($kill_ancestors as $mark) {
427
				// unset from $classes
428
				$idx = array_search($mark, $classes, true);
429
				if ($idx !== false) {
430
					unset($classes[$idx]);
431
				}
432
			}
433
		}
434
435
		return $classes;
436
	}
437
438
	/**
439
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
440
	 *
441
	 * @param array      $arguments
442
	 * @param string     $content
443
	 * @param ShortcodeParser $parser
444
	 * @return string
445
	 */
446
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
447
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) {
448
			return null;
449
		}
450
451
		/** @var SiteTree $page */
452
		if (
453
			   !($page = DataObject::get_by_id(__CLASS__, $arguments['id']))         // Get the current page by ID.
454
			&& !($page = Versioned::get_latest_version(__CLASS__, $arguments['id'])) // Attempt link to old version.
455
		) {
456
			 return null; // There were no suitable matches at all.
457
		}
458
459
		/** @var SiteTree $page */
460
		$link = Convert::raw2att($page->Link());
461
462
		if($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
463
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
464
		} else {
465
			return $link;
466
		}
467
	}
468
469
	/**
470
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
471
	 *
472
	 * @param string $action Optional controller action (method).
473
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
474
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
475
	 *                       append GET parameters.
476
	 * @return string
477
	 */
478
	public function Link($action = null) {
479
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
480
	}
481
482
	/**
483
	 * Get the absolute URL for this page, including protocol and host.
484
	 *
485
	 * @param string $action See {@link Link()}
486
	 * @return string
487
	 */
488
	public function AbsoluteLink($action = null) {
489
		if($this->hasMethod('alternateAbsoluteLink')) {
490
			return $this->alternateAbsoluteLink($action);
491
		} else {
492
			return Director::absoluteURL($this->Link($action));
493
		}
494
	}
495
496
	/**
497
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
498
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
499
	 *
500
	 * @param string $action See {@link Link()}
501
	 * @return string
502
	 */
503
	public function PreviewLink($action = null) {
504
		if($this->hasMethod('alternatePreviewLink')) {
505
			Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
506
			return $this->alternatePreviewLink($action);
507
		}
508
509
		$link = $this->AbsoluteLink($action);
510
		$this->extend('updatePreviewLink', $link, $action);
511
		return $link;
512
	}
513
514
	public function getMimeType() {
515
		return 'text/html';
516
	}
517
518
	/**
519
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
520
	 *
521
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
522
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
523
	 * and returned in its full form.
524
	 *
525
	 * @uses RootURLController::get_homepage_link()
526
	 *
527
	 * @param string $action See {@link Link()}
528
	 * @return string
529
	 */
530
	public function RelativeLink($action = null) {
531
		if($this->ParentID && self::config()->nested_urls) {
532
			$parent = $this->Parent();
533
			// If page is removed select parent from version history (for archive page view)
534
			if((!$parent || !$parent->exists()) && $this->getIsDeletedFromStage()) {
535
				$parent = Versioned::get_latest_version(__CLASS__, $this->ParentID);
536
			}
537
			$base = $parent->RelativeLink($this->URLSegment);
538
		} elseif(!$action && $this->URLSegment == RootURLController::get_homepage_link()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
539
			// Unset base for root-level homepages.
540
			// Note: Homepages with action parameters (or $action === true)
541
			// need to retain their URLSegment.
542
			$base = null;
543
		} else {
544
			$base = $this->URLSegment;
545
		}
546
547
		$this->extend('updateRelativeLink', $base, $action);
548
549
		// Legacy support: If $action === true, retain URLSegment for homepages,
550
		// but don't append any action
551
		if($action === true) $action = null;
552
553
		return Controller::join_links($base, '/', $action);
554
	}
555
556
	/**
557
	 * Get the absolute URL for this page on the Live site.
558
	 *
559
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
560
	 * @return string
561
	 */
562
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
563
		$oldReadingMode = Versioned::get_reading_mode();
564
		Versioned::set_stage(Versioned::LIVE);
565
		/** @var SiteTree $live */
566
		$live = Versioned::get_one_by_stage(__CLASS__, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1701
			}
1702
		}
1703
1704
		if(!self::config()->nested_urls || !$this->ParentID) {
1705
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'SilverStripe\\Control\\RequestHandler')) return false;
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of returns inconsistent results on some PHP versions for interfaces; you could instead use ReflectionClass::implementsInterface.
Loading history...
1706
		}
1707
1708
		// Filters by url, id, and parent
1709
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1710
		if($this->ID) {
1711
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1712
		}
1713
		if(self::config()->nested_urls) {
1714
			$filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
1715
		}
1716
1717
		$votes = array_filter(
1718
			(array)$this->extend('augmentValidURLSegment'),
1719
			function($v) {return !is_null($v);}
1720
		);
1721
		if($votes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $votes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1722
			return min($votes);
1723
		}
1724
1725
		// Check existence
1726
		$existingPage = DataObject::get_one('SilverStripe\\CMS\\Model\\SiteTree', $filter);
1727
		if ($existingPage) return false;
1728
1729
		return !($existingPage);
1730
		}
1731
1732
	/**
1733
	 * Generate a URL segment based on the title provided.
1734
	 *
1735
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1736
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1737
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1738
	 * scratch, or incrementally modify the generated URL.
1739
	 *
1740
	 * @param string $title Page title
1741
	 * @return string Generated url segment
1742
	 */
1743
	public function generateURLSegment($title){
1744
		$filter = URLSegmentFilter::create();
1745
		$t = $filter->filter($title);
1746
1747
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1748
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1749
1750
		// Hook for extensions
1751
		$this->extend('updateURLSegment', $t, $title);
1752
1753
		return $t;
1754
	}
1755
1756
	/**
1757
	 * Gets the URL segment for the latest draft version of this page.
1758
	 *
1759
	 * @return string
1760
	 */
1761
	public function getStageURLSegment() {
1762
		$stageRecord = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 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...
1763
			'"SiteTree"."ID"' => $this->ID
1764
		));
1765
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1766
	}
1767
1768
	/**
1769
	 * Gets the URL segment for the currently published version of this page.
1770
	 *
1771
	 * @return string
1772
	 */
1773
	public function getLiveURLSegment() {
1774
		$liveRecord = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 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...
1775
			'"SiteTree"."ID"' => $this->ID
1776
		));
1777
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1778
	}
1779
1780
	/**
1781
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1782
	 *
1783
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1784
	 * @return ArrayList
1785
	 */
1786
	public function DependentPages($includeVirtuals = true) {
1787
		if(class_exists('Subsite')) {
1788
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1789
			Subsite::disable_subsite_filter(true);
1790
		}
1791
1792
		// Content links
1793
		$items = new ArrayList();
1794
1795
		// We merge all into a regular SS_List, because DataList doesn't support merge
1796
		if($contentLinks = $this->BackLinkTracking()) {
1797
			$linkList = new ArrayList();
1798
			foreach($contentLinks as $item) {
1799
				$item->DependentLinkType = 'Content link';
1800
				$linkList->push($item);
1801
			}
1802
			$items->merge($linkList);
1803
		}
1804
1805
		// Virtual pages
1806
		if($includeVirtuals) {
1807
			$virtuals = $this->VirtualPages();
1808
			if($virtuals) {
1809
				$virtualList = new ArrayList();
1810
				foreach($virtuals as $item) {
1811
					$item->DependentLinkType = 'Virtual page';
1812
					$virtualList->push($item);
1813
				}
1814
				$items->merge($virtualList);
1815
			}
1816
		}
1817
1818
		// Redirector pages
1819
		$redirectors = RedirectorPage::get()->where(array(
1820
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1821
			'"RedirectorPage"."LinkToID"' => $this->ID
1822
		));
1823
		if($redirectors) {
1824
			$redirectorList = new ArrayList();
1825
			foreach($redirectors as $item) {
1826
				$item->DependentLinkType = 'Redirector page';
1827
				$redirectorList->push($item);
1828
			}
1829
			$items->merge($redirectorList);
1830
		}
1831
1832
		if(class_exists('Subsite')) {
1833
			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...
1834
		}
1835
1836
		return $items;
1837
	}
1838
1839
	/**
1840
	 * Return all virtual pages that link to this page.
1841
	 *
1842
	 * @return DataList
1843
	 */
1844
	public function VirtualPages() {
1845
		$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...
1846
1847
		// Disable subsite filter for these pages
1848
		if($pages instanceof DataList) {
1849
			return $pages->setDataQueryParam('Subsite.filter', false);
1850
		} else {
1851
			return $pages;
1852
		}
1853
	}
1854
1855
	/**
1856
	 * Returns a FieldList with which to create the main editing form.
1857
	 *
1858
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1859
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1860
	 *
1861
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1862
	 * e.g. access control.
1863
	 *
1864
	 * @return FieldList The fields to be displayed in the CMS
1865
	 */
1866
	public function getCMSFields() {
1867
		// Status / message
1868
		// Create a status message for multiple parents
1869
		if($this->ID && is_numeric($this->ID)) {
1870
			$linkedPages = $this->VirtualPages();
1871
1872
			$parentPageLinks = array();
1873
1874
			if($linkedPages->count() > 0) {
1875
				/** @var VirtualPage $linkedPage */
1876
				foreach($linkedPages as $linkedPage) {
1877
					$parentPage = $linkedPage->Parent();
1878
					if($parentPage && $parentPage->exists()) {
1879
						$link = Convert::raw2att($parentPage->CMSEditLink());
1880
						$title = Convert::raw2xml($parentPage->Title);
1881
						} else {
1882
						$link = CMSPageEditController::singleton()->Link('show');
1883
						$title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
1884
						}
1885
					$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1886
				}
1887
1888
				$lastParent = array_pop($parentPageLinks);
1889
				$parentList = "'$lastParent'";
1890
1891
				if(count($parentPageLinks)) {
1892
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1893
						. $parentList;
1894
				}
1895
1896
				$statusMessage[] = _t(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$statusMessage was never initialized. Although not strictly required by PHP, it is generally a good practice to add $statusMessage = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1897
					'SiteTree.APPEARSVIRTUALPAGES',
1898
					"This content also appears on the virtual pages in the {title} sections.",
1899
					array('title' => $parentList)
0 ignored issues
show
Documentation introduced by
array('title' => $parentList) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1900
				);
1901
			}
1902
		}
1903
1904
		if($this->HasBrokenLink || $this->HasBrokenFile) {
1905
			$statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
0 ignored issues
show
Bug introduced by
The variable $statusMessage does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1906
		}
1907
1908
		$dependentNote = '';
1909
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1910
1911
		// Create a table for showing pages linked to this one
1912
		$dependentPages = $this->DependentPages();
1913
		$dependentPagesCount = $dependentPages->count();
1914
		if($dependentPagesCount) {
1915
			$dependentColumns = array(
1916
				'Title' => $this->fieldLabel('Title'),
1917
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1918
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1919
			);
1920
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1921
1922
			$dependentNote = new LiteralField('DependentNote', '<p>' . _t('SiteTree.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
1923
			$dependentTable = GridField::create(
1924
				'DependentPages',
1925
				false,
1926
				$dependentPages
1927
			);
1928
			/** @var GridFieldDataColumns $dataColumns */
1929
			$dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1930
			$dataColumns
1931
				->setDisplayFields($dependentColumns)
1932
				->setFieldFormatting(array(
1933
					'Title' => function($value, &$item) {
1934
						return sprintf(
1935
							'<a href="admin/pages/edit/show/%d">%s</a>',
1936
							(int)$item->ID,
1937
							Convert::raw2xml($item->Title)
1938
						);
1939
					},
1940
					'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...
1941
						return sprintf(
1942
							'<a href="%s" target="_blank">%s</a>',
1943
							Convert::raw2xml($value),
1944
							Convert::raw2xml($value)
1945
						);
1946
					}
1947
				));
1948
		}
1949
1950
		$baseLink = Controller::join_links (
1951
			Director::absoluteBaseURL(),
1952
			(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
1953
		);
1954
1955
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1956
			->setURLPrefix($baseLink)
1957
			->setDefaultURL($this->generateURLSegment(_t(
1958
				'CMSMain.NEWPAGE',
1959
				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...
1960
			)));
1961
		$helpText = (self::config()->nested_urls && $this->Children()->count())
1962
			? $this->fieldLabel('LinkChangeNote')
1963
			: '';
1964
		if(!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
1965
			$helpText .= $helpText ? '<br />' : '';
1966
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1967
		}
1968
		$urlsegment->setHelpText($helpText);
1969
1970
		$fields = new FieldList(
1971
			$rootTab = new TabSet("Root",
1972
				$tabMain = new Tab('Main',
1973
					new TextField("Title", $this->fieldLabel('Title')),
1974
					$urlsegment,
1975
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1976
					$htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1977
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1978
						array(
1979
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1980
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1981
						)
1982
					)->setHeadingLevel(4)
1983
				),
1984
				$tabDependent = new Tab('Dependent',
1985
					$dependentNote,
1986
					$dependentTable
1987
				)
1988
			)
1989
		);
1990
		$htmlField->addExtraClass('stacked');
1991
1992
		// Help text for MetaData on page content editor
1993
		$metaFieldDesc
1994
			->setRightTitle(
1995
				_t(
1996
					'SiteTree.METADESCHELP',
1997
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1998
				)
1999
			)
2000
			->addExtraClass('help');
2001
		$metaFieldExtra
2002
			->setRightTitle(
2003
				_t(
2004
					'SiteTree.METAEXTRAHELP',
2005
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2006
				)
2007
			)
2008
			->addExtraClass('help');
2009
2010
		// Conditional dependent pages tab
2011
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2012
		else $fields->removeFieldFromTab('Root', 'Dependent');
2013
2014
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2015
2016
		if($this->ObsoleteClassName) {
2017
			$obsoleteWarning = _t(
2018
				'SiteTree.OBSOLETECLASS',
2019
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2020
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Documentation introduced by
array('type' => $this->ObsoleteClassName) is of type array<string,?,{"type":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2021
			);
2022
2023
			$fields->addFieldToTab(
2024
				"Root.Main",
2025
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2026
				"Title"
2027
			);
2028
		}
2029
2030
		if(file_exists(BASE_PATH . '/install.php')) {
2031
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2032
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2033
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2034
				. "</p>"), "Title");
2035
		}
2036
2037
		if(self::$runCMSFieldsExtensions) {
2038
			$this->extend('updateCMSFields', $fields);
2039
		}
2040
2041
		return $fields;
2042
	}
2043
2044
2045
	/**
2046
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2047
	 * for content-related fields.
2048
	 *
2049
	 * @return FieldList
2050
	 */
2051
	public function getSettingsFields() {
2052
		$groupsMap = array();
2053
		foreach(Group::get() as $group) {
2054
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2055
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2056
		}
2057
		asort($groupsMap);
2058
2059
		$fields = new FieldList(
2060
			$rootTab = new TabSet("Root",
2061
				$tabBehaviour = new Tab('Settings',
2062
					new DropdownField(
2063
						"ClassName",
2064
						$this->fieldLabel('ClassName'),
2065
						$this->getClassDropdown()
2066
					),
2067
					$parentTypeSelector = new CompositeField(
2068
						$parentType = new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2069
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2070
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2071
						)),
2072
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SilverStripe\\CMS\\Model\\SiteTree', 'ID', 'MenuTitle')
2073
					),
2074
					$visibility = new FieldGroup(
2075
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2076
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2077
					),
2078
					$viewersOptionsField = new OptionsetField(
2079
						"CanViewType",
2080
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2081
					),
2082
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2083
						->setSource($groupsMap)
2084
						->setAttribute(
2085
							'data-placeholder',
2086
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2087
						),
2088
					$editorsOptionsField = new OptionsetField(
2089
						"CanEditType",
2090
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2091
					),
2092
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2093
						->setSource($groupsMap)
2094
						->setAttribute(
2095
							'data-placeholder',
2096
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2097
						)
2098
				)
2099
			)
2100
		);
2101
2102
		$parentType->addExtraClass('noborder');
2103
		$visibility->setTitle($this->fieldLabel('Visibility'));
2104
2105
2106
		// This filter ensures that the ParentID dropdown selection does not show this node,
2107
		// or its descendents, as this causes vanishing bugs
2108
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2109
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2110
2111
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2112
2113
		// Make page location fields read-only if the user doesn't have the appropriate permission
2114
		if(!Permission::check("SITETREE_REORGANISE")) {
2115
			$fields->makeFieldReadonly('ParentType');
2116
			if($this->getParentType() === 'root') {
2117
				$fields->removeByName('ParentID');
2118
			} else {
2119
				$fields->makeFieldReadonly('ParentID');
2120
			}
2121
		}
2122
2123
		$viewersOptionsSource = array();
2124
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2125
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2126
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2127
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2128
		$viewersOptionsField->setSource($viewersOptionsSource);
2129
2130
		$editorsOptionsSource = array();
2131
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2132
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2133
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2134
		$editorsOptionsField->setSource($editorsOptionsSource);
2135
2136
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2137
			$fields->makeFieldReadonly($viewersOptionsField);
2138
			if($this->CanViewType == 'OnlyTheseUsers') {
2139
				$fields->makeFieldReadonly($viewerGroupsField);
2140
			} else {
2141
				$fields->removeByName('ViewerGroups');
2142
			}
2143
2144
			$fields->makeFieldReadonly($editorsOptionsField);
2145
			if($this->CanEditType == 'OnlyTheseUsers') {
2146
				$fields->makeFieldReadonly($editorGroupsField);
2147
			} else {
2148
				$fields->removeByName('EditorGroups');
2149
			}
2150
		}
2151
2152
		if(self::$runCMSFieldsExtensions) {
2153
			$this->extend('updateSettingsFields', $fields);
2154
		}
2155
2156
		return $fields;
2157
	}
2158
2159
	/**
2160
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2161
	 * @return array
2162
	 */
2163
	public function fieldLabels($includerelations = true) {
2164
		$cacheKey = $this->class . '_' . $includerelations;
2165
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2166
			$labels = parent::fieldLabels($includerelations);
2167
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2168
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2169
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2170
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2171
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2172
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2173
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2174
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2175
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2176
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2177
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2178
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2179
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2180
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2181
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2182
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2183
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2184
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2185
			$labels['LinkChangeNote'] = _t (
2186
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2187
			);
2188
2189
			if($includerelations){
2190
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2191
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2192
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2193
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2194
			}
2195
2196
			self::$_cache_field_labels[$cacheKey] = $labels;
2197
		}
2198
2199
		return self::$_cache_field_labels[$cacheKey];
2200
	}
2201
2202
	/**
2203
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2204
	 *
2205
	 * Frontend scripts and styles know how to handle the following FormFields:
2206
	 * - top-level FormActions appear as standalone buttons
2207
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2208
	 * - TabSet & Tabs appear as a drop ups
2209
	 * - FormActions within the Tab are restyled as links
2210
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2211
	 *
2212
	 * @return FieldList The available actions for this page.
2213
	 */
2214
	public function getCMSActions() {
2215
		$existsOnLive = $this->isPublished();
2216
2217
		// Major actions appear as buttons immediately visible as page actions.
2218
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('btn-group ss-ui-buttonset noborder');
2219
2220
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2221
		$rootTabSet = new TabSet('ActionMenus');
2222
		$moreOptions = new Tab(
2223
			'MoreOptions',
2224
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2225
		);
2226
		$rootTabSet->push($moreOptions);
2227
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2228
2229
		// Render page information into the "more-options" drop-up, on the top.
2230
		$live = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 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...
2231
			'"SiteTree"."ID"' => $this->ID
2232
		));
2233
		$infoTemplate = SSViewer::get_templates_by_class(get_class($this), '_Information', __CLASS__);
2234
		$moreOptions->push(
2235
			new LiteralField('Information',
2236
				$this->customise(array(
2237
					'Live' => $live,
2238
					'ExistsOnLive' => $existsOnLive
2239
				))->renderWith($infoTemplate)
2240
			)
2241
		);
2242
2243
		$moreOptions->push(AddToCampaignHandler_FormAction::create());
2244
2245
		// "readonly"/viewing version that isn't the current version of the record
2246
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), 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...
2247
			'"SiteTree"."ID"' => $this->ID
2248
		));
2249
		if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
2250
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'SilverStripe\\Control\\Email\\Email')));
2251
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2252
2253
			$actions = new FieldList(array($majorActions, $rootTabSet));
2254
2255
			// getCMSActions() can be extended with updateCMSActions() on a extension
2256
			$this->extend('updateCMSActions', $actions);
2257
2258
			return $actions;
2259
		}
2260
2261
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
2262
			// "unpublish"
2263
			$moreOptions->push(
2264
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2265
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2266
					->addExtraClass('ss-ui-action-destructive')
2267
			);
2268
		}
2269
2270
		if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE) && !$this->getIsDeletedFromStage()) {
2271
			if($this->isPublished() && $this->canEdit())	{
2272
				// "rollback"
2273
				$moreOptions->push(
2274
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2275
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2276
				);
2277
			}
2278
		}
2279
2280
		if($this->canEdit()) {
2281
			if($this->getIsDeletedFromStage()) {
2282
				// The usual major actions are not available, so we provide alternatives here.
2283
				if($existsOnLive) {
2284
					// "restore"
2285
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2286
					if($this->canDelete() && $this->canUnpublish()) {
2287
						// "delete from live"
2288
						$majorActions->push(
2289
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2290
								->addExtraClass('ss-ui-action-destructive')
2291
						);
2292
					}
2293
				} else {
2294
					// Determine if we should force a restore to root (where once it was a subpage)
2295
					$restoreToRoot = $this->isParentArchived();
2296
2297
					// "restore"
2298
					$title = $restoreToRoot
2299
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2300
						: _t('CMSMain.RESTORE','Restore draft');
2301
					$description = $restoreToRoot
2302
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2303
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2304
					$majorActions->push(
2305
						FormAction::create('restore', $title)
2306
							->setDescription($description)
2307
							->setAttribute('data-to-root', $restoreToRoot)
2308
							->setAttribute('data-icon', 'decline')
2309
					);
2310
				}
2311
			} else {
2312
					if($this->canDelete()) {
2313
						// delete
2314
						$moreOptions->push(
2315
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2316
								->addExtraClass('delete ss-ui-action-destructive')
2317
						);
2318
					}
2319
				if($this->canArchive()) {
2320
					// "archive"
2321
					$moreOptions->push(
2322
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2323
							->setDescription(_t(
2324
								'SiteTree.BUTTONARCHIVEDESC',
2325
								'Unpublish and send to archive'
2326
							))
2327
							->addExtraClass('delete ss-ui-action-destructive')
2328
					);
2329
				}
2330
2331
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2332
				$majorActions->push(
2333
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2334
						->setAttribute('data-icon', 'accept')
2335
						->setAttribute('data-icon-alternate', 'addpage')
2336
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2337
				);
2338
			}
2339
		}
2340
2341
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
2342
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2343
			$majorActions->push(
2344
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2345
					->setAttribute('data-icon', 'accept')
2346
					->setAttribute('data-icon-alternate', 'disk')
2347
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2348
			);
2349
2350
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2351
			if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE)) {
2352
				$publish->addExtraClass('ss-ui-alternate');
2353
			}
2354
		}
2355
2356
		$actions = new FieldList(array($majorActions, $rootTabSet));
2357
2358
		// Hook for extensions to add/remove actions.
2359
		$this->extend('updateCMSActions', $actions);
2360
2361
		return $actions;
2362
	}
2363
2364
	public function onAfterPublish() {
2365
		// Force live sort order to match stage sort order
2366
		DB::prepared_query('UPDATE "SiteTree_Live"
2367
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2368
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2369
			array($this->ParentID)
2370
		);
2371
		}
2372
2373
	/**
2374
	 * Update draft dependant pages
2375
	 */
2376
	public function onAfterRevertToLive() {
2377
		// Use an alias to get the updates made by $this->publish
2378
		/** @var SiteTree $stageSelf */
2379
		$stageSelf = Versioned::get_by_stage('SilverStripe\\CMS\\Model\\SiteTree', Versioned::DRAFT)->byID($this->ID);
2380
		$stageSelf->writeWithoutVersion();
2381
2382
		// Need to update pages linking to this one as no longer broken
2383
		foreach($stageSelf->DependentPages() as $page) {
2384
			/** @var SiteTree $page */
2385
			$page->writeWithoutVersion();
2386
		}
2387
	}
2388
2389
	/**
2390
	 * Determine if this page references a parent which is archived, and not available in stage
2391
	 *
2392
	 * @return bool True if there is an archived parent
2393
	 */
2394
	protected function isParentArchived() {
2395
		if($parentID = $this->ParentID) {
2396
			$parentPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
2397
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2398
				return true;
2399
			}
2400
		}
2401
		return false;
2402
	}
2403
2404
	/**
2405
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2406
	 *
2407
	 * @return self
2408
	 */
2409
	public function doRestoreToStage() {
2410
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2411
2412
		// Ensure that the parent page is restored, otherwise restore to root
2413
		if($this->isParentArchived()) {
2414
			$this->ParentID = 0;
2415
		}
2416
2417
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2418
		// create an empty record
2419
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2420
			$conn = DB::get_conn();
2421
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SilverStripe\\CMS\\Model\\SiteTree', true);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\SS_Database>.

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

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

Loading history...
2422
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2423
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SilverStripe\\CMS\\Model\\SiteTree', false);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\SS_Database>.

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

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

Loading history...
2424
		}
2425
2426
		$oldReadingMode = Versioned::get_reading_mode();
2427
		Versioned::set_stage(Versioned::DRAFT);
2428
		$this->forceChange();
2429
		$this->write();
2430
2431
		/** @var SiteTree $result */
2432
		$result = DataObject::get_by_id(__CLASS__, $this->ID);
2433
2434
		// Need to update pages linking to this one as no longer broken
2435
		foreach($result->DependentPages(false) as $page) {
2436
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2437
			$page->write();
2438
		}
2439
2440
		Versioned::set_reading_mode($oldReadingMode);
2441
2442
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2443
2444
		return $result;
2445
	}
2446
2447
	/**
2448
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2449
	 *
2450
	 * @return bool
2451
	 */
2452
	public function isNew() {
2453
		/**
2454
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2455
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2456
		 */
2457
		if(empty($this->ID)) return true;
2458
2459
		if(is_numeric($this->ID)) return false;
2460
2461
		return stripos($this->ID, 'new') === 0;
2462
	}
2463
2464
	/**
2465
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2466
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2467
	 * {@link SiteTree::$needs_permission}.
2468
	 *
2469
	 * @return array
2470
	 */
2471
	protected function getClassDropdown() {
2472
		$classes = self::page_type_classes();
2473
		$currentClass = null;
2474
2475
		$result = array();
2476
		foreach($classes as $class) {
2477
			$instance = singleton($class);
2478
2479
			// if the current page type is this the same as the class type always show the page type in the list
2480
			if ($this->ClassName != $instance->ClassName) {
2481
				if($instance instanceof HiddenClass) continue;
2482
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
2483
			}
2484
2485
			if($perms = $instance->stat('need_permission')) {
2486
				if(!$this->can($perms)) continue;
2487
			}
2488
2489
			$pageTypeName = $instance->i18n_singular_name();
2490
2491
			$currentClass = $class;
2492
			$result[$class] = $pageTypeName;
2493
2494
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2495
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2496
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2497
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2498
				$result[$class] = $result[$class] .  " ({$class})";
2499
			}
2500
		}
2501
2502
		// sort alphabetically, and put current on top
2503
		asort($result);
2504
		if($currentClass) {
2505
			$currentPageTypeName = $result[$currentClass];
2506
			unset($result[$currentClass]);
2507
			$result = array_reverse($result);
2508
			$result[$currentClass] = $currentPageTypeName;
2509
			$result = array_reverse($result);
2510
		}
2511
2512
		return $result;
2513
	}
2514
2515
	/**
2516
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2517
	 *
2518
	 * @return string[]
2519
	 */
2520
	public function allowedChildren() {
2521
		$allowedChildren = array();
2522
		$candidates = $this->stat('allowed_children');
2523
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2524
			foreach($candidates as $candidate) {
0 ignored issues
show
Bug introduced by
The expression $candidates of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2525
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2526
				// Otherwise, the class and all its subclasses are allowed.
2527
				if(substr($candidate,0,1) == '*') {
2528
					$allowedChildren[] = substr($candidate,1);
2529
				} else {
2530
					$subclasses = ClassInfo::subclassesFor($candidate);
2531
					foreach($subclasses as $subclass) {
2532
						if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2533
							continue;
2534
						}
2535
						$allowedChildren[] = $subclass;
2536
					}
2537
				}
2538
			}
2539
		}
2540
2541
		return $allowedChildren;
2542
	}
2543
2544
	/**
2545
	 * Returns the class name of the default class for children of this page.
2546
	 *
2547
	 * @return string
2548
	 */
2549
	public function defaultChild() {
2550
		$default = $this->stat('default_child');
2551
		$allowed = $this->allowedChildren();
2552
		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...
2553
			if(!$default || !in_array($default, $allowed)) {
2554
				$default = reset($allowed);
2555
			}
2556
			return $default;
2557
		}
2558
		return null;
2559
	}
2560
2561
	/**
2562
	 * Returns the class name of the default class for the parent of this page.
2563
	 *
2564
	 * @return string
2565
	 */
2566
	public function defaultParent() {
2567
		return $this->stat('default_parent');
2568
	}
2569
2570
	/**
2571
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2572
	 * Title field.
2573
	 *
2574
	 * @return string
2575
	 */
2576
	public function getMenuTitle(){
2577
		if($value = $this->getField("MenuTitle")) {
2578
			return $value;
2579
		} else {
2580
			return $this->getField("Title");
2581
		}
2582
	}
2583
2584
2585
	/**
2586
	 * Set the menu title for this page.
2587
	 *
2588
	 * @param string $value
2589
	 */
2590
	public function setMenuTitle($value) {
2591
		if($value == $this->getField("Title")) {
2592
			$this->setField("MenuTitle", null);
2593
		} else {
2594
			$this->setField("MenuTitle", $value);
2595
		}
2596
	}
2597
2598
	/**
2599
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2600
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2601
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2602
	 * the flags.
2603
	 *
2604
	 * Example (simple):
2605
	 *   "deletedonlive" => "Deleted"
2606
	 *
2607
	 * Example (with optional title attribute):
2608
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2609
	 *
2610
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2611
	 * @return array
2612
	 */
2613
	public function getStatusFlags($cached = true) {
2614
		if(!$this->_cache_statusFlags || !$cached) {
2615
			$flags = array();
2616
			if($this->getIsDeletedFromStage()) {
2617
				if($this->isPublished()) {
2618
					$flags['removedfromdraft'] = array(
2619
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2620
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2621
					);
2622
				} else {
2623
					$flags['archived'] = array(
2624
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2625
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2626
					);
2627
				}
2628
			} else if($this->getIsAddedToStage()) {
2629
				$flags['addedtodraft'] = array(
2630
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2631
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2632
				);
2633
			} else if($this->getIsModifiedOnStage()) {
2634
				$flags['modified'] = array(
2635
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2636
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2637
				);
2638
			}
2639
2640
			$this->extend('updateStatusFlags', $flags);
2641
2642
			$this->_cache_statusFlags = $flags;
2643
		}
2644
2645
		return $this->_cache_statusFlags;
2646
	}
2647
2648
	/**
2649
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2650
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2651
	 * publication status.
2652
	 *
2653
	 * @return string An HTML string ready to be directly used in a template
2654
	 */
2655
	public function getTreeTitle() {
2656
		// Build the list of candidate children
2657
		$children = array();
2658
		$candidates = static::page_type_classes();
2659
		foreach($this->allowedChildren() as $childClass) {
2660
			if(!in_array($childClass, $candidates)) continue;
2661
			$child = singleton($childClass);
2662
			if($child->canCreate(null, array('Parent' => $this))) {
2663
				$children[$childClass] = $child->i18n_singular_name();
2664
			}
2665
		}
2666
		$flags = $this->getStatusFlags();
2667
		$treeTitle = sprintf(
2668
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2669
			Convert::raw2att(Convert::raw2json($children)),
2670
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2671
		);
2672
		foreach($flags as $class => $data) {
2673
			if(is_string($data)) $data = array('text' => $data);
2674
			$treeTitle .= sprintf(
2675
				"<span class=\"badge %s\"%s>%s</span>",
2676
				'status-' . Convert::raw2xml($class),
2677
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2678
				Convert::raw2xml($data['text'])
2679
			);
2680
		}
2681
2682
		return $treeTitle;
2683
	}
2684
2685
	/**
2686
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2687
	 * we're currently inside, etc.
2688
	 *
2689
	 * @param int $level
2690
	 * @return SiteTree
2691
	 */
2692
	public function Level($level) {
2693
		$parent = $this;
2694
		$stack = array($parent);
2695
		while(($parent = $parent->Parent()) && $parent->exists()) {
2696
			array_unshift($stack, $parent);
2697
		}
2698
2699
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2700
	}
2701
2702
	/**
2703
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2704
	 *
2705
	 * @return int
2706
	 */
2707
	public function getPageLevel() {
2708
		if($this->ParentID) {
2709
			return 1 + $this->Parent()->getPageLevel();
2710
		}
2711
		return 1;
2712
	}
2713
2714
	/**
2715
	 * @param string $action
2716
	 * @return Controller
2717
	 */
2718
	public function getAssociatedController($action = null) {
2719
		$controllerName = $this->getControllerName($action);
2720
		return Injector::inst()->create($controllerName, $this);
2721
	}
2722
2723
	/**
2724
	 * @param string $action
2725
	 * @return string
2726
	 */
2727
	public function getControllerName($action = null) {
2728
		$action = ucfirst($action); // Needed as Injector service names are case sensitive
2729
		$ancestry = ClassInfo::ancestry($this->class);
2730
2731
		$controller = null;
2732
		while ($class = array_pop($ancestry)) {
2733
			if ($class === 'SilverStripe\\CMS\\Model\\SiteTree') {
2734
				break;
2735
			} else if (class_exists("{$class}_Controller")) {
2736
				$controller = "{$class}_Controller";
2737
				break;
2738
			} else if ($action && class_exists("{$class}_Controller_{$action}")) {
2739
				$controller = "{$class}_Controller_{$action}";
2740
				break;
2741
			}
2742
		}
2743
2744
		if (!$controller) {
2745
			$controller = 'SilverStripe\\CMS\\Controllers\\ContentController';
2746
		}
2747
2748
		return $controller;
2749
	}
2750
2751
	/**
2752
	 * Return the CSS classes to apply to this node in the CMS tree.
2753
	 *
2754
	 * @param string $numChildrenMethod
2755
	 * @return string
2756
	 */
2757
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2758
		$classes = sprintf('class-%s', $this->class);
2759
		if($this->HasBrokenFile || $this->HasBrokenLink) {
2760
			$classes .= " BrokenLink";
2761
		}
2762
2763
		if(!$this->canAddChildren()) {
2764
			$classes .= " nochildren";
2765
		}
2766
2767
		if(!$this->canEdit() && !$this->canAddChildren()) {
2768
			if (!$this->canView()) {
2769
				$classes .= " disabled";
2770
			} else {
2771
				$classes .= " edit-disabled";
2772
			}
2773
		}
2774
2775
		if(!$this->ShowInMenus) {
2776
			$classes .= " notinmenu";
2777
		}
2778
2779
		//TODO: Add integration
2780
		/*
2781
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2782
			$classes .= " untranslated ";
2783
		*/
2784
		$classes .= $this->markingClasses($numChildrenMethod);
2785
2786
		return $classes;
2787
	}
2788
2789
	/**
2790
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2791
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2792
	 *
2793
	 * @return bool
2794
	 */
2795
	public function getIsDeletedFromStage() {
2796
		if(!$this->ID) return true;
2797
		if($this->isNew()) return false;
2798
2799
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', Versioned::DRAFT, $this->ID);
2800
2801
		// Return true for both completely deleted pages and for pages just deleted from stage
2802
		return !($stageVersion);
2803
	}
2804
2805
	/**
2806
	 * Return true if this page exists on the live site
2807
	 *
2808
	 * @return bool
2809
	 */
2810
	public function getExistsOnLive() {
2811
		return $this->isPublished();
2812
	}
2813
2814
	/**
2815
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2816
	 * unpublished changes to the draft site.
2817
	 *
2818
	 * @return bool
2819
	 */
2820
	public function getIsModifiedOnStage() {
2821
		// New unsaved pages could be never be published
2822
		if($this->isNew()) return false;
2823
2824
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Stage', $this->ID);
2825
		$liveVersion =	Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->ID);
2826
2827
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2828
		$this->extend('getIsModifiedOnStage', $isModified);
2829
2830
		return $isModified;
2831
	}
2832
2833
	/**
2834
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2835
	 * published.
2836
	 *
2837
	 * @return bool
2838
	 */
2839
	public function getIsAddedToStage() {
2840
		// New unsaved pages could be never be published
2841
		if($this->isNew()) return false;
2842
2843
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Stage', $this->ID);
2844
		$liveVersion =	Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->ID);
2845
2846
		return ($stageVersion && !$liveVersion);
2847
	}
2848
2849
	/**
2850
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2851
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2852
	 */
2853
	static public function disableCMSFieldsExtensions() {
2854
		self::$runCMSFieldsExtensions = false;
2855
	}
2856
2857
	/**
2858
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2859
	 * disableCMSFieldsExtensions().
2860
	 */
2861
	static public function enableCMSFieldsExtensions() {
2862
		self::$runCMSFieldsExtensions = true;
2863
	}
2864
2865
	public function providePermissions() {
2866
		return array(
2867
			'SITETREE_GRANT_ACCESS' => array(
2868
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2869
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2870
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2871
				'sort' => 100
2872
			),
2873
			'SITETREE_VIEW_ALL' => array(
2874
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2875
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2876
				'sort' => -100,
2877
				'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')
2878
			),
2879
			'SITETREE_EDIT_ALL' => array(
2880
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2881
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2882
				'sort' => -50,
2883
				'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')
2884
			),
2885
			'SITETREE_REORGANISE' => array(
2886
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2887
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2888
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2889
				'sort' => 100
2890
			),
2891
			'VIEW_DRAFT_CONTENT' => array(
2892
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2893
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2894
				'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.'),
2895
				'sort' => 100
2896
			)
2897
		);
2898
	}
2899
2900
	/**
2901
	 * Return the translated Singular name.
2902
	 *
2903
	 * @return string
2904
	 */
2905
	public function i18n_singular_name() {
2906
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2907
		/** @skipUpgrade */
2908
		// @todo When we namespace translations, change 'SiteTree' to FQN of the class
2909
		$class = ($this->class == 'Page' || $this->class === __CLASS__)
2910
			? 'SiteTree'
2911
			: $this->class;
2912
		return _t($class.'.SINGULARNAME', $this->singular_name());
2913
	}
2914
2915
	/**
2916
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2917
	 * picks it up for the wrong folder.
2918
	 *
2919
	 * @return array
2920
	 */
2921
	public function provideI18nEntities() {
2922
		$entities = parent::provideI18nEntities();
2923
2924
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2925
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2926
2927
		$entities[$this->class . '.DESCRIPTION'] = array(
2928
			$this->stat('description'),
2929
			'Description of the page type (shown in the "add page" dialog)'
2930
		);
2931
2932
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2933
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2934
2935
		return $entities;
0 ignored issues
show
Best Practice introduced by
The expression return $entities; seems to be an array, but some of its elements' types (null) are incompatible with the return type of the parent method SilverStripe\ORM\DataObject::provideI18nEntities of type array<*,array<array|inte...double|string|boolean>>.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2936
	}
2937
2938
	/**
2939
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2940
	 *
2941
	 * @return string
2942
	 */
2943
	public function getParentType() {
2944
		return $this->ParentID == 0 ? 'root' : 'subpage';
2945
	}
2946
2947
	/**
2948
	 * Clear the permissions cache for SiteTree
2949
	 */
2950
	public static function reset() {
2951
		self::$cache_permissions = array();
2952
	}
2953
2954
	static public function on_db_reset() {
2955
		self::$cache_permissions = array();
2956
	}
2957
2958
}
2959