Completed
Push — master ( 25a8fb...29fc7a )
by Hamish
12s
created

SiteTree::getCMSFields()   F

Complexity

Conditions 22
Paths 4608

Size

Total Lines 185
Code Lines 122

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 185
rs 2
c 2
b 0
f 0
cc 22
eloc 122
nc 4608
nop 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use GridFieldDataColumns;
6
use ShortcodeParser;
7
use SilverStripe\CMS\Controllers\CMSPageEditController;
8
use SilverStripe\CMS\Controllers\ContentController;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\Hierarchy\Hierarchy;
11
use SilverStripe\ORM\ManyManyList;
12
use SilverStripe\ORM\Versioning\Versioned;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\ORM\DB;
15
use SilverStripe\ORM\DataList;
16
use SilverStripe\ORM\HiddenClass;
17
use SilverStripe\Security\Member;
18
use SilverStripe\Security\Permission;
19
use SilverStripe\Security\Group;
20
use SilverStripe\Security\PermissionProvider;
21
use i18nEntityProvider;
22
use CMSPreviewable;
23
use Director;
24
use SilverStripe\CMS\Controllers\RootURLController;
25
use ClassInfo;
26
use Convert;
27
use Controller;
28
use Deprecation;
29
use SSViewer;
30
use ArrayData;
31
use SiteConfig;
32
use FormField;
33
use Config;
34
use Page;
35
use URLSegmentFilter;
36
use SilverStripe\CMS\Controllers\ModelAsController;
37
use Subsite;
38
use LiteralField;
39
use GridField;
40
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
41
use FieldList;
42
use TabSet;
43
use Tab;
44
use TextField;
45
use HTMLEditorField;
46
use ToggleCompositeField;
47
use TextareaField;
48
use DropdownField;
49
use CompositeField;
50
use OptionsetField;
51
use TreeDropdownField;
52
use FieldGroup;
53
use CheckboxField;
54
use ListboxField;
55
use AddToCampaignHandler_FormAction;
56
use FormAction;
57
use i18n;
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
 * @package cms
93
 */
94
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
95
96
	/**
97
	 * Indicates what kind of children this page type can have.
98
	 * This can be an array of allowed child classes, or the string "none" -
99
	 * indicating that this page type can't have children.
100
	 * If a classname is prefixed by "*", such as "*Page", then only that
101
	 * class is allowed - no subclasses. Otherwise, the class and all its
102
	 * subclasses are allowed.
103
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
104
	 *
105
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
106
	 *
107
	 * @config
108
	 * @var array
109
	 */
110
	private static $allowed_children = array("SilverStripe\\CMS\\Model\\SiteTree");
111
112
	/**
113
	 * The default child class for this page.
114
	 * Note: Value might be cached, see {@link $allowed_chilren}.
115
	 *
116
	 * @config
117
	 * @var string
118
	 */
119
	private static $default_child = "Page";
120
121
	/**
122
	 * Default value for SiteTree.ClassName enum
123
	 * {@see DBClassName::getDefault}
124
	 *
125
	 * @config
126
	 * @var string
127
	 */
128
	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...
129
130
	/**
131
	 * The default parent class for this page.
132
	 * Note: Value might be cached, see {@link $allowed_chilren}.
133
	 *
134
	 * @config
135
	 * @var string
136
	 */
137
	private static $default_parent = null;
138
139
	/**
140
	 * Controls whether a page can be in the root of the site tree.
141
	 * Note: Value might be cached, see {@link $allowed_chilren}.
142
	 *
143
	 * @config
144
	 * @var bool
145
	 */
146
	private static $can_be_root = true;
147
148
	/**
149
	 * List of permission codes a user can have to allow a user to create a page of this type.
150
	 * Note: Value might be cached, see {@link $allowed_chilren}.
151
	 *
152
	 * @config
153
	 * @var array
154
	 */
155
	private static $need_permission = null;
156
157
	/**
158
	 * If you extend a class, and don't want to be able to select the old class
159
	 * in the cms, set this to the old class name. Eg, if you extended Product
160
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
161
	 *
162
	 * @config
163
	 * @var string
164
	 */
165
	private static $hide_ancestor = null;
166
167
	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...
168
		"URLSegment" => "Varchar(255)",
169
		"Title" => "Varchar(255)",
170
		"MenuTitle" => "Varchar(100)",
171
		"Content" => "HTMLText",
172
		"MetaDescription" => "Text",
173
		"ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
174
		"ShowInMenus" => "Boolean",
175
		"ShowInSearch" => "Boolean",
176
		"Sort" => "Int",
177
		"HasBrokenFile" => "Boolean",
178
		"HasBrokenLink" => "Boolean",
179
		"ReportClass" => "Varchar",
180
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
181
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
182
	);
183
184
	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...
185
		"URLSegment" => true,
186
	);
187
188
	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...
189
		"ViewerGroups" => "SilverStripe\\Security\\Group",
190
		"EditorGroups" => "SilverStripe\\Security\\Group",
191
	);
192
193
	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...
194
		"VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
195
	);
196
197
	private static $owned_by = array(
198
		"VirtualPages"
199
	);
200
201
	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...
202
		"Breadcrumbs" => "HTMLFragment",
203
		"LastEdited" => "Datetime",
204
		"Created" => "Datetime",
205
		'Link' => 'Text',
206
		'RelativeLink' => 'Text',
207
		'AbsoluteLink' => 'Text',
208
		'CMSEditLink' => 'Text',
209
		'TreeTitle' => 'HTMLFragment',
210
		'MetaTags' => 'HTMLFragment',
211
	);
212
213
	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...
214
		"ShowInMenus" => 1,
215
		"ShowInSearch" => 1,
216
		"CanViewType" => "Inherit",
217
		"CanEditType" => "Inherit"
218
	);
219
220
	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...
221
222
	private static $versioning = array(
223
		"Stage",  "Live"
224
	);
225
226
	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...
227
228
	/**
229
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
230
	 * @var boolean
231
	 * @config
232
	 */
233
	private static $can_create = true;
234
235
	/**
236
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
237
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
238
	 *
239
	 * @see CMSMain::generateTreeStylingCSS()
240
	 * @config
241
	 * @var string
242
	 */
243
	private static $icon = null;
244
245
	/**
246
	 * @config
247
	 * @var string Description of the class functionality, typically shown to a user
248
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
249
	 */
250
	private static $description = 'Generic content page';
251
252
	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...
253
		'SilverStripe\ORM\Hierarchy\Hierarchy',
254
		'SilverStripe\ORM\Versioning\Versioned',
255
		"SilverStripe\\CMS\\Model\\SiteTreeLinkTracking"
256
	);
257
258
	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...
259
		'Title',
260
		'Content',
261
	);
262
263
	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...
264
		'URLSegment' => 'URL'
265
	);
266
267
	/**
268
	 * @config
269
	 */
270
	private static $nested_urls = true;
271
272
	/**
273
	 * @config
274
	*/
275
	private static $create_default_pages = true;
276
277
	/**
278
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
279
	 */
280
	private static $runCMSFieldsExtensions = true;
281
282
	/**
283
	 * Cache for canView/Edit/Publish/Delete permissions.
284
	 * Keyed by permission type (e.g. 'edit'), with an array
285
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
286
	 * See {@link batch_permission_check()} for details.
287
	 */
288
	private static $cache_permissions = array();
289
290
	/**
291
	 * @config
292
	 * @var boolean
293
	 */
294
	private static $enforce_strict_hierarchy = true;
295
296
	/**
297
	 * The value used for the meta generator tag. Leave blank to omit the tag.
298
	 *
299
	 * @config
300
	 * @var string
301
	 */
302
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
303
304
	protected $_cache_statusFlags = null;
305
306
	/**
307
	 * Fetches the {@link SiteTree} object that maps to a link.
308
	 *
309
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
310
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
311
	 *
312
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
313
	 * by a extension attached to {@link SiteTree}
314
	 *
315
	 * @param string $link  The link of the page to search for
316
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
317
	 * @return SiteTree
318
	 */
