Completed
Pull Request — master (#1605)
by Loz
04:06
created

SiteTree::getControllerName()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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