Completed
Pull Request — master (#1458)
by Sam
04:45 queued 02:13
created

SiteTree::can()   B

Complexity

Conditions 12
Paths 14

Size

Total Lines 17
Code Lines 10

Duplication

Lines 3
Ratio 17.65 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 3
loc 17
rs 7.4259
cc 12
eloc 10
nc 14
nop 3

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
293
		}
294
295
		// Check if we have any more URL parts to parse.
296
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
297
298
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
299
		foreach($parts as $segment) {
300
			$next = DataObject::get_one('SiteTree', array(
301
					'"SiteTree"."URLSegment"' => $segment,
302
					'"SiteTree"."ParentID"' => $sitetree->ID
303
				),
304
				$cache
305
			);
306
307
			if(!$next) {
308
				$parentID = (int) $sitetree->ID;
309
310 View Code Duplication
				if($alternatives = singleton('SiteTree')->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...
311
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
312
				}
313
314
				if(!$next) return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SiteTree::get_by_link of type SiteTree.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
315
			}
316
317
			$sitetree->destroy();
318
			$sitetree = $next;
319
		}
320
321
		return $sitetree;
322
	}
323
324
	/**
325
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
326
	 *
327
	 * @return array
328
	 */
329
	public static function page_type_classes() {
330
		$classes = ClassInfo::getValidSubClasses();
331
332
		$baseClassIndex = array_search('SiteTree', $classes);
333
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
334
335
		$kill_ancestors = array();
336
337
		// figure out if there are any classes we don't want to appear
338
		foreach($classes as $class) {
339
			$instance = singleton($class);
340
341
			// do any of the progeny want to hide an ancestor?
342
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
343
				// note for killing later
344
				$kill_ancestors[] = $ancestor_to_hide;
345
			}
346
		}
347
348
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
349
		// requirements
350
		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...
351
			$kill_ancestors = array_unique($kill_ancestors);
352
			foreach($kill_ancestors as $mark) {
353
				// unset from $classes
354
				$idx = array_search($mark, $classes, true);
355
				if ($idx !== false) {
356
					unset($classes[$idx]);
357
				}
358
			}
359
		}
360
361
		return $classes;
362
	}
363
364
	/**
365
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
366
	 *
367
	 * @param array      $arguments
368
	 * @param string     $content
369
	 * @param TextParser $parser
370
	 * @return string
371
	 */
372
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
373
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
374
375
		if (
376
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
377
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
378
		) {
379
			 return null; // There were no suitable matches at all.
380
		}
381
382
		$link = Convert::raw2att($page->Link());
383
384
		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...
385
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
386
		} else {
387
			return $link;
388
		}
389
	}
390
391
	/**
392
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
393
	 *
394
	 * @param string $action Optional controller action (method).
395
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
396
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
397
	 *                       append GET parameters.
398
	 * @return string
399
	 */
400
	public function Link($action = null) {
401
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
402
	}
403
404
	/**
405
	 * Get the absolute URL for this page, including protocol and host.
406
	 *
407
	 * @param string $action See {@link Link()}
408
	 * @return string
409
	 */
410
	public function AbsoluteLink($action = null) {
411
		if($this->hasMethod('alternateAbsoluteLink')) {
412
			return $this->alternateAbsoluteLink($action);
413
		} else {
414
			return Director::absoluteURL($this->Link($action));
415
		}
416
	}
417
418
	/**
419
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
420
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
421
	 *
422
	 * @param string $action See {@link Link()}
423
	 * @return string
424
	 */
425
	public function PreviewLink($action = null) {
426
		if($this->hasMethod('alternatePreviewLink')) {
427
			return $this->alternatePreviewLink($action);
428
		} else {
429
			return $this->AbsoluteLink($action);
430
		}
431
	}
432
433
	/**
434
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
435
	 *
436
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
437
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
438
	 * and returned in its full form.
439
	 *
440
	 * @uses RootURLController::get_homepage_link()
441
	 *
442
	 * @param string $action See {@link Link()}
443
	 * @return string
444
	 */
445
	public function RelativeLink($action = null) {
446
		if($this->ParentID && self::config()->nested_urls) {
447
			$parent = $this->Parent();
448
			// If page is removed select parent from version history (for archive page view)
449
			if((!$parent || !$parent->exists()) && $this->IsDeletedFromStage) {
450
				$parent = Versioned::get_latest_version('SiteTree', $this->ParentID);
451
			}
452
			$base = $parent->RelativeLink($this->URLSegment);
453
		} 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...
454
			// Unset base for root-level homepages.
455
			// Note: Homepages with action parameters (or $action === true)
456
			// need to retain their URLSegment.
457
			$base = null;
458
		} else {
459
			$base = $this->URLSegment;
460
		}
461
462
		$this->extend('updateRelativeLink', $base, $action);
463
464
		// Legacy support: If $action === true, retain URLSegment for homepages,
465
		// but don't append any action
466
		if($action === true) $action = null;
467
468
		return Controller::join_links($base, '/', $action);
469
	}
470
471
	/**
472
	 * Get the absolute URL for this page on the Live site.
473
	 *
474
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
475
	 * @return string
476
	 */
477
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
478
		$oldStage = Versioned::get_stage();
479
		Versioned::set_stage(Versioned::LIVE);
480
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
481
			'"SiteTree"."ID"' => $this->ID
482
		));
483
		if($live) {
484
			$link = $live->AbsoluteLink();
485
			if($includeStageEqualsLive) $link .= '?stage=Live';
486
		} else {
487
			$link = null;
488
		}
489
490
		Versioned::set_stage($oldStage);
491
		return $link;
492
	}
493
494
	/**
495
	 * Generates a link to edit this page in the CMS.
496
	 *
497
	 * @return string
498
	 */
499
	public function CMSEditLink() {
500
		return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
501
	}
502
503
504
	/**
505
	 * Return a CSS identifier generated from this page's link.
506
	 *
507
	 * @return string The URL segment
508
	 */
509
	public function ElementName() {
510
		return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
511
	}
512
513
	/**
514
	 * Returns true if this is the currently active page being used to handle this request.
515
	 *
516
	 * @return bool
517
	 */
518
	public function isCurrent() {
519
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
520
	}
521
522
	/**
523
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
524
	 * currently being viewed).
525
	 *
526
	 * @return bool
527
	 */
528
	public function isSection() {
529
		return $this->isCurrent() || (
530
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
531
		);
532
	}
533
534
	/**
535
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
536
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
537
	 * to external users.
538
	 *
539
	 * @return bool
540
	 */
541
	public function isOrphaned() {
542
		// Always false for root pages
543
		if(empty($this->ParentID)) return false;
544
545
		// Parent must exist and not be an orphan itself
546
		$parent = $this->Parent();
547
		return !$parent || !$parent->exists() || $parent->isOrphaned();
548
	}
549
550
	/**
551
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
552
	 *
553
	 * @return string
554
	 */
555
	public function LinkOrCurrent() {
556
		return $this->isCurrent() ? 'current' : 'link';
557
	}
558
559
	/**
560
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
561
	 *
562
	 * @return string
563
	 */
564
	public function LinkOrSection() {
565
		return $this->isSection() ? 'section' : 'link';
566
	}
567
568
	/**
569
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
570
	 * but in the current section.
571
	 *
572
	 * @return string
573
	 */
574
	public function LinkingMode() {
575
		if($this->isCurrent()) {
576
			return 'current';
577
		} elseif($this->isSection()) {
578
			return 'section';
579
		} else {
580
			return 'link';
581
		}
582
	}
583
584
	/**
585
	 * Check if this page is in the given current section.
586
	 *
587
	 * @param string $sectionName Name of the section to check
588
	 * @return bool True if we are in the given section
589
	 */
590
	public function InSection($sectionName) {
591
		$page = Director::get_current_page();
592
		while($page) {
593
			if($sectionName == $page->URLSegment)
594
				return true;
595
			$page = $page->Parent;
596
		}
597
		return false;
598
	}
599
600
	/**
601
	 * Create a duplicate of this node. Doesn't affect joined data - create a custom overloading of this if you need
602
	 * such behaviour.
603
	 *
604
	 * @param bool $doWrite Whether to write the new object before returning it
605
	 * @return self The duplicated object
606
	 */
607
	 public function duplicate($doWrite = true) {
608
609
		$page = parent::duplicate(false);
610
		$page->Sort = 0;
611
		$this->invokeWithExtensions('onBeforeDuplicate', $page);
612
613
		if($doWrite) {
614
			$page->write();
615
616
			$page = $this->duplicateManyManyRelations($this, $page);
617
		}
618
		$this->invokeWithExtensions('onAfterDuplicate', $page);
619
620
		return $page;
621
	}