319
	static public function get_by_link($link, $cache = true) {
320
		if(trim($link, '/')) {
321
			$link = trim(Director::makeRelative($link), '/');
322
		} else {
323
			$link = RootURLController::get_homepage_link();
324
		}
325
326
		$parts = preg_split('|/+|', $link);
327
328
		// Grab the initial root level page to traverse down from.
329
		$URLSegment = array_shift($parts);
330
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
331
		if(self::config()->nested_urls) {
332
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
333
		}
334
		/** @var SiteTree $sitetree */
335
		$sitetree = DataObject::get_one(__CLASS__, $conditions, $cache);
336
337
		/// Fall back on a unique URLSegment for b/c.
338
		if(	!$sitetree
339
			&& self::config()->nested_urls
340
			&& $sitetree = DataObject::get_one(__CLASS__, array(
341
				'"SiteTree"."URLSegment"' => $URLSegment
342
			), $cache)
343
		) {
344
			return $sitetree;
345
		}
346
347
		// Attempt to grab an alternative page from extensions.
348
		if(!$sitetree) {
349
			$parentID = self::config()->nested_urls ? 0 : null;
350
351 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...
352
				foreach($alternatives as $alternative) {
353
					if($alternative) {
354
						$sitetree = $alternative;
355
					}
356
				}
357
			}
358
359
			if(!$sitetree) {
360
				return null;
361
			}
362
		}
363
364
		// Check if we have any more URL parts to parse.
365
		if(!self::config()->nested_urls || !count($parts)) {
366
			return $sitetree;
367
		}
368
369
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
370
		foreach($parts as $segment) {
371
			$next = DataObject::get_one('SilverStripe\\CMS\\Model\\SiteTree', array(
372
					'"SiteTree"."URLSegment"' => $segment,
373
					'"SiteTree"."ParentID"' => $sitetree->ID
374
				),
375
				$cache
376
			);
377
378
			if(!$next) {
379
				$parentID = (int) $sitetree->ID;
380
381 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...
382
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
383
				}
384
385
				if(!$next) {
386
					return null;
387
				}
388
			}
389
390
			$sitetree->destroy();
391
			$sitetree = $next;
392
		}
393
394
		return $sitetree;
395
	}
396
397
	/**
398
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
399
	 *
400
	 * @return array
401
	 */
402
	public static function page_type_classes() {
403
		$classes = ClassInfo::getValidSubClasses();
404
405
		$baseClassIndex = array_search('SilverStripe\\CMS\\Model\\SiteTree', $classes);
406
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
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
		if (
451
			   !($page = DataObject::get_by_id(__CLASS__, $arguments['id']))         // Get the current page by ID.
452
			&& !($page = Versioned::get_latest_version(__CLASS__, $arguments['id'])) // Attempt link to old version.
453
		) {
454
			 return null; // There were no suitable matches at all.
455
		}
456
457
		/** @var SiteTree $page */
458
		$link = Convert::raw2att($page->Link());
459
460
		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...
461
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
0 ignored issues
show
Bug introduced by
It seems like $parser is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
462
		} else {
463
			return $link;
464
		}
465
	}
466
467
	/**
468
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
469
	 *
470
	 * @param string $action Optional controller action (method).
471
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
472
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
473
	 *                       append GET parameters.
474
	 * @return string
475
	 */
476
	public function Link($action = null) {
477
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
478
	}
479
480
	/**
481
	 * Get the absolute URL for this page, including protocol and host.
482
	 *
483
	 * @param string $action See {@link Link()}
484
	 * @return string
485
	 */
486
	public function AbsoluteLink($action = null) {
487
		if($this->hasMethod('alternateAbsoluteLink')) {
488
			return $this->alternateAbsoluteLink($action);
0 ignored issues
show
Bug introduced by
The method alternateAbsoluteLink() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean AbsoluteLink()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
489
		} else {
490
			return Director::absoluteURL($this->Link($action));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($this->Link($action)); of type string|false adds false to the return on line 490 which is incompatible with the return type documented by SilverStripe\CMS\Model\SiteTree::AbsoluteLink of type string. It seems like you forgot to handle an error condition.
Loading history...
491
		}
492
	}
493
494
	/**
495
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
496
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
497
	 *
498
	 * @param string $action See {@link Link()}
499
	 * @return string
500
	 */
501
	public function PreviewLink($action = null) {
502
		if($this->hasMethod('alternatePreviewLink')) {
503
			Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
504
			return $this->alternatePreviewLink($action);
0 ignored issues
show
Bug introduced by
The method alternatePreviewLink() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean PreviewLink()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
505
		}
506
507
		$link = $this->AbsoluteLink($action);
508
		$this->extend('updatePreviewLink', $link, $action);
509
		return $link;
510
	}
511
512
	public function getMimeType() {
513
		return 'text/html';
514
	}
515
516
	/**
517
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
518
	 *
519
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
520
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
521
	 * and returned in its full form.
522
	 *
523
	 * @uses RootURLController::get_homepage_link()
524
	 *
525
	 * @param string $action See {@link Link()}
526
	 * @return string
527
	 */
528
	public function RelativeLink($action = null) {
529
		if($this->ParentID && self::config()->nested_urls) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
530
			$parent = $this->Parent();
531
			// If page is removed select parent from version history (for archive page view)
532
			if((!$parent || !$parent->exists()) && $this->getIsDeletedFromStage()) {
533
				$parent = Versioned::get_latest_version(__CLASS__, $this->ParentID);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
534
			}
535
			$base = $parent->RelativeLink($this->URLSegment);
536
		} 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...
537
			// Unset base for root-level homepages.
538
			// Note: Homepages with action parameters (or $action === true)
539
			// need to retain their URLSegment.
540
			$base = null;
541
		} else {
542
			$base = $this->URLSegment;
543
		}
544
545
		$this->extend('updateRelativeLink', $base, $action);
546
547
		// Legacy support: If $action === true, retain URLSegment for homepages,
548
		// but don't append any action
549
		if($action === true) $action = null;
550
551
		return Controller::join_links($base, '/', $action);
552
	}
553
554
	/**
555
	 * Get the absolute URL for this page on the Live site.
556
	 *
557
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
558
	 * @return string
559
	 */
560
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
561
		$oldReadingMode = Versioned::get_reading_mode();
562
		Versioned::set_stage(Versioned::LIVE);
563
		/** @var SiteTree $live */
564
		$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,{"\...e\".\"ID\"":"integer"}>, 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...
565
			'"SiteTree"."ID"' => $this->ID
566
		));
567
		if($live) {
568
			$link = $live->AbsoluteLink();
569
			if($includeStageEqualsLive) {
570
				$link = Controller::join_links($link, '?stage=Live');
571
			}
572
		} else {
573
			$link = null;
574
		}
575
576
		Versioned::set_reading_mode($oldReadingMode);
577
		return $link;
578
	}
579
580
	/**
581
	 * Generates a link to edit this page in the CMS.
582
	 *
583
	 * @return string
584
	 */
585
	public function CMSEditLink() {
586
		$link = Controller::join_links(
587
			CMSPageEditController::singleton()->Link('show'),
588
			$this->ID
589
		);
590
		return Director::absoluteURL($link);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($link); of type string|false adds false to the return on line 590 which is incompatible with the return type declared by the interface CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
591
	}
592
593
594
	/**
595
	 * Return a CSS identifier generated from this page's link.
596
	 *
597
	 * @return string The URL segment
598
	 */
599
	public function ElementName() {
600
		return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
601
	}
602
603
	/**
604
	 * Returns true if this is the currently active page being used to handle this request.
605
	 *
606
	 * @return bool
607
	 */
608
	public function isCurrent() {
609
		$currentPage = Director::get_current_page();
610
		if ($currentPage instanceof ContentController) {
611
			$currentPage = $currentPage->data();
612
		}
613
		if($currentPage instanceof SiteTree) {
614
			return $currentPage === $this || $currentPage->ID === $this->ID;
615
		}
616
		return false;
617
	}
618
619
	/**
620
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
621
	 * currently being viewed).
622
	 *
623
	 * @return bool
624
	 */
625
	public function isSection() {
626
		return $this->isCurrent() || (
627
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
628
		);
629
	}
630
631
	/**
632
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
633
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
634
	 * to external users.
635
	 *
636
	 * @return bool
637
	 */
638
	public function isOrphaned() {
639
		// Always false for root pages
640
		if(empty($this->ParentID)) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
641
			return false;
642
		}
643
644
		// Parent must exist and not be an orphan itself
645
		$parent = $this->Parent();
646
		return !$parent || !$parent->exists() || $parent->isOrphaned();
647
	}
648
649
	/**
650
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
651
	 *
652
	 * @return string
653
	 */
654
	public function LinkOrCurrent() {
655
		return $this->isCurrent() ? 'current' : 'link';
656
	}
657
658
	/**
659
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
660
	 *
661
	 * @return string
662
	 */
663
	public function LinkOrSection() {
664
		return $this->isSection() ? 'section' : 'link';
665
	}
666
667
	/**
668
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
669
	 * but in the current section.
670
	 *
671
	 * @return string
672
	 */