622
623
	/**
624
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
625
	 *
626
	 * @return self The duplicated object
627
	 */
628
	public function duplicateWithChildren() {
629
		$clone = $this->duplicate();
630
		$children = $this->AllChildren();
631
632
		if($children) {
633
			foreach($children as $child) {
634
				$childClone = method_exists($child, 'duplicateWithChildren')
635
					? $child->duplicateWithChildren()
636
					: $child->duplicate();
637
				$childClone->ParentID = $clone->ID;
638
				$childClone->write();
639
			}
640
		}
641
642
		return $clone;
643
	}
644
645
	/**
646
	 * Duplicate this node and its children as a child of the node with the given ID
647
	 *
648
	 * @param int $id ID of the new node's new parent
649
	 */
650
	public function duplicateAsChild($id) {
651
		$newSiteTree = $this->duplicate();
652
		$newSiteTree->ParentID = $id;
653
		$newSiteTree->Sort = 0;
654
		$newSiteTree->write();
655
	}
656
657
	/**
658
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
659
	 *
660
	 * @param int $maxDepth The maximum depth to traverse.
661
	 * @param boolean $unlinked Whether to link page titles.
662
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
663
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
664
	 * @return HTMLText The breadcrumb trail.
665
	 */
666
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
667
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
668
		$template = new SSViewer('BreadcrumbsTemplate');
669
		return $template->process($this->customise(new ArrayData(array(
670
			"Pages" => $pages,
671
			"Unlinked" => $unlinked
672
		))));
673
	}
674
675
676
	/**
677
	 * Returns a list of breadcrumbs for the current page.
678
	 *
679
	 * @param int $maxDepth The maximum depth to traverse.
680
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
681
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
682
	 *
683
	 * @return ArrayList
684
	*/
685
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
686
		$page = $this;
687
		$pages = array();
688
689
		while(
690
			$page
691
 			&& (!$maxDepth || count($pages) < $maxDepth)
692
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
693
 		) {
694
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
695
				$pages[] = $page;
696
			}
697
698
			$page = $page->Parent;
699
		}
700
701
		return new ArrayList(array_reverse($pages));
702
	}
703
704
705
	/**
706
	 * Make this page a child of another page.
707
	 *
708
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
709
	 *
710
	 * @param SiteTree|int $item Either the parent object, or the parent ID
711
	 */
712
	public function setParent($item) {
713
		if(is_object($item)) {
714
			if (!$item->exists()) $item->write();
715
			$this->setField("ParentID", $item->ID);
716
		} else {
717
			$this->setField("ParentID", $item);
718
		}
719
	}
720
721
	/**
722
	 * Get the parent of this page.
723
	 *
724
	 * @return SiteTree Parent of this page
725
	 */
726
	public function getParent() {
727
		if ($parentID = $this->getField("ParentID")) {
728
			return DataObject::get_by_id("SiteTree", $parentID);
729
		}
730
	}
731
732
	/**
733
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
734
	 *
735
	 * @param int $level The maximum amount of levels to traverse.
736
	 * @param string $separator Seperating string
737
	 * @return string The resulting string
738
	 */
739
	public function NestedTitle($level = 2, $separator = " - ") {
740
		$item = $this;
741
		while($item && $level > 0) {
742
			$parts[] = $item->Title;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = 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...
743
			$item = $item->Parent;
744
			$level--;
745
		}
746
		return implode($separator, array_reverse($parts));
0 ignored issues
show
Bug introduced by
The variable $parts 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...
747
	}
748
749
	/**
750
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
751
	 * the security model for an application.
752
	 *
753
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
754
	 * - Checks for existence of a method named "can<$perm>()" on the object
755
	 * - Calls decorators and only returns for FALSE "vetoes"
756
	 * - Falls back to {@link Permission::check()}
757
	 * - Does NOT check for many-many relations named "Can<$perm>"
758
	 *
759
	 * @uses DataObjectDecorator->can()
760
	 *
761
	 * @param string $perm The permission to be checked, such as 'View'
762
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
763
	 * @param array $context Context argument for canCreate()
764
	 * @return bool True if the the member is allowed to do the given action
765
	 */
766
	public function can($perm, $member = null, $context = array()) {
767 View Code Duplication
		if(!$member || !(is_a($member, '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...
768
			$member = Member::currentUserID();
769
		}
770
771
		if($member && Permission::checkMember($member, "ADMIN")) return true;
772
773
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
774
			$method = 'can' . ucfirst($perm);
775
			return $this->$method($member);
776
		}
777
778
		$results = $this->extend('can', $member);
779
		if($results && is_array($results)) if(!min($results)) return false;
780
781
		return ($member && Permission::checkMember($member, $perm));
782
	}
783
784
	/**
785
	 * This function should return true if the current user can add children to this page. It can be overloaded to
786
	 * customise the security model for an application.
787
	 *
788
	 * Denies permission if any of the following conditions is true:
789
	 * - alternateCanAddChildren() on a extension returns false
790
	 * - canEdit() is not granted
791
	 * - There are no classes defined in {@link $allowed_children}
792
	 *
793
	 * @uses SiteTreeExtension->canAddChildren()
794
	 * @uses canEdit()
795
	 * @uses $allowed_children
796
	 *
797
	 * @param Member|int $member
798
	 * @return bool True if the current user can add children
799
	 */
800
	public function canAddChildren($member = null) {
801
		// Disable adding children to archived pages
802
		if($this->getIsDeletedFromStage()) {
803
			return false;
804
		}
805
806 View Code Duplication
		if(!$member || !(is_a($member, '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...
807
			$member = Member::currentUserID();
808
		}
809
810
		if($member && Permission::checkMember($member, "ADMIN")) return true;
811
812
		// Standard mechanism for accepting permission changes from extensions
813
		$extended = $this->extendedCan('canAddChildren', $member);
814
		if($extended !== null) return $extended;
815
816
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
817
	}
818
819
	/**
820
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
821
	 * security model for an application.
822
	 *
823
	 * Denies permission if any of the following conditions is true:
824
	 * - canView() on any extension returns false
825
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
826
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
827
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
828
	 *
829
	 * @uses DataExtension->canView()
830
	 * @uses ViewerGroups()
831
	 *
832
	 * @param Member|int $member
833
	 * @return bool True if the current user can view this page
834
	 */
835
	public function canView($member = null) {
836 View Code Duplication
		if(!$member || !(is_a($member, '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...
837
			$member = Member::currentUserID();
838
		}
839
840
		// admin override
841
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
842
843
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
844
		if($this->isOrphaned()) return false;
845
846
		// Standard mechanism for accepting permission changes from extensions
847
		$extended = $this->extendedCan('canView', $member);
848
		if($extended !== null) return $extended;
849
850
		// check for empty spec
851
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
852
853
		// check for inherit
854
		if($this->CanViewType == 'Inherit') {
855
			if($this->ParentID) return $this->Parent()->canView($member);
856
			else return $this->getSiteConfig()->canViewPages($member);
857
		}
858
859
		// check for any logged-in users
860
		if($this->CanViewType == 'LoggedInUsers' && $member) {
861
			return true;
862
		}
863
864
		// check for specific groups
865
		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
866
		if(
867
			$this->CanViewType == 'OnlyTheseUsers'
868
			&& $member
869
			&& $member->inGroups($this->ViewerGroups())
870
		) return true;
871
872
		return false;
873
	}
874
875
	/**
876
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
877
	 * security model for an application.
878
	 *
879
	 * Denies permission if any of the following conditions is true:
880
	 * - canDelete() returns false on any extension
881
	 * - canEdit() returns false
882
	 * - any descendant page returns false for canDelete()
883
	 *
884
	 * @uses canDelete()
885
	 * @uses SiteTreeExtension->canDelete()
886
	 * @uses canEdit()
887
	 *
888
	 * @param Member $member
889
	 * @return bool True if the current user can delete this page
890
	 */
891
	public function canDelete($member = null) {
892 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...
Bug introduced by
The class Member does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
893
		else if(is_numeric($member)) $memberID = $member;
894
		else $memberID = Member::currentUserID();
895
896
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
897
			return true;
898
		}
899
900
		// Standard mechanism for accepting permission changes from extensions
901
		$extended = $this->extendedCan('canDelete', $memberID);
902
		if($extended !== null) return $extended;
903
904
		// Regular canEdit logic is handled by can_edit_multiple
905
		$results = self::can_delete_multiple(array($this->ID), $memberID);
906
907
		// If this page no longer exists in stage/live results won't contain the page.
908
		// Fail-over to false
909
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
910
	}
911
912
	/**
913
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
914
	 * can be overloaded to customise the security model for an application.
915
	 *
916
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
917
	 * create beneath a parent is based on the ability to edit that parent page.
918
	 *
919
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
920
	 *
921
	 * @uses $can_create
922
	 * @uses DataExtension->canCreate()
923
	 *
924
	 * @param Member $member
925
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
926
	 *                       If a parent page is known, it will be checked for validity.
927
	 *                       If omitted, it will be assumed this is to be created as a top level page.
928
	 * @return bool True if the current user can create pages on this class.
929
	 */
930
	public function canCreate($member = null, $context = array()) {
931 View Code Duplication
		if(!$member || !(is_a($member, '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...
932
			$member = Member::currentUserID();
933
		}
934
935
		// Check parent (custom canCreate option for SiteTree)
936
		// Block children not allowed for this parent type
937
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
938
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
939
			return false;
940
		}
941
942
		// Check permission
943
		if($member && Permission::checkMember($member, "ADMIN")) {
944
			return true;
945
		}
946
947
		// Standard mechanism for accepting permission changes from extensions
948
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
949
		if($extended !== null) {
950
			return $extended;
951
		}
952
953
		// Fall over to inherited permissions
954
		if($parent) {
955
			return $parent->canAddChildren($member);
956
		} else {
957
			// This doesn't necessarily mean we are creating a root page, but that
958
			// we don't know if there is a parent, so default to this permission
959
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
960
		}
961
	}
962
963
	/**
964
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
965
	 * security model for an application.
966
	 *
967
	 * Denies permission if any of the following conditions is true:
968
	 * - canEdit() on any extension returns false
969
	 * - canView() return false
970
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
971
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
972
	 *   CMS_Access_CMSMAIN permission code
973
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
974
	 *
975
	 * @uses canView()
976
	 * @uses EditorGroups()
977
	 * @uses DataExtension->canEdit()
978
	 *
979
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
980
	 *                       unit tests)
981
	 * @return bool True if the current user can edit this page
982
	 */
983
	public function canEdit($member = null) {
984 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...
Bug introduced by
The class Member does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
985
		else if(is_numeric($member)) $memberID = $member;
986
		else $memberID = Member::currentUserID();
987
988
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
989
990
		// Standard mechanism for accepting permission changes from extensions
991
		$extended = $this->extendedCan('canEdit', $memberID);
992
		if($extended !== null) return $extended;
993
994
		if($this->ID) {
995
			// Regular canEdit logic is handled by can_edit_multiple
996
			$results = self::can_edit_multiple(array($this->ID), $memberID);
997
998
			// If this page no longer exists in stage/live results won't contain the page.
999
			// Fail-over to false
1000
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1001
1002
		// Default for unsaved pages
1003
		} else {
1004
			return $this->getSiteConfig()->canEditPages($member);
1005
		}
1006
	}
1007
1008
	/**
1009
	 * Stub method to get the site config, unless the current class can provide an alternate.
1010
	 *
1011
	 * @return SiteConfig
1012
	 */
1013
	public function getSiteConfig() {
1014
1015
		if($this->hasMethod('alternateSiteConfig')) {
1016
			$altConfig = $this->alternateSiteConfig();
1017
			if($altConfig) return $altConfig;
1018
		}
1019
1020
		return SiteConfig::current_site_config();
1021
	}
1022
1023
	/**
1024
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1025
	 * can_(perm)_multiple method for efficiency.
1026
	 *
1027
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1028
	 * @param array           $ids           An array of page IDs
1029
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1030
	 *                                       to 'SiteTree::can_(permission)_multiple'
1031
	 */
1032
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1033
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1034
1035
		if(is_callable($batchCallback)) {
1036
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1037
		} else {
1038
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1039
				. "with callback '$batchCallback'", E_USER_WARNING);
1040
		}
1041
	}
1042
1043
	/**
1044
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1045
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1046
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1047
	 * efficiently.
1048
	 *
1049
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1050
	 * property to FALSE.
1051
	 *
1052
	 * @param array  $ids              Of {@link SiteTree} IDs
1053
	 * @param int    $memberID         Member ID
1054
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1055
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1056
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1057
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1058
	 * @param bool   $useCached
1059
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1060
	 */
1061
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1062
												  $globalPermission = null, $useCached = true) {
1063
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1064
1065
		// Sanitise the IDs
1066
		$ids = array_filter($ids, 'is_numeric');
1067
1068
		// This is the name used on the permission cache
1069
		// converts something like 'CanEditType' to 'edit'.
1070
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1071
1072
		// Default result: nothing editable
1073
		$result = array_fill_keys($ids, false);
1074
		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...
1075
1076
			// Look in the cache for values
1077
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1078
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1079
1080
				// If we can't find everything in the cache, then look up the remainder separately
1081
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1082
				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...
1083
					$cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
1084
				}
1085
				return $cachedValues;
1086
			}
1087
1088
			// If a member doesn't have a certain permission then they can't edit anything
1089
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1090
				return $result;
1091
			}
1092
1093
			// Placeholder for parameterised ID list
1094
			$idPlaceholders = DB::placeholders($ids);
1095
1096
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1097
			// be enabled
1098
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1099
1100
			// Get the groups that the given member belongs to
1101
			$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
1102
			$SQL_groupList = implode(", ", $groupIDs);
1103
			if (!$SQL_groupList) $SQL_groupList = '0';
1104
1105
			$combinedStageResult = array();
1106
1107
			foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1108
				// Start by filling the array with the pages that actually exist
1109
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1110
1111
				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...
1112
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1113
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1114
				} else {
1115
					$stageIds = array();
1116
				}
1117
				$result = array_fill_keys($stageIds, false);
1118
1119
				// Get the uninherited permissions
1120
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1121
					->where(array(
1122
						"(\"$typeField\" = 'LoggedInUsers' OR
1123
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1124
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1125
						=> $ids
1126
					))
1127
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1128
1129
				if($uninheritedPermissions) {
1130
					// Set all the relevant items in $result to true
1131
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1132
				}
1133
1134
				// Get permissions that are inherited
1135
				$potentiallyInherited = Versioned::get_by_stage(
1136
					"SiteTree",
1137
					$stage,
1138
					array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
1139
				);
1140
1141
				if($potentiallyInherited) {
1142
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1143
					// then see which ones the user has permission on
1144
					$groupedByParent = array();
1145
					foreach($potentiallyInherited as $item) {
1146
						if($item->ParentID) {
1147
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1148
							$groupedByParent[$item->ParentID][] = $item->ID;
1149
						} else {
1150
							// Might return different site config based on record context, e.g. when subsites module
1151
							// is used
1152
							$siteConfig = $item->getSiteConfig();
1153
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1154
						}
1155
					}
1156
1157
					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...
1158
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1159
						if($actuallyInherited) {
1160
							$parentIDs = array_keys(array_filter($actuallyInherited));
1161
							foreach($parentIDs as $parentID) {
1162
								// Set all the relevant items in $result to true
1163
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1164
							}
1165
						}
1166
					}
1167
				}
1168
1169
				$combinedStageResult = $combinedStageResult + $result;
1170
1171
			}
1172
		}
1173
1174
		if(isset($combinedStageResult)) {
1175
			// Cache the results
1176
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1177
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1178
1179
			return $combinedStageResult;
1180
		} else {
1181
			return array();
1182
		}
1183
	}
1184
1185
	/**
1186
	 * Get the 'can edit' information for a number of SiteTree pages.
1187
	 *
1188
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1189
	 * @param int   $memberID  ID of member
1190
	 * @param bool  $useCached Return values from the permission cache if they exist
1191
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1192
	 *                         edited
1193
	 */
1194
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1195
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1196
	}
1197
1198
	/**
1199
	 * Get the 'can edit' information for a number of SiteTree pages.
1200
	 *
1201
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1202
	 * @param int   $memberID  ID of member
1203
	 * @param bool  $useCached Return values from the permission cache if they exist
1204
	 * @return array
1205
	 */
1206
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1207
		$deletable = array();
0 ignored issues
show
Unused Code introduced by
$deletable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1208
		$result = array_fill_keys($ids, false);
1209
		$cacheKey = "delete-$memberID";
1210
1211
		// Look in the cache for values
1212
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1213
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1214
1215
			// If we can't find everything in the cache, then look up the remainder separately
1216
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1217
			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...
1218
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1219
					+ $cachedValues;
1220
			}
1221
			return $cachedValues;
1222
		}
1223
1224
		// You can only delete pages that you can edit
1225
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1226
		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...
1227
1228
			// You can only delete pages whose children you can delete
1229
			$editablePlaceholders = DB::placeholders($editableIDs);
1230
			$childRecords = SiteTree::get()->where(array(
1231
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1232
			));