673
	public function LinkingMode() {
674
		if($this->isCurrent()) {
675
			return 'current';
676
		} elseif($this->isSection()) {
677
			return 'section';
678
		} else {
679
			return 'link';
680
		}
681
	}
682
683
	/**
684
	 * Check if this page is in the given current section.
685
	 *
686
	 * @param string $sectionName Name of the section to check
687
	 * @return bool True if we are in the given section
688
	 */
689
	public function InSection($sectionName) {
690
		$page = Director::get_current_page();
691
		while($page && $page->exists()) {
692
			if($sectionName == $page->URLSegment) {
693
				return true;
694
			}
695
			$page = $page->Parent();
696
		}
697
		return false;
698
	}
699
700
	/**
701
	 * Reset Sort on duped page
702
	 *
703
	 * @param SiteTree $original
704
	 * @param bool $doWrite
705
	 */
706
	public function onBeforeDuplicate($original, $doWrite) {
0 ignored issues
show
Unused Code introduced by
The parameter $original is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $doWrite is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
707
		$this->Sort = 0;
708
	}
709
710
	/**
711
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
712
	 *
713
	 * @return static The duplicated object
714
	 */
715
	public function duplicateWithChildren() {
716
		/** @var SiteTree $clone */
717
		$clone = $this->duplicate();
718
		$children = $this->AllChildren();
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
719
720
		if($children) {
721
			/** @var SiteTree $child */
722
			foreach($children as $child) {
723
				$childClone = $child->duplicateWithChildren();
724
				$childClone->ParentID = $clone->ID;
725
				$childClone->write();
726
			}
727
		}
728
729
		return $clone;
730
	}
731
732
	/**
733
	 * Duplicate this node and its children as a child of the node with the given ID
734
	 *
735
	 * @param int $id ID of the new node's new parent
736
	 */
737
	public function duplicateAsChild($id) {
738
		/** @var SiteTree $newSiteTree */
739
		$newSiteTree = $this->duplicate();
740
		$newSiteTree->ParentID = $id;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
741
		$newSiteTree->Sort = 0;
742
		$newSiteTree->write();
743
	}
744
745
	/**
746
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
747
	 *
748
	 * @param int $maxDepth The maximum depth to traverse.
749
	 * @param boolean $unlinked Whether to link page titles.
750
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
751
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
752
	 * @return string The breadcrumb trail.
753
	 */
754
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
755
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
756
		$template = new SSViewer('BreadcrumbsTemplate');
757
		return $template->process($this->customise(new ArrayData(array(
758
			"Pages" => $pages,
759
			"Unlinked" => $unlinked
760
		))));
761
	}
762
763
764
	/**
765
	 * Returns a list of breadcrumbs for the current page.
766
	 *
767
	 * @param int $maxDepth The maximum depth to traverse.
768
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
769
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
770
	 *
771
	 * @return ArrayList
772
	*/
773
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
774
		$page = $this;
775
		$pages = array();
776
777
		while(
778
			$page
779
			&& $page->exists()
780
 			&& (!$maxDepth || count($pages) < $maxDepth)
781
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
782
 		) {
783
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
784
				$pages[] = $page;
785
			}
786
787
			$page = $page->Parent();
788
		}
789
790
		return new ArrayList(array_reverse($pages));
791
	}
792
793
794
	/**
795
	 * Make this page a child of another page.
796
	 *
797
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
798
	 *
799
	 * @param SiteTree|int $item Either the parent object, or the parent ID
800
	 */
801
	public function setParent($item) {
802
		if(is_object($item)) {
803
			if (!$item->exists()) $item->write();
804
			$this->setField("ParentID", $item->ID);
805
		} else {
806
			$this->setField("ParentID", $item);
807
		}
808
	}
809
810
	/**
811
	 * Get the parent of this page.
812
	 *
813
	 * @return SiteTree Parent of this page
814
	 */
815
	public function getParent() {
816
		if ($parentID = $this->getField("ParentID")) {
817
			return DataObject::get_by_id("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
818
		}
819
		return null;
820
	}
821
822
	/**
823
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
824
	 *
825
	 * @param int $level The maximum amount of levels to traverse.
826
	 * @param string $separator Seperating string
827
	 * @return string The resulting string
828
	 */
829
	public function NestedTitle($level = 2, $separator = " - ") {
830
		$item = $this;
831
		$parts = [];
832
		while($item && $level > 0) {
833
			$parts[] = $item->Title;
834
			$item = $item->getParent();
835
			$level--;
836
		}
837
		return implode($separator, array_reverse($parts));
838
	}
839
840
	/**
841
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
842
	 * the security model for an application.
843
	 *
844
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
845
	 * - Checks for existence of a method named "can<$perm>()" on the object
846
	 * - Calls decorators and only returns for FALSE "vetoes"
847
	 * - Falls back to {@link Permission::check()}
848
	 * - Does NOT check for many-many relations named "Can<$perm>"
849
	 *
850
	 * @uses DataObjectDecorator->can()
851
	 *
852
	 * @param string $perm The permission to be checked, such as 'View'
853
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
854
	 * @param array $context Context argument for canCreate()
855
	 * @return bool True if the the member is allowed to do the given action
856
	 */
857
	public function can($perm, $member = null, $context = array()) {
858 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...
859
			$member = Member::currentUserID();
860
		}
861
862
		if($member && Permission::checkMember($member, "ADMIN")) return true;
863
864
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
865
			$method = 'can' . ucfirst($perm);
866
			return $this->$method($member);
867
		}
868
869
		$results = $this->extend('can', $member);
870
		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...
871
872
		return ($member && Permission::checkMember($member, $perm));
873
	}
874
875
	/**
876
	 * This function should return true if the current user can add children to this page. It can be overloaded to
877
	 * customise the security model for an application.
878
	 *
879
	 * Denies permission if any of the following conditions is true:
880
	 * - alternateCanAddChildren() on a extension returns false
881
	 * - canEdit() is not granted
882
	 * - There are no classes defined in {@link $allowed_children}
883
	 *
884
	 * @uses SiteTreeExtension->canAddChildren()
885
	 * @uses canEdit()
886
	 * @uses $allowed_children
887
	 *
888
	 * @param Member|int $member
889
	 * @return bool True if the current user can add children
890
	 */
891
	public function canAddChildren($member = null) {
892
		// Disable adding children to archived pages
893
		if($this->getIsDeletedFromStage()) {
894
			return false;
895
		}
896
897 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...
898
			$member = Member::currentUserID();
899
		}
900
901
		// Standard mechanism for accepting permission changes from extensions
902
		$extended = $this->extendedCan('canAddChildren', $member);
903
		if($extended !== null) {
904
			return $extended;
905
		}
906
907
		// Default permissions
908
		if($member && Permission::checkMember($member, "ADMIN")) {
909
			return true;
910
		}
911
912
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
913
	}
914
915
	/**
916
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
917
	 * security model for an application.
918
	 *
919
	 * Denies permission if any of the following conditions is true:
920
	 * - canView() on any extension returns false
921
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
922
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
923
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
924
	 *
925
	 * @uses DataExtension->canView()
926
	 * @uses ViewerGroups()
927
	 *
928
	 * @param Member|int $member
929
	 * @return bool True if the current user can view this page
930
	 */
931
	public function canView($member = null) {
932 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...
933
			$member = Member::currentUserID();
934
		}
935
936
		// Standard mechanism for accepting permission changes from extensions
937
		$extended = $this->extendedCan('canView', $member);
938
		if($extended !== null) {
939
			return $extended;
940
		}
941
942
		// admin override
943
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
944
			return true;
945
		}
946
947
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
948
		if($this->isOrphaned()) {
949
			return false;
950
		}
951
952
		// check for empty spec
953
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') {
954
			return true;
955
		}
956
957
		// check for inherit
958
		if($this->CanViewType == 'Inherit') {
959
			if($this->ParentID) return $this->Parent()->canView($member);
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
960
			else return $this->getSiteConfig()->canViewPages($member);
961
		}
962
963
		// check for any logged-in users
964
		if($this->CanViewType == 'LoggedInUsers' && $member) {
965
			return true;
966
		}
967
968
		// check for specific groups
969
		if($member && is_numeric($member)) {
970
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
971
		}
972
		if(
973
			$this->CanViewType == 'OnlyTheseUsers'
974
			&& $member
975
			&& $member->inGroups($this->ViewerGroups())
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
976
		) return true;
977
978
		return false;
979
	}
980
981
	/**
982
	 * Check if this page can be published
983
	 *
984
	 * @param Member $member
985
	 * @return bool
986
	 */
987
	public function canPublish($member = null) {
988
		if(!$member) {
989
			$member = Member::currentUser();
990
		}
991
992
		// Check extension
993
		$extended = $this->extendedCan('canPublish', $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
994
		if($extended !== null) {
995
			return $extended;
996
		}
997
998
		if(Permission::checkMember($member, "ADMIN")) {
999
			return true;
1000
		}
1001
1002
		// Default to relying on edit permission
1003
		return $this->canEdit($member);
1004
	}
1005
1006
	/**
1007
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
1008
	 * security model for an application.
1009
	 *
1010
	 * Denies permission if any of the following conditions is true:
1011
	 * - canDelete() returns false on any extension
1012
	 * - canEdit() returns false
1013
	 * - any descendant page returns false for canDelete()
1014
	 *
1015
	 * @uses canDelete()
1016
	 * @uses SiteTreeExtension->canDelete()
1017
	 * @uses canEdit()
1018
	 *
1019
	 * @param Member $member
1020
	 * @return bool True if the current user can delete this page
1021
	 */
1022
	public function canDelete($member = null) {
1023 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...
1024
		else if(is_numeric($member)) $memberID = $member;
1025
		else $memberID = Member::currentUserID();
1026
1027
		// Standard mechanism for accepting permission changes from extensions
1028
		$extended = $this->extendedCan('canDelete', $memberID);
1029
		if($extended !== null) {
1030
			return $extended;
1031
		}
1032
1033
		// Default permission check
1034
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1035
			return true;
1036
		}
1037
1038
		// Regular canEdit logic is handled by can_edit_multiple
1039
		$results = self::can_delete_multiple(array($this->ID), $memberID);
1040
1041
		// If this page no longer exists in stage/live results won't contain the page.
1042
		// Fail-over to false
1043
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
1044
	}
1045
1046
	/**
1047
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
1048
	 * can be overloaded to customise the security model for an application.
1049
	 *
1050
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1051
	 * create beneath a parent is based on the ability to edit that parent page.
1052
	 *
1053
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1054
	 *
1055
	 * @uses $can_create
1056
	 * @uses DataExtension->canCreate()
1057
	 *
1058
	 * @param Member $member
1059
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
1060
	 *                       If a parent page is known, it will be checked for validity.
1061
	 *                       If omitted, it will be assumed this is to be created as a top level page.
1062
	 * @return bool True if the current user can create pages on this class.
1063
	 */
1064
	public function canCreate($member = null, $context = array()) {
1065 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...
1066
			$member = Member::currentUserID();
1067
		}
1068
1069
		// Check parent (custom canCreate option for SiteTree)
1070
		// Block children not allowed for this parent type
1071
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
1072
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1073
			return false;
1074
		}
1075
1076
		// Standard mechanism for accepting permission changes from extensions
1077
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1078
		if($extended !== null) {
1079
			return $extended;
1080
		}
1081
1082
		// Check permission
1083
		if($member && Permission::checkMember($member, "ADMIN")) {
1084
			return true;
1085
		}
1086
1087
		// Fall over to inherited permissions
1088
		if($parent) {
1089
			return $parent->canAddChildren($member);
1090
		} else {
1091
			// This doesn't necessarily mean we are creating a root page, but that
1092
			// we don't know if there is a parent, so default to this permission
1093
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1094
		}
1095
	}
1096
1097
	/**
1098
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1099
	 * security model for an application.
1100
	 *
1101
	 * Denies permission if any of the following conditions is true:
1102
	 * - canEdit() on any extension returns false
1103
	 * - canView() return false
1104
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1105
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1106
	 *   CMS_Access_CMSMAIN permission code
1107
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1108
	 *
1109
	 * @uses canView()
1110
	 * @uses EditorGroups()
1111
	 * @uses DataExtension->canEdit()
1112
	 *
1113
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1114
	 *                       unit tests)
1115
	 * @return bool True if the current user can edit this page
1116
	 */
1117
	public function canEdit($member = null) {
1118 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...
1119
		else if(is_numeric($member)) $memberID = $member;
1120
		else $memberID = Member::currentUserID();
1121
1122
		// Standard mechanism for accepting permission changes from extensions
1123
		$extended = $this->extendedCan('canEdit', $memberID);
1124
		if($extended !== null) {
1125
			return $extended;
1126
		}
1127
1128
		// Default permissions
1129
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1130
			return true;
1131
		}
1132
1133
		if($this->ID) {
1134
			// Regular canEdit logic is handled by can_edit_multiple
1135
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1136
1137
			// If this page no longer exists in stage/live results won't contain the page.
1138
			// Fail-over to false
1139
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1140
1141
		// Default for unsaved pages
1142
		} else {
1143
			return $this->getSiteConfig()->canEditPages($member);
1144
		}
1145
	}
1146
1147
	/**
1148
	 * Stub method to get the site config, unless the current class can provide an alternate.
1149
	 *
1150
	 * @return SiteConfig
1151
	 */
1152
	public function getSiteConfig() {
1153
		$configs = $this->invokeWithExtensions('alternateSiteConfig');
1154
		foreach(array_filter($configs) as $config) {
1155
			return $config;
1156
		}
1157
1158
		return SiteConfig::current_site_config();
1159
	}
1160
1161
	/**
1162
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1163
	 * can_(perm)_multiple method for efficiency.
1164
	 *
1165
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1166
	 * @param array           $ids           An array of page IDs
1167
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1168
	 *                                       to 'SiteTree::can_(permission)_multiple'
1169
	 */
1170
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1171
		if(!$batchCallback) {
1172
			$batchCallback = __CLASS__ . "::can_{$permission}_multiple";
1173
		}
1174
1175
		if(is_callable($batchCallback)) {
1176
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1177
		} else {
1178
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1179
				. "with callback '$batchCallback'", E_USER_WARNING);
1180
		}
1181
	}
1182
1183
	/**
1184
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1185
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1186
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1187
	 * efficiently.
1188
	 *
1189
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1190
	 * property to FALSE.
1191
	 *
1192
	 * @param array  $ids              Of {@link SiteTree} IDs
1193
	 * @param int    $memberID         Member ID
1194
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1195
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1196
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1197
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1198
	 * @param bool   $useCached
1199
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1200
	 */
1201
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1202
												  $globalPermission = null, $useCached = true) {
1203
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1204
1205
		// Sanitise the IDs
1206
		$ids = array_filter($ids, 'is_numeric');
1207
1208
		// This is the name used on the permission cache
1209
		// converts something like 'CanEditType' to 'edit'.
1210
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1211
1212
		// Default result: nothing editable
1213
		$result = array_fill_keys($ids, false);
1214
		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...
1215
1216
			// Look in the cache for values
1217
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1218
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1219
1220
				// If we can't find everything in the cache, then look up the remainder separately
1221
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1222
				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...
1223
					$cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
0 ignored issues
show
Bug introduced by
It seems like $globalPermission defined by array('CMS_ACCESS_LeftAn..., 'CMS_ACCESS_CMSMain') on line 1203 can also be of type array<integer,string,{"0":"string","1":"string"}>; however, SilverStripe\CMS\Model\S...atch_permission_check() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1224
				}
1225
				return $cachedValues;
1226
			}
1227
1228
			// If a member doesn't have a certain permission then they can't edit anything
1229
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1230
				return $result;
1231
			}
1232
1233
			// Placeholder for parameterised ID list
1234
			$idPlaceholders = DB::placeholders($ids);
1235
1236
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1237
			// be enabled
1238
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1239
1240
			// Get the groups that the given member belongs to
1241
			/** @var Member $member */
1242
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1243
			$groupIDs = $member->Groups()->column("ID");
1244
			$SQL_groupList = implode(", ", $groupIDs);
1245
			if (!$SQL_groupList) {
1246
				$SQL_groupList = '0';
1247
			}
1248
1249
			$combinedStageResult = array();
1250
1251
			foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1252
				// Start by filling the array with the pages that actually exist
1253
				/** @skipUpgrade */
1254
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1255
1256
				if($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1257
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1258
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1259
				} else {
1260
					$stageIds = array();
1261
				}
1262
				$result = array_fill_keys($stageIds, false);
1263
1264
				// Get the uninherited permissions
1265
				$uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1266
					->where(array(
1267
						"(\"$typeField\" = 'LoggedInUsers' OR
1268
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1269
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1270
						=> $ids
1271
					))
1272
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1273
1274
				if($uninheritedPermissions) {
1275
					// Set all the relevant items in $result to true
1276
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1277
				}