1233
			if($childRecords) {
1234
				$children = $childRecords->map("ID", "ParentID");
1235
1236
				// Find out the children that can be deleted
1237
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1238
1239
				// Get a list of all the parents that have no undeletable children
1240
				$deletableParents = array_fill_keys($editableIDs, true);
1241
				foreach($deletableChildren as $id => $canDelete) {
1242
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1243
				}
1244
1245
				// Use that to filter the list of deletable parents that have children
1246
				$deletableParents = array_keys($deletableParents);
1247
1248
				// Also get the $ids that don't have children
1249
				$parents = array_unique($children->values());
1250
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1251
1252
				// Combine the two
1253
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1254
1255
			} else {
1256
				$deletable = $editableIDs;
1257
			}
1258
		} else {
1259
			$deletable = array();
1260
		}
1261
1262
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1263
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1264
	}
1265
1266
	/**
1267
	 * Collate selected descendants of this page.
1268
	 *
1269
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1270
	 * $collator array.
1271
	 *
1272
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1273
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1274
	 * @return bool
1275
	 */
1276
	public function collateDescendants($condition, &$collator) {
1277
		if($children = $this->Children()) {
1278
			foreach($children as $item) {
1279
				if(eval("return $condition;")) $collator[] = $item;
1280
				$item->collateDescendants($condition, $collator);
1281
			}
1282
			return true;
1283
		}
1284
	}
1285
1286
	/**
1287
	 * Return the title, description, keywords and language metatags.
1288
	 *
1289
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1290
	 *
1291
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1292
	 * @return string The XHTML metatags
1293
	 */
1294
	public function MetaTags($includeTitle = true) {
1295
		$tags = "";
1296
		if($includeTitle === true || $includeTitle == 'true') {
1297
			$tags .= "<title>" . Convert::raw2xml($this->Title) . "</title>\n";
1298
		}
1299
1300
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1301
		if (!empty($generator)) {
1302
			$tags .= "<meta name=\"generator\" content=\"" . Convert::raw2att($generator) . "\" />\n";
1303
		}
1304
1305
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1306
		$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
1307
		if($this->MetaDescription) {
1308
			$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
1309
		}
1310
		if($this->ExtraMeta) {
1311
			$tags .= $this->ExtraMeta . "\n";
1312
		}
1313
1314
		if(Permission::check('CMS_ACCESS_CMSMain')
1315
			&& in_array('CMSPreviewable', class_implements($this))
1316
			&& !$this instanceof ErrorPage
1317
			&& $this->ID > 0
1318
		) {
1319
			$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
1320
			$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
1321
		}
1322
1323
		$this->extend('MetaTags', $tags);
1324
1325
		return $tags;
1326
	}
1327
1328
	/**
1329
	 * Returns the object that contains the content that a user would associate with this page.
1330
	 *
1331
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1332
	 * return the page that is linked to.
1333
	 *
1334
	 * @return $this
1335
	 */
1336
	public function ContentSource() {
1337
		return $this;
1338
	}
1339
1340
	/**
1341
	 * Add default records to database.
1342
	 *
1343
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1344
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1345
	 */
1346
	public function requireDefaultRecords() {
1347
		parent::requireDefaultRecords();
1348
1349
		// default pages
1350
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1351
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1352
				$homepage = new Page();
1353
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1354
				$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>');
1355
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1356
				$homepage->Sort = 1;
1357
				$homepage->write();
1358
				$homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1359
				$homepage->flushCache();
1360
				DB::alteration_message('Home page created', 'created');
1361
			}
1362
1363
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1364
				$aboutus = new Page();
1365
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1366
				$aboutus->Content = _t('SiteTree.DEFAULTABOUTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
1367
				$aboutus->Sort = 2;
1368
				$aboutus->write();
1369
				$aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1370
				$aboutus->flushCache();
1371
				DB::alteration_message('About Us page created', 'created');
1372
1373
				$contactus = new Page();
1374
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1375
				$contactus->Content = _t('SiteTree.DEFAULTCONTACTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
1376
				$contactus->Sort = 3;
1377
				$contactus->write();
1378
				$contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1379
				$contactus->flushCache();
1380
				DB::alteration_message('Contact Us page created', 'created');
1381
			}
1382
		}
1383
1384
		// schema migration
1385
		// @todo Move to migration task once infrastructure is implemented
1386
		if($this->class == 'SiteTree') {
1387
			$conn = DB::get_schema();
1388
			// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
1389
			if($conn->hasField('SiteTree' ,'Viewers')) {
1390
				$task = new UpgradeSiteTreePermissionSchemaTask();
1391
				$task->run(new SS_HTTPRequest('GET','/'));
1392
			}
1393
		}
1394
	}
1395
1396
	protected function onBeforeWrite() {
1397
		parent::onBeforeWrite();
1398
1399
		// If Sort hasn't been set, make this page come after it's siblings
1400
		if(!$this->Sort) {
1401
			$parentID = ($this->ParentID) ? $this->ParentID : 0;
1402
			$this->Sort = DB::prepared_query(
1403
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1404
				array($parentID)
1405
			)->value();
1406
		}
1407
1408
		// If there is no URLSegment set, generate one from Title
1409
		$defaultSegment = $this->generateURLSegment(_t(
1410
			'CMSMain.NEWPAGE',
1411
			array('pagetype' => $this->i18n_singular_name())
1412
		));
1413
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1414
			$this->URLSegment = $this->generateURLSegment($this->Title);
1415
		} else if($this->isChanged('URLSegment', 2)) {
1416
			// Do a strict check on change level, to avoid double encoding caused by
1417
			// bogus changes through forceChange()
1418
			$filter = URLSegmentFilter::create();
1419
			$this->URLSegment = $filter->filter($this->URLSegment);
1420
			// If after sanitising there is no URLSegment, give it a reasonable default
1421
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1422
		}
1423
1424
		// Ensure that this object has a non-conflicting URLSegment value.
1425
		$count = 2;
1426
		while(!$this->validURLSegment()) {
1427
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1428
			$count++;
1429
		}
1430
1431
		$this->syncLinkTracking();
1432
1433
		// Check to see if we've only altered fields that shouldn't affect versioning
1434
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1435
		$changedFields = array_keys($this->getChangedFields(true, 2));
1436
1437
		// This more rigorous check is inline with the test that write() does to dedcide whether or not to write to the
1438
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1439
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1440
1441
		if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oneChangedFields 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...
1442
			// This will have the affect of preserving the versioning
1443
			$this->migrateVersion($this->Version);
1444
		}
1445
	}
1446
1447
	/**
1448
	 * Trigger synchronisation of link tracking
1449
	 *
1450
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1451
	 */
1452
	public function syncLinkTracking() {
1453
		$this->extend('augmentSyncLinkTracking');
1454
	}
1455
1456
	public function onBeforeDelete() {
1457
		parent::onBeforeDelete();
1458
1459
		// If deleting this page, delete all its children.
1460
		if(SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
1461
			foreach($children as $child) {
1462
				$child->delete();
1463
			}
1464
		}
1465
	}
1466
1467
	public function onAfterDelete() {
1468
		// Need to flush cache to avoid outdated versionnumber references
1469
		$this->flushCache();
1470
1471
		// Need to mark pages depending to this one as broken
1472
		$dependentPages = $this->DependentPages();
1473
		if($dependentPages) foreach($dependentPages as $page) {
1474
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1475
			$page->write();
1476
		}
1477
1478
		parent::onAfterDelete();
1479
	}
1480
1481
	public function flushCache($persistent = true) {
1482
		parent::flushCache($persistent);
1483
		$this->_cache_statusFlags = null;
1484
	}
1485
1486
	public function validate() {
1487
		$result = parent::validate();
1488
1489
		// Allowed children validation
1490
		$parent = $this->getParent();
1491
		if($parent && $parent->exists()) {
1492
			// No need to check for subclasses or instanceof, as allowedChildren() already
1493
			// deconstructs any inheritance trees already.
1494
			$allowed = $parent->allowedChildren();
1495
			$subject = ($this instanceof VirtualPage && $this->CopyContentFromID) ? $this->CopyContentFrom() : $this;
1496
			if(!in_array($subject->ClassName, $allowed)) {
1497
1498
				$result->error(
1499
					_t(
1500
						'SiteTree.PageTypeNotAllowed',
1501
						'Page type "{type}" not allowed as child of this parent page',
1502
						array('type' => $subject->i18n_singular_name())
1503
					),
1504
					'ALLOWED_CHILDREN'
1505
				);
1506
			}
1507
		}
1508
1509
		// "Can be root" validation
1510
		if(!$this->stat('can_be_root') && !$this->ParentID) {
1511
			$result->error(
1512
				_t(
1513
					'SiteTree.PageTypNotAllowedOnRoot',
1514
					'Page type "{type}" is not allowed on the root level',
1515
					array('type' => $this->i18n_singular_name())
1516
				),
1517
				'CAN_BE_ROOT'
1518
			);
1519
		}
1520
1521
		return $result;
1522
	}