1278
1279
				// Get permissions that are inherited
1280
				$potentiallyInherited = Versioned::get_by_stage(
1281
					"SilverStripe\\CMS\\Model\\SiteTree",
1282
					$stage,
1283
					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...
1284
				);
1285
1286
				if($potentiallyInherited) {
1287
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1288
					// then see which ones the user has permission on
1289
					$groupedByParent = array();
1290
					foreach($potentiallyInherited as $item) {
1291
						/** @var SiteTree $item */
1292
						if($item->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1293
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1294
							$groupedByParent[$item->ParentID][] = $item->ID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1295
						} else {
1296
							// Might return different site config based on record context, e.g. when subsites module
1297
							// is used
1298
							$siteConfig = $item->getSiteConfig();
1299
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1300
						}
1301
					}
1302
1303
					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...
1304
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1305
						if($actuallyInherited) {
1306
							$parentIDs = array_keys(array_filter($actuallyInherited));
1307
							foreach($parentIDs as $parentID) {
1308
								// Set all the relevant items in $result to true
1309
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1310
							}
1311
						}
1312
					}
1313
				}
1314
1315
				$combinedStageResult = $combinedStageResult + $result;
1316
1317
			}
1318
		}
1319
1320
		if(isset($combinedStageResult)) {
1321
			// Cache the results
1322
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1323
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1324
			return $combinedStageResult;
1325
		} else {
1326
			return array();
1327
		}
1328
	}
1329
1330
	/**
1331
	 * Get the 'can edit' information for a number of SiteTree pages.
1332
	 *
1333
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1334
	 * @param int   $memberID  ID of member
1335
	 * @param bool  $useCached Return values from the permission cache if they exist
1336
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1337
	 *                         edited
1338
	 */
1339
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1340
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1341
	}
1342
1343
	/**
1344
	 * Get the 'can edit' information for a number of SiteTree pages.
1345
	 *
1346
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1347
	 * @param int   $memberID  ID of member
1348
	 * @param bool  $useCached Return values from the permission cache if they exist
1349
	 * @return array
1350
	 */
1351
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1352
		$deletable = array();
1353
		$result = array_fill_keys($ids, false);
1354
		$cacheKey = "delete-$memberID";
1355
1356
		// Look in the cache for values
1357
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1358
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1359
1360
			// If we can't find everything in the cache, then look up the remainder separately
1361
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1362
			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...
1363
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1364
					+ $cachedValues;
1365
			}
1366
			return $cachedValues;
1367
		}
1368
1369
		// You can only delete pages that you can edit
1370
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1371
		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...
1372
1373
			// You can only delete pages whose children you can delete
1374
			$editablePlaceholders = DB::placeholders($editableIDs);
1375
			$childRecords = SiteTree::get()->where(array(
1376
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1377
			));
1378
			if($childRecords) {
1379
				$children = $childRecords->map("ID", "ParentID");
1380
1381
				// Find out the children that can be deleted
1382
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1383
1384
				// Get a list of all the parents that have no undeletable children
1385
				$deletableParents = array_fill_keys($editableIDs, true);
1386
				foreach($deletableChildren as $id => $canDelete) {
1387
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1388
				}
1389
1390
				// Use that to filter the list of deletable parents that have children
1391
				$deletableParents = array_keys($deletableParents);
1392
1393
				// Also get the $ids that don't have children
1394
				$parents = array_unique($children->values());
1395
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1396
1397
				// Combine the two
1398
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1399
1400
			} else {
1401
				$deletable = $editableIDs;
1402
			}
1403
		}
1404
1405
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1406
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1407
	}
1408
1409
	/**
1410
	 * Collate selected descendants of this page.
1411
	 *
1412
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1413
	 * $collator array.
1414
	 *
1415
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1416
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1417
	 * @return bool
1418
	 */
1419
	public function collateDescendants($condition, &$collator) {
1420
		$children = $this->Children();
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean duplicateWithChildren()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1421
		if($children) {
1422
			foreach($children as $item) {
1423
1424
				if(eval("return $condition;")) {
1425
					$collator[] = $item;
1426
				}
1427
				/** @var SiteTree $item */
1428
				$item->collateDescendants($condition, $collator);
1429
			}
1430
			return true;
1431
		}
1432
		return false;
1433
	}
1434
1435
	/**
1436
	 * Return the title, description, keywords and language metatags.
1437
	 *
1438
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1439
	 *
1440
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1441
	 * @return string The XHTML metatags
1442
	 */
1443
	public function MetaTags($includeTitle = true) {
1444
		$tags = array();
1445
		if($includeTitle && strtolower($includeTitle) != 'false') {
1446
			$tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1447
		}
1448
1449
		$generator = trim(Config::inst()->get(__CLASS__, 'meta_generator'));
1450
		if (!empty($generator)) {
1451
			$tags[] = FormField::create_tag('meta', array(
1452
				'name' => 'generator',
1453
				'content' => $generator,
1454
			));
1455
		}
1456
1457
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1458
		$tags[] = FormField::create_tag('meta', array(
1459
			'http-equiv' => 'Content-Type',
1460
			'content' => 'text/html; charset=' . $charset,
1461
		));
1462
		if($this->MetaDescription) {
1463
			$tags[] = FormField::create_tag('meta', array(
1464
				'name' => 'description',
1465
				'content' => $this->MetaDescription,
1466
			));
1467
		}
1468
1469
		if(Permission::check('CMS_ACCESS_CMSMain')
1470
			&& in_array('CMSPreviewable', class_implements($this))
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;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

Loading history...
Documentation Bug introduced by
The method migrateVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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()) {
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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)
0 ignored issues
show
Bug introduced by
The property CopyContentFromID does not seem to exist. Did you mean Content?

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

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

Loading history...
1658
				? $this->CopyContentFrom()
0 ignored issues
show
Documentation Bug introduced by
The method CopyContentFrom does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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;
1700
			}
1701
		}
1702
1703
		if(!self::config()->nested_urls || !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1704
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
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;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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,{"\...e\".\"ID\"":"integer"}>, 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,{"\...e\".\"ID\"":"integer"}>, 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()) {
0 ignored issues
show
Documentation Bug introduced by
The method BackLinkTracking does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenLink does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property HasBrokenFile does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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
1929
				->getConfig()
1930
				->getComponentByType('GridFieldDataColumns');
1931
			$dataColumns
1932
				->setDisplayFields($dependentColumns)
1933
				->setFieldFormatting(array(
1934
					'Title' => function($value, &$item) {
1935
						return sprintf(
1936
							'<a href="admin/pages/edit/show/%d">%s</a>',
1937
							(int)$item->ID,
1938
							Convert::raw2xml($item->Title)
1939
						);
1940
					},
1941
					'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...
1942
						return sprintf(
1943
							'<a href="%s" target="_blank">%s</a>',
1944
							Convert::raw2xml($value),
1945
							Convert::raw2xml($value)
1946
						);
1947
					}
1948
				));
1949
		}
1950
1951
		$baseLink = Controller::join_links (
1952
			Director::absoluteBaseURL(),
1953
			(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
true is of type boolean, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1954
		);
1955
1956
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1957
			->setURLPrefix($baseLink)
1958
			->setDefaultURL($this->generateURLSegment(_t(
1959
				'CMSMain.NEWPAGE',
1960
				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...
1961
			)));
1962
		$helpText = (self::config()->nested_urls && $this->Children()->count())
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean duplicateWithChildren()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1963
			? $this->fieldLabel('LinkChangeNote')
1964
			: '';
1965
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1966
			$helpText .= $helpText ? '<br />' : '';
1967
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1968
		}
1969
		$urlsegment->setHelpText($helpText);
1970
1971
		$fields = new FieldList(
1972
			$rootTab = new TabSet("Root",
1973
				$tabMain = new Tab('Main',
1974
					new TextField("Title", $this->fieldLabel('Title')),
1975
					$urlsegment,
1976
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1977
					$htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1978
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1979
						array(
1980
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1981
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1982
						)
1983
					)->setHeadingLevel(4)
1984
				),
1985
				$tabDependent = new Tab('Dependent',
1986
					$dependentNote,
1987
					$dependentTable
1988
				)
1989
			)
1990
		);
1991
		$htmlField->addExtraClass('stacked');
1992
1993
		// Help text for MetaData on page content editor
1994
		$metaFieldDesc
1995
			->setRightTitle(
1996
				_t(
1997
					'SiteTree.METADESCHELP',
1998
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1999
				)
2000
			)
2001
			->addExtraClass('help');
2002
		$metaFieldExtra