1523
1524
	/**
1525
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1526
	 * checks for:
1527
	 *  - A page with the same URLSegment that has a conflict
1528
	 *  - Conflicts with actions on the parent page
1529
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1530
	 *
1531
	 * @return bool
1532
	 */
1533
	public function validURLSegment() {
1534
		if(self::config()->nested_urls && $parent = $this->Parent()) {
1535
			if($controller = ModelAsController::controller_for($parent)) {
1536
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
0 ignored issues
show
Bug introduced by
The class Controller does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1537
			}
1538
		}
1539
1540
		if(!self::config()->nested_urls || !$this->ParentID) {
1541
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of returns inconsistent results on some PHP versions for interfaces; you could instead use ReflectionClass::implementsInterface.
Loading history...
1542
		}
1543
1544
		// Filters by url, id, and parent
1545
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1546
		if($this->ID) {
1547
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1548
		}
1549
		if(self::config()->nested_urls) {
1550
			$filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
1551
		}
1552
1553
		$votes = array_filter(
1554
			(array)$this->extend('augmentValidURLSegment'),
1555
			function($v) {return !is_null($v);}
1556
		);
1557
		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...
1558
			return min($votes);
1559
		}
1560
1561
		// Check existence
1562
		$existingPage = DataObject::get_one('SiteTree', $filter);
1563
		if ($existingPage) return false;
1564
1565
		return !($existingPage);
1566
		}
1567
1568
	/**
1569
	 * Generate a URL segment based on the title provided.
1570
	 *
1571
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1572
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1573
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1574
	 * scratch, or incrementally modify the generated URL.
1575
	 *
1576
	 * @param string $title Page title
1577
	 * @return string Generated url segment
1578
	 */
1579
	public function generateURLSegment($title){
1580
		$filter = URLSegmentFilter::create();
1581
		$t = $filter->filter($title);
1582
1583
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1584
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1585
1586
		// Hook for extensions
1587
		$this->extend('updateURLSegment', $t, $title);
1588
1589
		return $t;
1590
	}
1591
1592
	/**
1593
	 * Gets the URL segment for the latest draft version of this page.
1594
	 *
1595
	 * @return string
1596
	 */
1597
	public function getStageURLSegment() {
1598
		$stageRecord = Versioned::get_one_by_stage('SiteTree', Versioned::DRAFT, array(
1599
			'"SiteTree"."ID"' => $this->ID
1600
		));
1601
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1602
	}
1603
1604
	/**
1605
	 * Gets the URL segment for the currently published version of this page.
1606
	 *
1607
	 * @return string
1608
	 */
1609
	public function getLiveURLSegment() {
1610
		$liveRecord = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
1611
			'"SiteTree"."ID"' => $this->ID
1612
		));
1613
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1614
	}
1615
1616
	/**
1617
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1618
	 *
1619
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1620
	 * @return ArrayList
1621
	 */
1622
	public function DependentPages($includeVirtuals = true) {
1623
		if(class_exists('Subsite')) {
1624
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1625
			Subsite::disable_subsite_filter(true);
1626
		}
1627
1628
		// Content links
1629
		$items = new ArrayList();
1630
1631
		// We merge all into a regular SS_List, because DataList doesn't support merge
1632
		if($contentLinks = $this->BackLinkTracking()) {
1633
			$linkList = new ArrayList();
1634
			foreach($contentLinks as $item) {
1635
				$item->DependentLinkType = 'Content link';
1636
				$linkList->push($item);
1637
			}
1638
			$items->merge($linkList);
1639
		}
1640
1641
		// Virtual pages
1642
		if($includeVirtuals) {
1643
			$virtuals = $this->VirtualPages();
1644
			if($virtuals) {
1645
				$virtualList = new ArrayList();
1646
				foreach($virtuals as $item) {
1647
					$item->DependentLinkType = 'Virtual page';
1648
					$virtualList->push($item);
1649
				}
1650
				$items->merge($virtualList);
1651
			}
1652
		}
1653
1654
		// Redirector pages
1655
		$redirectors = RedirectorPage::get()->where(array(
1656
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1657
			'"RedirectorPage"."LinkToID"' => $this->ID
1658
		));
1659
		if($redirectors) {
1660
			$redirectorList = new ArrayList();
1661
			foreach($redirectors as $item) {
1662
				$item->DependentLinkType = 'Redirector page';
1663
				$redirectorList->push($item);
1664
			}
1665
			$items->merge($redirectorList);
1666
		}
1667
1668
		if(class_exists('Subsite')) 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...
1669
1670
		return $items;
1671
	}
1672
1673
	/**
1674
	 * Return all virtual pages that link to this page.
1675
	 *
1676
	 * @return DataList
1677
	 */
1678
	public function VirtualPages() {
1679
		$pages = parent::VirtualPages();
1680
1681
		// Disable subsite filter for these pages
1682
		if($pages instanceof DataList) {
0 ignored issues
show
Bug introduced by
The class DataList does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1683
			return $pages->setDataQueryParam('Subsite.filter', false);
1684
		} else {
1685
			return $pages;
1686
		}
1687
	}
1688
1689
	/**
1690
	 * Returns a FieldList with which to create the main editing form.
1691
	 *
1692
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1693
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1694
	 *
1695
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1696
	 * e.g. access control.
1697
	 *
1698
	 * @return FieldList The fields to be displayed in the CMS
1699
	 */
1700
	public function getCMSFields() {
1701
		require_once("forms/Form.php");
1702
		// Status / message
1703
		// Create a status message for multiple parents
1704
		if($this->ID && is_numeric($this->ID)) {
1705
			$linkedPages = $this->VirtualPages();
1706
1707
			$parentPageLinks = array();
1708
1709
			if($linkedPages->Count() > 0) {
1710
				foreach($linkedPages as $linkedPage) {
1711
					$parentPage = $linkedPage->Parent;
1712
					if($parentPage) {
1713
						if($parentPage->ID) {
1714
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1715
						} else {
1716
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1717
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1718
								"</a>";
1719
						}
1720
					}
1721
				}
1722
1723
				$lastParent = array_pop($parentPageLinks);
1724
				$parentList = "'$lastParent'";
1725
1726
				if(count($parentPageLinks) > 0) {
1727
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1728
						. $parentList;
1729
				}
1730
1731
				$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...
1732
					'SiteTree.APPEARSVIRTUALPAGES',
1733
					"This content also appears on the virtual pages in the {title} sections.",
1734
					array('title' => $parentList)
1735
				);
1736
			}
1737
		}
1738
1739
		if($this->HasBrokenLink || $this->HasBrokenFile) {
1740
			$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...
1741
		}
1742
1743
		$dependentNote = '';
1744
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1745
1746
		// Create a table for showing pages linked to this one
1747
		$dependentPages = $this->DependentPages();
1748
		$dependentPagesCount = $dependentPages->Count();
1749
		if($dependentPagesCount) {
1750
			$dependentColumns = array(
1751
				'Title' => $this->fieldLabel('Title'),
1752
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1753
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1754
			);
1755
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1756
1757
			$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>');
1758
			$dependentTable = GridField::create(
1759
				'DependentPages',
1760
				false,
1761
				$dependentPages
1762
			);
1763
			$dependentTable->getConfig()->getComponentByType('GridFieldDataColumns')
1764
				->setDisplayFields($dependentColumns)
1765
				->setFieldFormatting(array(
1766
					'Title' => function($value, &$item) {
1767
						return sprintf(
1768
							'<a href="admin/pages/edit/show/%d">%s</a>',
1769
							(int)$item->ID,
1770
							Convert::raw2xml($item->Title)
1771
						);
1772
					},
1773
					'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...
1774
						return sprintf(
1775
							'<a href="%s" target="_blank">%s</a>',
1776
							Convert::raw2xml($value),
1777
							Convert::raw2xml($value)
1778
						);
1779
					}
1780
				));
1781
		}
1782
1783
		$baseLink = Controller::join_links (
1784
			Director::absoluteBaseURL(),
1785
			(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
1786
		);
1787
1788
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1789
			->setURLPrefix($baseLink)
1790
			->setDefaultURL($this->generateURLSegment(_t(
1791
				'CMSMain.NEWPAGE',
1792
				array('pagetype' => $this->i18n_singular_name())
1793
			)));
1794
		$helpText = (self::config()->nested_urls && count($this->Children())) ? $this->fieldLabel('LinkChangeNote') : '';
1795
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1796
			$helpText .= $helpText ? '<br />' : '';
1797
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1798
		}