2003
			->setRightTitle(
2004
				_t(
2005
					'SiteTree.METAEXTRAHELP',
2006
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2007
				)
2008
			)
2009
			->addExtraClass('help');
2010
2011
		// Conditional dependent pages tab
2012
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2013
		else $fields->removeFieldFromTab('Root', 'Dependent');
2014
2015
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2016
2017
		if($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

Loading history...
2018
			$obsoleteWarning = _t(
2019
				'SiteTree.OBSOLETECLASS',
2020
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2021
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2022
			);
2023
2024
			$fields->addFieldToTab(
2025
				"Root.Main",
2026
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2027
				"Title"
2028
			);
2029
		}
2030
2031
		if(file_exists(BASE_PATH . '/install.php')) {
2032
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
2033
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
2034
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
2035
				. "</p>"), "Title");
2036
		}
2037
2038
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
2039
		$fields->setTabPathRewrites(array(
2040
			'/^Root\.Content\.Main$/' => 'Root.Main',
2041
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
2042
		));
2043
2044
		if(self::$runCMSFieldsExtensions) {
2045
			$this->extend('updateCMSFields', $fields);
2046
		}
2047
2048
		return $fields;
2049
	}
2050
2051
2052
	/**
2053
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2054
	 * for content-related fields.
2055
	 *
2056
	 * @return FieldList
2057
	 */
2058
	public function getSettingsFields() {
2059
		$groupsMap = array();
2060
		foreach(Group::get() as $group) {
2061
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
2062
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2063
		}
2064
		asort($groupsMap);
2065
2066
		$fields = new FieldList(
2067
			$rootTab = new TabSet("Root",
2068
				$tabBehaviour = new Tab('Settings',
2069
					new DropdownField(
2070
						"ClassName",
2071
						$this->fieldLabel('ClassName'),
2072
						$this->getClassDropdown()
2073
					),
2074
					$parentTypeSelector = new CompositeField(
2075
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2076
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2077
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2078
						)),
2079
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SilverStripe\\CMS\\Model\\SiteTree', 'ID', 'MenuTitle')
2080
					),
2081
					$visibility = new FieldGroup(
2082
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2083
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2084
					),
2085
					$viewersOptionsField = new OptionsetField(
2086
						"CanViewType",
2087
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
2088
					),
2089
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2090
						->setSource($groupsMap)
2091
						->setAttribute(
2092
							'data-placeholder',
2093
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2094
						),
2095
					$editorsOptionsField = new OptionsetField(
2096
						"CanEditType",
2097
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2098
					),
2099
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2100
						->setSource($groupsMap)
2101
						->setAttribute(
2102
							'data-placeholder',
2103
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2104
						)
2105
				)
2106
			)
2107
		);
2108
2109
		$visibility->setTitle($this->fieldLabel('Visibility'));
2110
2111
2112
		// This filter ensures that the ParentID dropdown selection does not show this node,
2113
		// or its descendents, as this causes vanishing bugs
2114
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2115
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2116
2117
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2118
2119
		// Make page location fields read-only if the user doesn't have the appropriate permission
2120
		if(!Permission::check("SITETREE_REORGANISE")) {
2121
			$fields->makeFieldReadonly('ParentType');
2122
			if($this->getParentType() === 'root') {
2123
				$fields->removeByName('ParentID');
2124
			} else {
2125
				$fields->makeFieldReadonly('ParentID');
2126
			}
2127
		}
2128
2129
		$viewersOptionsSource = array();
2130
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2131
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2132
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2133
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2134
		$viewersOptionsField->setSource($viewersOptionsSource);
2135
2136
		$editorsOptionsSource = array();
2137
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2138
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2139
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2140
		$editorsOptionsField->setSource($editorsOptionsSource);
2141
2142
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2143
			$fields->makeFieldReadonly($viewersOptionsField);
2144
			if($this->CanViewType == 'OnlyTheseUsers') {
2145
				$fields->makeFieldReadonly($viewerGroupsField);
2146
			} else {
2147
				$fields->removeByName('ViewerGroups');
2148
			}
2149
2150
			$fields->makeFieldReadonly($editorsOptionsField);
2151
			if($this->CanEditType == 'OnlyTheseUsers') {
2152
				$fields->makeFieldReadonly($editorGroupsField);
2153
			} else {
2154
				$fields->removeByName('EditorGroups');
2155
			}
2156
		}
2157
2158
		if(self::$runCMSFieldsExtensions) {
2159
			$this->extend('updateSettingsFields', $fields);
2160
		}
2161
2162
		return $fields;
2163
	}
2164
2165
	/**
2166
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2167
	 * @return array
2168
	 */
2169
	public function fieldLabels($includerelations = true) {
2170
		$cacheKey = $this->class . '_' . $includerelations;
2171
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2172
			$labels = parent::fieldLabels($includerelations);
2173
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2174
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2175
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2176
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2177
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2178
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2179
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2180
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2181
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2182
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2183
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2184
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2185
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2186
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2187
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2188
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2189
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2190
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2191
			$labels['LinkChangeNote'] = _t (
2192
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2193
			);
2194
2195
			if($includerelations){
2196
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2197
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2198
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2199
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2200
			}
2201
2202
			self::$_cache_field_labels[$cacheKey] = $labels;
2203
		}
2204
2205
		return self::$_cache_field_labels[$cacheKey];
2206
	}
2207
2208
	/**
2209
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2210
	 *
2211
	 * Frontend scripts and styles know how to handle the following FormFields:
2212
	 * - top-level FormActions appear as standalone buttons
2213
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2214
	 * - TabSet & Tabs appear as a drop ups
2215
	 * - FormActions within the Tab are restyled as links
2216
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2217
	 *
2218
	 * @return FieldList The available actions for this page.
2219
	 */
2220
	public function getCMSActions() {
2221
		$existsOnLive = $this->isPublished();
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2222
2223
		// Major actions appear as buttons immediately visible as page actions.
2224
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('btn-group ss-ui-buttonset noborder');
2225
2226
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2227
		$rootTabSet = new TabSet('ActionMenus');
2228
		$moreOptions = new Tab(
2229
			'MoreOptions',
2230
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2231
		);
2232
		$rootTabSet->push($moreOptions);
2233
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2234
2235
		// Render page information into the "more-options" drop-up, on the top.
2236
		$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,{"\...e\".\"ID\"":"integer"}>, 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...
2237
			'"SiteTree"."ID"' => $this->ID
2238
		));
2239
		$moreOptions->push(
2240
			new LiteralField('Information',
2241
				$this->customise(array(
2242
					'Live' => $live,
2243
					'ExistsOnLive' => $existsOnLive
2244
				))->renderWith('SiteTree_Information')
2245
			)
2246
		);
2247
2248
		$moreOptions->push(AddToCampaignHandler_FormAction::create());
2249
2250
		// "readonly"/viewing version that isn't the current version of the record
2251
		$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,{"\...e\".\"ID\"":"integer"}>, 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...
2252
			'"SiteTree"."ID"' => $this->ID
2253
		));
2254
		if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

Loading history...
2255
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2256
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2257
2258
			$actions = new FieldList(array($majorActions, $rootTabSet));
2259
2260
			// getCMSActions() can be extended with updateCMSActions() on a extension
2261
			$this->extend('updateCMSActions', $actions);
2262
2263
			return $actions;
2264
		}
2265
2266
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2267
			// "unpublish"
2268
			$moreOptions->push(
2269
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2270
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2271
					->addExtraClass('ss-ui-action-destructive')
2272
			);
2273
		}
2274
2275
		if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE) && !$this->getIsDeletedFromStage()) {
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2276
			if($this->isPublished() && $this->canEdit())	{
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2277
				// "rollback"
2278
				$moreOptions->push(
2279
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2280
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2281
				);
2282
			}
2283
		}