1799
		$urlsegment->setHelpText($helpText);
1800
1801
		$fields = new FieldList(
1802
			$rootTab = new TabSet("Root",
1803
				$tabMain = new Tab('Main',
1804
					new TextField("Title", $this->fieldLabel('Title')),
1805
					$urlsegment,
1806
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1807
					$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1808
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1809
						array(
1810
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1811
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1812
						)
1813
					)->setHeadingLevel(4)
1814
				),
1815
				$tabDependent = new Tab('Dependent',
1816
					$dependentNote,
1817
					$dependentTable
1818
				)
1819
			)
1820
		);
1821
		$htmlField->addExtraClass('stacked');
1822
1823
		// Help text for MetaData on page content editor
1824
		$metaFieldDesc
1825
			->setRightTitle(
1826
				_t(
1827
					'SiteTree.METADESCHELP',
1828
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1829
				)
1830
			)
1831
			->addExtraClass('help');
1832
		$metaFieldExtra
1833
			->setRightTitle(
1834
				_t(
1835
					'SiteTree.METAEXTRAHELP',
1836
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1837
				)
1838
			)
1839
			->addExtraClass('help');
1840
1841
		// Conditional dependent pages tab
1842
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1843
		else $fields->removeFieldFromTab('Root', 'Dependent');
1844
1845
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1846
1847
		if($this->ObsoleteClassName) {
1848
			$obsoleteWarning = _t(
1849
				'SiteTree.OBSOLETECLASS',
1850
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1851
				array('type' => $this->ObsoleteClassName)
1852
			);
1853
1854
			$fields->addFieldToTab(
1855
				"Root.Main",
1856
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1857
				"Title"
1858
			);
1859
		}
1860
1861
		if(file_exists(BASE_PATH . '/install.php')) {
1862
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
1863
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
1864
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
1865
				. "</p>"), "Title");
1866
		}
1867
1868
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
1869
		$fields->setTabPathRewrites(array(
1870
			'/^Root\.Content\.Main$/' => 'Root.Main',
1871
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
1872
		));
1873
1874
		if(self::$runCMSFieldsExtensions) {
1875
			$this->extend('updateCMSFields', $fields);
1876
		}
1877
1878
		return $fields;
1879
	}
1880
1881
1882
	/**
1883
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1884
	 * for content-related fields.
1885
	 *
1886
	 * @return FieldList
1887
	 */
1888
	public function getSettingsFields() {
1889
		$groupsMap = array();
1890
		foreach(Group::get() as $group) {
1891
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
1892
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1893
		}
1894
		asort($groupsMap);
1895
1896
		$fields = new FieldList(
1897
			$rootTab = new TabSet("Root",
1898
				$tabBehaviour = new Tab('Settings',
1899
					new DropdownField(
1900
						"ClassName",
1901
						$this->fieldLabel('ClassName'),
1902
						$this->getClassDropdown()
1903
					),
1904
					$parentTypeSelector = new CompositeField(
1905
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
1906
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1907
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1908
						)),
1909
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
1910
					),
1911
					$visibility = new FieldGroup(
1912
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1913
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1914
					),
1915
					$viewersOptionsField = new OptionsetField(
1916
						"CanViewType",
1917
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
1918
					),
1919
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
1920
						->setSource($groupsMap)
1921
						->setAttribute(
1922
							'data-placeholder',
1923
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
1924
						),
1925
					$editorsOptionsField = new OptionsetField(
1926
						"CanEditType",
1927
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
1928
					),
1929
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
1930
						->setSource($groupsMap)
1931
						->setAttribute(
1932
							'data-placeholder',
1933
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
1934
						)
1935
				)
1936
			)
1937
		);
1938
1939
		$visibility->setTitle($this->fieldLabel('Visibility'));
1940
1941
1942
		// This filter ensures that the ParentID dropdown selection does not show this node,
1943
		// or its descendents, as this causes vanishing bugs
1944
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
1945
		$parentTypeSelector->addExtraClass('parentTypeSelector');
1946
1947
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
1948
1949
		// Make page location fields read-only if the user doesn't have the appropriate permission
1950
		if(!Permission::check("SITETREE_REORGANISE")) {
1951
			$fields->makeFieldReadonly('ParentType');
1952
			if($this->ParentType == 'root') {
1953
				$fields->removeByName('ParentID');
1954
			} else {
1955
				$fields->makeFieldReadonly('ParentID');
1956
			}
1957
		}
1958
1959
		$viewersOptionsSource = array();
1960
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
1961
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
1962
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
1963
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
1964
		$viewersOptionsField->setSource($viewersOptionsSource);
1965
1966
		$editorsOptionsSource = array();
1967
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
1968
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
1969
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
1970
		$editorsOptionsField->setSource($editorsOptionsSource);
1971
1972
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
1973
			$fields->makeFieldReadonly($viewersOptionsField);
1974
			if($this->CanViewType == 'OnlyTheseUsers') {
1975
				$fields->makeFieldReadonly($viewerGroupsField);
1976
			} else {
1977
				$fields->removeByName('ViewerGroups');
1978
			}
1979
1980
			$fields->makeFieldReadonly($editorsOptionsField);
1981
			if($this->CanEditType == 'OnlyTheseUsers') {
1982
				$fields->makeFieldReadonly($editorGroupsField);
1983
			} else {
1984
				$fields->removeByName('EditorGroups');
1985
			}
1986
		}
1987
1988
		if(self::$runCMSFieldsExtensions) {
1989
			$this->extend('updateSettingsFields', $fields);
1990
		}
1991
1992
		return $fields;
1993
	}
1994
1995
	/**
1996
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
1997
	 * @return array
1998
	 */
1999
	public function fieldLabels($includerelations = true) {
2000
		$cacheKey = $this->class . '_' . $includerelations;
2001
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2002
			$labels = parent::fieldLabels($includerelations);
2003
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2004
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2005
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2006
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2007
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2008
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2009
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2010
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2011
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2012
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2013
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2014
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2015
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2016
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2017
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2018
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2019
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2020
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2021
			$labels['LinkChangeNote'] = _t (
2022
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2023
			);
2024
2025
			if($includerelations){
2026
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2027
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2028
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2029
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2030
			}
2031
2032
			self::$_cache_field_labels[$cacheKey] = $labels;
2033
		}
2034
2035
		return self::$_cache_field_labels[$cacheKey];
2036
	}
2037
2038
	/**
2039
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2040
	 *
2041
	 * Frontend scripts and styles know how to handle the following FormFields:
2042
	 * - top-level FormActions appear as standalone buttons
2043
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2044
	 * - TabSet & Tabs appear as a drop ups
2045
	 * - FormActions within the Tab are restyled as links
2046
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2047
	 *
2048
	 * @return FieldList The available actions for this page.
2049
	 */
2050
	public function getCMSActions() {
2051
		$existsOnLive = $this->isPublished();
2052
2053
		// Major actions appear as buttons immediately visible as page actions.
2054
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset noborder');
2055
2056
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2057
		$rootTabSet = new TabSet('ActionMenus');
2058
		$moreOptions = new Tab(
2059
			'MoreOptions',
2060
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2061
		);
2062
		$rootTabSet->push($moreOptions);
2063
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2064
2065
		// Render page information into the "more-options" drop-up, on the top.
2066
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
2067
			'"SiteTree"."ID"' => $this->ID
2068
		));
2069
		$moreOptions->push(
2070
			new LiteralField('Information',
2071
				$this->customise(array(
2072
					'Live' => $live,
2073
					'ExistsOnLive' => $existsOnLive
2074
				))->renderWith('SiteTree_Information')
2075
			)
2076
		);
2077
2078
		// "readonly"/viewing version that isn't the current version of the record
2079
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), array(
2080
			'"SiteTree"."ID"' => $this->ID
2081
		));
2082
		if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
2083
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2084
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2085
2086
			$actions = new FieldList(array($majorActions, $rootTabSet));
2087
2088
			// getCMSActions() can be extended with updateCMSActions() on a extension
2089
			$this->extend('updateCMSActions', $actions);
2090
2091
			return $actions;
2092
		}
2093
2094
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
2095
			// "unpublish"
2096
			$moreOptions->push(
2097
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2098
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2099
					->addExtraClass('ss-ui-action-destructive')
2100
			);
2101
		}
2102
2103
		if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE) && !$this->getIsDeletedFromStage()) {
2104
			if($this->isPublished() && $this->canEdit())	{
2105
				// "rollback"
2106
				$moreOptions->push(
2107
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2108
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2109
				);
2110
			}
2111
		}
2112
2113
		if($this->canEdit()) {
2114
			if($this->getIsDeletedFromStage()) {
2115
				// The usual major actions are not available, so we provide alternatives here.
2116
				if($existsOnLive) {
2117
					// "restore"
2118
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2119
					if($this->canDelete() && $this->canUnpublish()) {
2120
						// "delete from live"
2121
						$majorActions->push(
2122
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2123
								->addExtraClass('ss-ui-action-destructive')
2124
						);
2125
					}
2126
				} else {
2127
					// Determine if we should force a restore to root (where once it was a subpage)
2128
					$restoreToRoot = $this->isParentArchived();
2129
2130
					// "restore"
2131
					$title = $restoreToRoot
2132
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2133
						: _t('CMSMain.RESTORE','Restore draft');
2134
					$description = $restoreToRoot
2135
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2136
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2137
					$majorActions->push(
2138
						FormAction::create('restore', $title)
2139
							->setDescription($description)
2140
							->setAttribute('data-to-root', $restoreToRoot)
2141
							->setAttribute('data-icon', 'decline')
2142
					);
2143
				}
2144
			} else {
2145
					if($this->canDelete()) {
2146
						// delete
2147
						$moreOptions->push(
2148
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2149
								->addExtraClass('delete ss-ui-action-destructive')
2150
						);
2151
					}
2152
				if($this->canArchive()) {
2153
					// "archive"
2154
					$moreOptions->push(
2155
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2156
							->setDescription(_t(
2157
								'SiteTree.BUTTONARCHIVEDESC',
2158
								'Unpublish and send to archive'
2159
							))
2160
							->addExtraClass('delete ss-ui-action-destructive')
2161
					);
2162
				}
2163
2164
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2165
				$majorActions->push(
2166
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2167
						->setAttribute('data-icon', 'accept')
2168
						->setAttribute('data-icon-alternate', 'addpage')
2169
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2170
				);
2171
			}
2172
		}
2173
2174
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
2175
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2176
			$majorActions->push(
2177
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2178
					->setAttribute('data-icon', 'accept')
2179
					->setAttribute('data-icon-alternate', 'disk')
2180
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2181
			);
2182
2183
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2184
			if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE)) {
2185
				$publish->addExtraClass('ss-ui-alternate');
2186
			}
2187
		}
2188
2189
		$actions = new FieldList(array($majorActions, $rootTabSet));
2190
2191
		// Hook for extensions to add/remove actions.
2192
		$this->extend('updateCMSActions', $actions);
2193
2194
		return $actions;
2195
	}
2196
2197
	public function onAfterPublish() {
2198
		// Force live sort order to match stage sort order
2199
		DB::prepared_query('UPDATE "SiteTree_Live"
2200
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2201
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2202
			array($this->ParentID)
2203
		);
2204
		}
2205
2206
	/**
2207
	 * Update draft dependant pages
2208
	 */
2209
	public function onAfterRevertToLive() {
2210
		// Use an alias to get the updates made by $this->publish
2211
		/** @var SiteTree $stageSelf */
2212
		$stageSelf = Versioned::get_by_stage('SiteTree', Versioned::DRAFT)->byID($this->ID);
2213
		$stageSelf->writeWithoutVersion();
2214
2215
		// Need to update pages linking to this one as no longer broken
2216
		foreach($stageSelf->DependentPages() as $page) {
2217
			/** @var SiteTree $page */
2218
			$page->writeWithoutVersion();
2219
		}
2220
	}
2221
2222
	/**
2223
	 * Determine if this page references a parent which is archived, and not available in stage
2224
	 *
2225
	 * @return bool True if there is an archived parent
2226
	 */
2227
	protected function isParentArchived() {
2228
		if($parentID = $this->ParentID) {
2229
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2230
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2231
				return true;
2232
			}
2233
		}
2234
		return false;
2235
	}
2236
2237
	/**
2238
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2239
	 *
2240
	 * @return self
2241
	 */
2242
	public function doRestoreToStage() {
2243
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2244
2245
		// Ensure that the parent page is restored, otherwise restore to root
2246
		if($this->isParentArchived()) {
2247
			$this->ParentID = 0;
2248
		}
2249
2250
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2251
		// create an empty record
2252
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2253
			$conn = DB::get_conn();
2254
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
2255
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2256
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
2257
		}
2258
2259
		$oldStage = Versioned::get_stage();
2260
		Versioned::set_stage(Versioned::DRAFT);
2261
		$this->forceChange();
2262
		$this->write();
2263
2264
		$result = DataObject::get_by_id($this->class, $this->ID);
2265
2266
		// Need to update pages linking to this one as no longer broken
2267
		foreach($result->DependentPages(false) as $page) {
2268
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2269
			$page->write();
2270
		}
2271
2272
		Versioned::set_stage($oldStage);
2273
2274
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2275
2276
		return $result;
2277
	}
2278
2279
	/**
2280
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2281
	 *
2282
	 * @return bool
2283
	 */
2284
	public function isNew() {
2285
		/**
2286
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2287
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2288
		 */
2289
		if(empty($this->ID)) return true;
2290
2291
		if(is_numeric($this->ID)) return false;
2292
2293
		return stripos($this->ID, 'new') === 0;
2294
	}
2295
2296
	/**
2297
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2298
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2299
	 * {@link SiteTree::$needs_permission}.
2300
	 *
2301
	 * @return array
2302
	 */
2303
	protected function getClassDropdown() {
2304
		$classes = self::page_type_classes();
2305
		$currentClass = null;
2306
		$result = array();
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
2307
2308
		$result = array();
2309
		foreach($classes as $class) {
2310
			$instance = singleton($class);
2311
2312
			// if the current page type is this the same as the class type always show the page type in the list
2313
			if ($this->ClassName != $instance->ClassName) {
2314
				if($instance instanceof HiddenClass) continue;
0 ignored issues
show
Bug introduced by
The class HiddenClass does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2315
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
2316
			}
2317
2318
			if($perms = $instance->stat('need_permission')) {
2319
				if(!$this->can($perms)) continue;
2320
			}
2321
2322
			$pageTypeName = $instance->i18n_singular_name();
2323
2324
			$currentClass = $class;
2325
			$result[$class] = $pageTypeName;
2326
2327
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2328
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2329
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2330
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2331
				$result[$class] = $result[$class] .  " ({$class})";
2332
			}
2333
		}
2334
2335
		// sort alphabetically, and put current on top
2336
		asort($result);
2337
		if($currentClass) {
2338
			$currentPageTypeName = $result[$currentClass];
2339
			unset($result[$currentClass]);
2340
			$result = array_reverse($result);
2341
			$result[$currentClass] = $currentPageTypeName;
2342
			$result = array_reverse($result);
2343
		}
2344
2345
		return $result;
2346
	}
2347
2348
	/**
2349
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2350
	 *
2351
	 * @return string[]
2352
	 */
2353
	public function allowedChildren() {
2354
		$allowedChildren = array();
2355
		$candidates = $this->stat('allowed_children');
2356
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2357
			foreach($candidates as $candidate) {
2358
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2359
				// Otherwise, the class and all its subclasses are allowed.
2360
				if(substr($candidate,0,1) == '*') {
2361
					$allowedChildren[] = substr($candidate,1);
2362
				} else {
2363
					$subclasses = ClassInfo::subclassesFor($candidate);
2364
					foreach($subclasses as $subclass) {
2365
						if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
2366
					}
2367
				}
2368
			}
2369
		}
2370
2371
		return $allowedChildren;
2372
	}
2373
2374
	/**
2375
	 * Returns the class name of the default class for children of this page.
2376
	 *
2377
	 * @return string
2378
	 */
2379
	public function defaultChild() {
2380
		$default = $this->stat('default_child');
2381
		$allowed = $this->allowedChildren();
2382
		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...
2383
			if(!$default || !in_array($default, $allowed))
2384
				$default = reset($allowed);
2385
			return $default;
2386
		}
2387
	}
2388
2389
	/**
2390
	 * Returns the class name of the default class for the parent of this page.
2391
	 *
2392
	 * @return string
2393
	 */
2394
	public function defaultParent() {
2395
		return $this->stat('default_parent');
2396
	}
2397
2398
	/**
2399
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2400
	 * Title field.
2401
	 *
2402
	 * @return string
2403
	 */
2404
	public function getMenuTitle(){
2405
		if($value = $this->getField("MenuTitle")) {
2406
			return $value;
2407
		} else {
2408
			return $this->getField("Title");
2409
		}
2410
	}
2411
2412
2413
	/**
2414
	 * Set the menu title for this page.
2415
	 *
2416
	 * @param string $value
2417
	 */