2284
2285
		if($this->canEdit()) {
2286
			if($this->getIsDeletedFromStage()) {
2287
				// The usual major actions are not available, so we provide alternatives here.
2288
				if($existsOnLive) {
2289
					// "restore"
2290
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2291
					if($this->canDelete() && $this->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2292
						// "delete from live"
2293
						$majorActions->push(
2294
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2295
								->addExtraClass('ss-ui-action-destructive')
2296
						);
2297
					}
2298
				} else {
2299
					// Determine if we should force a restore to root (where once it was a subpage)
2300
					$restoreToRoot = $this->isParentArchived();
2301
2302
					// "restore"
2303
					$title = $restoreToRoot
2304
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2305
						: _t('CMSMain.RESTORE','Restore draft');
2306
					$description = $restoreToRoot
2307
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2308
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2309
					$majorActions->push(
2310
						FormAction::create('restore', $title)
2311
							->setDescription($description)
2312
							->setAttribute('data-to-root', $restoreToRoot)
0 ignored issues
show
Documentation introduced by
$restoreToRoot is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2313
							->setAttribute('data-icon', 'decline')
2314
					);
2315
				}
2316
			} else {
2317
					if($this->canDelete()) {
2318
						// delete
2319
						$moreOptions->push(
2320
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2321
								->addExtraClass('delete ss-ui-action-destructive')
2322
						);
2323
					}
2324
				if($this->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2325
					// "archive"
2326
					$moreOptions->push(
2327
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2328
							->setDescription(_t(
2329
								'SiteTree.BUTTONARCHIVEDESC',
2330
								'Unpublish and send to archive'
2331
							))
2332
							->addExtraClass('delete ss-ui-action-destructive')
2333
					);
2334
				}
2335
2336
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2337
				$majorActions->push(
2338
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2339
						->setAttribute('data-icon', 'accept')
2340
						->setAttribute('data-icon-alternate', 'addpage')
2341
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2342
				);
2343
			}
2344
		}
2345
2346
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
2347
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2348
			$majorActions->push(
2349
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2350
					->setAttribute('data-icon', 'accept')
2351
					->setAttribute('data-icon-alternate', 'disk')
2352
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2353
			);
2354
2355
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2356
			if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE)) {
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2357
				$publish->addExtraClass('ss-ui-alternate');
2358
			}
2359
		}
2360
2361
		$actions = new FieldList(array($majorActions, $rootTabSet));
2362
2363
		// Hook for extensions to add/remove actions.
2364
		$this->extend('updateCMSActions', $actions);
2365
2366
		return $actions;
2367
	}
2368
2369
	public function onAfterPublish() {
2370
		// Force live sort order to match stage sort order
2371
		DB::prepared_query('UPDATE "SiteTree_Live"
2372
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2373
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2374
			array($this->ParentID)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2375
		);
2376
		}
2377
2378
	/**
2379
	 * Update draft dependant pages
2380
	 */
2381
	public function onAfterRevertToLive() {
2382
		// Use an alias to get the updates made by $this->publish
2383
		/** @var SiteTree $stageSelf */
2384
		$stageSelf = Versioned::get_by_stage('SilverStripe\\CMS\\Model\\SiteTree', Versioned::DRAFT)->byID($this->ID);
2385
		$stageSelf->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2386
2387
		// Need to update pages linking to this one as no longer broken
2388
		foreach($stageSelf->DependentPages() as $page) {
2389
			/** @var SiteTree $page */
2390
			$page->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2391
		}
2392
	}
2393
2394
	/**
2395
	 * Determine if this page references a parent which is archived, and not available in stage
2396
	 *
2397
	 * @return bool True if there is an archived parent
2398
	 */
2399
	protected function isParentArchived() {
2400
		if($parentID = $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2401
			$parentPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
2402
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2403
				return true;
2404
			}
2405
		}
2406
		return false;
2407
	}
2408
2409
	/**
2410
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2411
	 *
2412
	 * @return self
2413
	 */
2414
	public function doRestoreToStage() {
2415
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2416
2417
		// Ensure that the parent page is restored, otherwise restore to root
2418
		if($this->isParentArchived()) {
2419
			$this->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2420
		}
2421
2422
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2423
		// create an empty record
2424
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2425
			$conn = DB::get_conn();
2426
			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...
2427
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2428
			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...
2429
		}
2430
2431
		$oldReadingMode = Versioned::get_reading_mode();
2432
		Versioned::set_stage(Versioned::DRAFT);
2433
		$this->forceChange();
2434
		$this->write();
2435
2436
		/** @var SiteTree $result */
2437
		$result = DataObject::get_by_id(__CLASS__, $this->ID);
2438
2439
		// Need to update pages linking to this one as no longer broken
2440
		foreach($result->DependentPages(false) as $page) {
2441
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2442
			$page->write();
2443
		}
2444
2445
		Versioned::set_reading_mode($oldReadingMode);
2446
2447
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2448
2449
		return $result;
2450
	}
2451
2452
	/**
2453
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2454
	 *
2455
	 * @return bool
2456
	 */
2457
	public function isNew() {
2458
		/**
2459
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2460
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2461
		 */
2462
		if(empty($this->ID)) return true;
2463
2464
		if(is_numeric($this->ID)) return false;
2465
2466
		return stripos($this->ID, 'new') === 0;
2467
	}
2468
2469
	/**
2470
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2471
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2472
	 * {@link SiteTree::$needs_permission}.
2473
	 *
2474
	 * @return array
2475
	 */
2476
	protected function getClassDropdown() {
2477
		$classes = self::page_type_classes();
2478
		$currentClass = null;
2479
2480
		$result = array();
2481
		foreach($classes as $class) {
2482
			$instance = singleton($class);
2483
2484
			// if the current page type is this the same as the class type always show the page type in the list
2485
			if ($this->ClassName != $instance->ClassName) {
2486
				if($instance instanceof HiddenClass) continue;
2487
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2488
			}
2489
2490
			if($perms = $instance->stat('need_permission')) {
2491
				if(!$this->can($perms)) continue;
2492
			}
2493
2494
			$pageTypeName = $instance->i18n_singular_name();
2495
2496
			$currentClass = $class;
2497
			$result[$class] = $pageTypeName;
2498
2499
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2500
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2501
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2502
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2503
				$result[$class] = $result[$class] .  " ({$class})";
2504
			}
2505
		}
2506
2507
		// sort alphabetically, and put current on top
2508
		asort($result);
2509
		if($currentClass) {
2510
			$currentPageTypeName = $result[$currentClass];
2511
			unset($result[$currentClass]);
2512
			$result = array_reverse($result);
2513
			$result[$currentClass] = $currentPageTypeName;
2514
			$result = array_reverse($result);
2515
		}
2516
2517
		return $result;
2518
	}
2519
2520
	/**
2521
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2522
	 *
2523
	 * @return string[]
2524
	 */
2525
	public function allowedChildren() {
2526
		$allowedChildren = array();
2527
		$candidates = $this->stat('allowed_children');
2528
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2529
			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...
2530
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2531
				// Otherwise, the class and all its subclasses are allowed.
2532
				if(substr($candidate,0,1) == '*') {
2533
					$allowedChildren[] = substr($candidate,1);
2534
				} else {
2535
					$subclasses = ClassInfo::subclassesFor($candidate);
2536
					foreach($subclasses as $subclass) {
2537
						if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2538
							continue;
2539
						}
2540
						$allowedChildren[] = $subclass;
2541
					}
2542
				}
2543
			}
2544
		}
2545
2546
		return $allowedChildren;
2547
	}
2548
2549
	/**
2550
	 * Returns the class name of the default class for children of this page.
2551
	 *
2552
	 * @return string
2553
	 */
2554
	public function defaultChild() {
2555
		$default = $this->stat('default_child');
2556
		$allowed = $this->allowedChildren();
2557
		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...
2558
			if(!$default || !in_array($default, $allowed)) {
2559
				$default = reset($allowed);
2560
			}
2561
			return $default;
2562
		}
2563
		return null;
2564
	}
2565
2566
	/**
2567
	 * Returns the class name of the default class for the parent of this page.
2568
	 *
2569
	 * @return string
2570
	 */
2571
	public function defaultParent() {
2572
		return $this->stat('default_parent');
2573
	}
2574
2575
	/**
2576
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2577
	 * Title field.
2578
	 *
2579
	 * @return string
2580
	 */
2581
	public function getMenuTitle(){
2582
		if($value = $this->getField("MenuTitle")) {
2583
			return $value;
2584
		} else {
2585
			return $this->getField("Title");
2586
		}
2587
	}
2588
2589
2590
	/**
2591
	 * Set the menu title for this page.
2592
	 *
2593
	 * @param string $value
2594
	 */
2595
	public function setMenuTitle($value) {
2596
		if($value == $this->getField("Title")) {
2597
			$this->setField("MenuTitle", null);
2598
		} else {
2599
			$this->setField("MenuTitle", $value);
2600
		}
2601
	}
2602
2603
	/**
2604
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2605
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2606
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2607
	 * the flags.
2608
	 *
2609
	 * Example (simple):
2610
	 *   "deletedonlive" => "Deleted"
2611
	 *
2612
	 * Example (with optional title attribute):
2613
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2614
	 *
2615
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2616
	 * @return array
2617
	 */