2418
	public function setMenuTitle($value) {
2419
		if($value == $this->getField("Title")) {
2420
			$this->setField("MenuTitle", null);
2421
		} else {
2422
			$this->setField("MenuTitle", $value);
2423
		}
2424
	}
2425
2426
	/**
2427
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2428
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2429
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2430
	 * the flags.
2431
	 *
2432
	 * Example (simple):
2433
	 *   "deletedonlive" => "Deleted"
2434
	 *
2435
	 * Example (with optional title attribute):
2436
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2437
	 *
2438
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2439
	 * @return array
2440
	 */
2441
	public function getStatusFlags($cached = true) {
2442
		if(!$this->_cache_statusFlags || !$cached) {
2443
			$flags = array();
2444
			if($this->getIsDeletedFromStage()) {
2445
				if($this->isPublished()) {
2446
					$flags['removedfromdraft'] = array(
2447
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2448
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2449
					);
2450
				} else {
2451
					$flags['archived'] = array(
2452
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2453
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2454
					);
2455
				}
2456
			} else if($this->getIsAddedToStage()) {
2457
				$flags['addedtodraft'] = array(
2458
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2459
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2460
				);
2461
			} else if($this->getIsModifiedOnStage()) {
2462
				$flags['modified'] = array(
2463
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2464
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2465
				);
2466
			}
2467
2468
			$this->extend('updateStatusFlags', $flags);
2469
2470
			$this->_cache_statusFlags = $flags;
2471
		}
2472
2473
		return $this->_cache_statusFlags;
2474
	}
2475
2476
	/**
2477
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2478
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2479
	 * publication status.
2480
	 *
2481
	 * @return string An HTML string ready to be directly used in a template
2482
	 */
2483
	public function getTreeTitle() {
2484
		// Build the list of candidate children
2485
		$children = array();
2486
		$candidates = static::page_type_classes();
2487
		foreach($this->allowedChildren() as $childClass) {
2488
			if(!in_array($childClass, $candidates)) continue;
2489
			$child = singleton($childClass);
2490
			if($child->canCreate(null, array('Parent' => $this))) {
2491
				$children[$childClass] = $child->i18n_singular_name();
2492
			}
2493
		}
2494
		$flags = $this->getStatusFlags();
2495
		$treeTitle = sprintf(
2496
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2497
			Convert::raw2att(Convert::raw2json($children)),
2498
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2499
		);
2500
		foreach($flags as $class => $data) {
2501
			if(is_string($data)) $data = array('text' => $data);
2502
			$treeTitle .= sprintf(
2503
				"<span class=\"badge %s\"%s>%s</span>",
2504
				'status-' . Convert::raw2xml($class),
2505
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2506
				Convert::raw2xml($data['text'])
2507
			);
2508
		}
2509
2510
		return $treeTitle;
2511
	}
2512
2513
	/**
2514
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2515
	 * we're currently inside, etc.
2516
	 *
2517
	 * @param int $level
2518
	 * @return SiteTree
2519
	 */
2520
	public function Level($level) {
2521
		$parent = $this;
2522
		$stack = array($parent);
2523
		while($parent = $parent->Parent) {
2524
			array_unshift($stack, $parent);
2525
		}
2526
2527
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2528
	}
2529
2530
	/**
2531
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2532
	 *
2533
	 * @return int
2534
	 */
2535
	public function getPageLevel() {
2536
		if($this->ParentID) {
2537
			return 1 + $this->Parent()->getPageLevel();
2538
		}
2539
		return 1;
2540
	}
2541
2542
	/**
2543
	 * Return the CSS classes to apply to this node in the CMS tree.
2544
	 *
2545
	 * @param string $numChildrenMethod
2546
	 * @return string
2547
	 */
2548
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2549
		$classes = sprintf('class-%s', $this->class);
2550
		if($this->HasBrokenFile || $this->HasBrokenLink) {
2551
			$classes .= " BrokenLink";
2552
		}
2553
2554
		if(!$this->canAddChildren()) {
2555
			$classes .= " nochildren";
2556
		}
2557
2558
		if(!$this->canEdit() && !$this->canAddChildren()) {
2559
			if (!$this->canView()) {
2560
				$classes .= " disabled";
2561
			} else {
2562
				$classes .= " edit-disabled";
2563
			}
2564
		}
2565
2566
		if(!$this->ShowInMenus) {
2567
			$classes .= " notinmenu";
2568
		}
2569
2570
		//TODO: Add integration
2571
		/*
2572
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2573
			$classes .= " untranslated ";
2574
		*/
2575
		$classes .= $this->markingClasses($numChildrenMethod);
2576
2577
		return $classes;
2578
	}
2579
2580
	/**
2581
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2582
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2583
	 *
2584
	 * @return bool
2585
	 */
2586
	public function getIsDeletedFromStage() {
2587
		if(!$this->ID) return true;
2588
		if($this->isNew()) return false;
2589
2590
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', Versioned::DRAFT, $this->ID);
2591
2592
		// Return true for both completely deleted pages and for pages just deleted from stage
2593
		return !($stageVersion);
2594
	}
2595
2596
	/**
2597
	 * Return true if this page exists on the live site
2598
	 *
2599
	 * @return bool
2600
	 */
2601
	public function getExistsOnLive() {
2602
		return $this->isPublished();
2603
	}
2604
2605
	/**
2606
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2607
	 * unpublished changes to the draft site.
2608
	 *
2609
	 * @return bool
2610
	 */
2611
	public function getIsModifiedOnStage() {
2612
		// New unsaved pages could be never be published
2613
		if($this->isNew()) return false;
2614
2615
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2616
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2617
2618
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2619
		$this->extend('getIsModifiedOnStage', $isModified);
2620
2621
		return $isModified;
2622
	}
2623
2624
	/**
2625
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2626
	 * published.
2627
	 *
2628
	 * @return bool
2629
	 */
2630
	public function getIsAddedToStage() {
2631
		// New unsaved pages could be never be published
2632
		if($this->isNew()) return false;
2633
2634
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2635
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2636
2637
		return ($stageVersion && !$liveVersion);
2638
	}
2639
2640
	/**
2641
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2642
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2643
	 */
2644
	static public function disableCMSFieldsExtensions() {
2645
		self::$runCMSFieldsExtensions = false;
2646
	}
2647
2648
	/**
2649
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2650
	 * disableCMSFieldsExtensions().
2651
	 */
2652
	static public function enableCMSFieldsExtensions() {
2653
		self::$runCMSFieldsExtensions = true;
2654
	}
2655
2656
	public function providePermissions() {
2657
		return array(
2658
			'SITETREE_GRANT_ACCESS' => array(
2659
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2660
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2661
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2662
				'sort' => 100
2663
			),
2664
			'SITETREE_VIEW_ALL' => array(
2665
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2666
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2667
				'sort' => -100,
2668
				'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')
2669
			),
2670
			'SITETREE_EDIT_ALL' => array(
2671
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2672
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2673
				'sort' => -50,
2674
				'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')
2675
			),
2676
			'SITETREE_REORGANISE' => array(
2677
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2678
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2679
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2680
				'sort' => 100
2681
			),
2682
			'VIEW_DRAFT_CONTENT' => array(
2683
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2684
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2685
				'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.'),
2686
				'sort' => 100
2687
			)
2688
		);
2689
	}
2690
2691
	/**
2692
	 * Return the translated Singular name.
2693
	 *
2694
	 * @return string
2695
	 */
2696
	public function i18n_singular_name() {
2697
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2698
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2699
		return _t($class.'.SINGULARNAME', $this->singular_name());
2700
	}
2701
2702
	/**
2703
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2704
	 * picks it up for the wrong folder.
2705
	 *
2706
	 * @return array
2707
	 */
2708
	public function provideI18nEntities() {
2709
		$entities = parent::provideI18nEntities();
2710
2711
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2712
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2713
2714
		$entities[$this->class . '.DESCRIPTION'] = array(
2715
			$this->stat('description'),
2716
			'Description of the page type (shown in the "add page" dialog)'
2717
		);
2718
2719
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2720
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2721
2722
		return $entities;
2723
	}
2724
2725
	/**
2726
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2727
	 *
2728
	 * @return string
2729
	 */
2730
	public function getParentType() {
2731
		return $this->ParentID == 0 ? 'root' : 'subpage';
2732
	}
2733
2734
	/**
2735
	 * Clear the permissions cache for SiteTree
2736
	 */
2737
	public static function reset() {
2738
		self::$cache_permissions = array();
2739
	}
2740
2741
	static public function on_db_reset() {
2742
		self::$cache_permissions = array();
2743
	}
2744
2745
}
2746