2618
	public function getStatusFlags($cached = true) {
2619
		if(!$this->_cache_statusFlags || !$cached) {
2620
			$flags = array();
2621
			if($this->getIsDeletedFromStage()) {
2622
				if($this->isPublished()) {
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2623
					$flags['removedfromdraft'] = array(
2624
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2625
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2626
					);
2627
				} else {
2628
					$flags['archived'] = array(
2629
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2630
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2631
					);
2632
				}
2633
			} else if($this->getIsAddedToStage()) {
2634
				$flags['addedtodraft'] = array(
2635
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2636
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2637
				);
2638
			} else if($this->getIsModifiedOnStage()) {
2639
				$flags['modified'] = array(
2640
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2641
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2642
				);
2643
			}
2644
2645
			$this->extend('updateStatusFlags', $flags);
2646
2647
			$this->_cache_statusFlags = $flags;
2648
		}
2649
2650
		return $this->_cache_statusFlags;
2651
	}
2652
2653
	/**
2654
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2655
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2656
	 * publication status.
2657
	 *
2658
	 * @return string An HTML string ready to be directly used in a template
2659
	 */
2660
	public function getTreeTitle() {
2661
		// Build the list of candidate children
2662
		$children = array();
2663
		$candidates = static::page_type_classes();
2664
		foreach($this->allowedChildren() as $childClass) {
2665
			if(!in_array($childClass, $candidates)) continue;
2666
			$child = singleton($childClass);
2667
			if($child->canCreate(null, array('Parent' => $this))) {
2668
				$children[$childClass] = $child->i18n_singular_name();
2669
			}
2670
		}
2671
		$flags = $this->getStatusFlags();
2672
		$treeTitle = sprintf(
2673
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2674
			Convert::raw2att(Convert::raw2json($children)),
2675
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2676
		);
2677
		foreach($flags as $class => $data) {
2678
			if(is_string($data)) $data = array('text' => $data);
2679
			$treeTitle .= sprintf(
2680
				"<span class=\"badge %s\"%s>%s</span>",
2681
				'status-' . Convert::raw2xml($class),
2682
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2683
				Convert::raw2xml($data['text'])
2684
			);
2685
		}
2686
2687
		return $treeTitle;
2688
	}
2689
2690
	/**
2691
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2692
	 * we're currently inside, etc.
2693
	 *
2694
	 * @param int $level
2695
	 * @return SiteTree
2696
	 */
2697
	public function Level($level) {
2698
		$parent = $this;
2699
		$stack = array($parent);
2700
		while(($parent = $parent->Parent()) && $parent->exists()) {
2701
			array_unshift($stack, $parent);
2702
		}
2703
2704
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2705
	}
2706
2707
	/**
2708
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2709
	 *
2710
	 * @return int
2711
	 */
2712
	public function getPageLevel() {
2713
		if($this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2714
			return 1 + $this->Parent()->getPageLevel();
2715
		}
2716
		return 1;
2717
	}
2718
2719
	/**
2720
	 * Return the CSS classes to apply to this node in the CMS tree.
2721
	 *
2722
	 * @param string $numChildrenMethod
2723
	 * @return string
2724
	 */
2725
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2726
		$classes = sprintf('class-%s', $this->class);
2727
		if($this->HasBrokenFile || $this->HasBrokenLink) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenFile does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property HasBrokenLink does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2728
			$classes .= " BrokenLink";
2729
		}
2730
2731
		if(!$this->canAddChildren()) {
2732
			$classes .= " nochildren";
2733
		}
2734
2735
		if(!$this->canEdit() && !$this->canAddChildren()) {
2736
			if (!$this->canView()) {
2737
				$classes .= " disabled";
2738
			} else {
2739
				$classes .= " edit-disabled";
2740
			}
2741
		}
2742
2743
		if(!$this->ShowInMenus) {
2744
			$classes .= " notinmenu";
2745
		}
2746
2747
		//TODO: Add integration
2748
		/*
2749
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2750
			$classes .= " untranslated ";
2751
		*/
2752
		$classes .= $this->markingClasses($numChildrenMethod);
0 ignored issues
show
Documentation Bug introduced by
The method markingClasses does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2753
2754
		return $classes;
2755
	}
2756
2757
	/**
2758
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2759
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2760
	 *
2761
	 * @return bool
2762
	 */
2763
	public function getIsDeletedFromStage() {
2764
		if(!$this->ID) return true;
2765
		if($this->isNew()) return false;
2766
2767
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', Versioned::DRAFT, $this->ID);
2768
2769
		// Return true for both completely deleted pages and for pages just deleted from stage
2770
		return !($stageVersion);
2771
	}
2772
2773
	/**
2774
	 * Return true if this page exists on the live site
2775
	 *
2776
	 * @return bool
2777
	 */
2778
	public function getExistsOnLive() {
2779
		return $this->isPublished();
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2780
	}
2781
2782
	/**
2783
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2784
	 * unpublished changes to the draft site.
2785
	 *
2786
	 * @return bool
2787
	 */
2788
	public function getIsModifiedOnStage() {
2789
		// New unsaved pages could be never be published
2790
		if($this->isNew()) return false;
2791
2792
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Stage', $this->ID);
2793
		$liveVersion =	Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->ID);
2794
2795
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2796
		$this->extend('getIsModifiedOnStage', $isModified);
2797
2798
		return $isModified;
2799
	}
2800
2801
	/**
2802
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2803
	 * published.
2804
	 *
2805
	 * @return bool
2806
	 */
2807
	public function getIsAddedToStage() {
2808
		// New unsaved pages could be never be published
2809
		if($this->isNew()) return false;
2810
2811
		$stageVersion = Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Stage', $this->ID);
2812
		$liveVersion =	Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->ID);
2813
2814
		return ($stageVersion && !$liveVersion);
2815
	}
2816
2817
	/**
2818
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2819
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2820
	 */
2821
	static public function disableCMSFieldsExtensions() {
2822
		self::$runCMSFieldsExtensions = false;
2823
	}
2824
2825
	/**
2826
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2827
	 * disableCMSFieldsExtensions().
2828
	 */
2829
	static public function enableCMSFieldsExtensions() {
2830
		self::$runCMSFieldsExtensions = true;
2831
	}
2832
2833
	public function providePermissions() {
2834
		return array(
2835
			'SITETREE_GRANT_ACCESS' => array(
2836
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2837
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2838
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2839
				'sort' => 100
2840
			),
2841
			'SITETREE_VIEW_ALL' => array(
2842
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2843
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2844
				'sort' => -100,
2845
				'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')
2846
			),
2847
			'SITETREE_EDIT_ALL' => array(
2848
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2849
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2850
				'sort' => -50,
2851
				'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')
2852
			),
2853
			'SITETREE_REORGANISE' => array(
2854
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2855
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2856
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2857
				'sort' => 100
2858
			),
2859
			'VIEW_DRAFT_CONTENT' => array(
2860
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2861
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2862
				'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.'),
2863
				'sort' => 100
2864
			)
2865
		);
2866
	}
2867
2868
	/**
2869
	 * Return the translated Singular name.
2870
	 *
2871
	 * @return string
2872
	 */
2873
	public function i18n_singular_name() {
2874
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2875
		/** @skipUpgrade */
2876
		// @todo When we namespace translations, change 'SiteTree' to FQN of the class
2877
		$class = ($this->class == 'Page' || $this->class === __CLASS__)
2878
			? 'SiteTree'
2879
			: $this->class;
2880
		return _t($class.'.SINGULARNAME', $this->singular_name());
2881
	}
2882
2883
	/**
2884
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2885
	 * picks it up for the wrong folder.
2886
	 *
2887
	 * @return array
2888
	 */
2889
	public function provideI18nEntities() {
2890
		$entities = parent::provideI18nEntities();
2891
2892
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2893
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2894
2895
		$entities[$this->class . '.DESCRIPTION'] = array(
2896
			$this->stat('description'),
2897
			'Description of the page type (shown in the "add page" dialog)'
2898
		);
2899
2900
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2901
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2902
2903
		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...
2904
	}
2905
2906
	/**
2907
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2908
	 *
2909
	 * @return string
2910
	 */
2911
	public function getParentType() {
2912
		return $this->ParentID == 0 ? 'root' : 'subpage';
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
2913
	}
2914
2915
	/**
2916
	 * Clear the permissions cache for SiteTree
2917
	 */
2918
	public static function reset() {
2919
		self::$cache_permissions = array();
2920
	}
2921
2922
	static public function on_db_reset() {
2923
		self::$cache_permissions = array();
2924
	}
2925
2926
}
2927