Completed
Push — master ( 1ae58b...75d722 )
by Daniel
04:28 queued 01:15
created

SiteTree   F

Complexity

Total Complexity 435

Size/Duplication

Total Lines 2773
Duplicated Lines 0.87 %

Coupling/Cohesion

Components 1
Dependencies 34

Importance

Changes 18
Bugs 4 Features 1
Metric Value
wmc 435
c 18
b 4
f 1
lcom 1
cbo 34
dl 24
loc 2773
rs 0.5217

86 Methods

Rating   Name   Duplication   Size   Complexity  
C page_type_classes() 0 34 7
A Link() 0 3 1
A AbsoluteLink() 0 7 2
A PreviewLink() 0 10 2
A getMimeType() 0 3 1
C get_by_link() 6 66 20
B link_shortcode_handler() 0 18 6
D RelativeLink() 0 25 9
A getAbsoluteLiveLink() 0 18 3
A CMSEditLink() 0 7 1
A ElementName() 0 3 1
A isCurrent() 0 3 2
A isSection() 0 5 3
A isOrphaned() 0 8 4
A LinkOrCurrent() 0 3 2
A LinkOrSection() 0 3 2
A LinkingMode() 0 9 3
A InSection() 0 9 3
A onBeforeDuplicate() 0 3 1
A duplicateWithChildren() 0 16 4
A duplicateAsChild() 0 6 1
A Breadcrumbs() 0 8 1
B getBreadcrumbItems() 0 18 9
A setParent() 0 8 3
A getParent() 0 5 2
A NestedTitle() 0 10 3
B can() 3 17 12
C canAddChildren() 3 23 9
C canView() 3 49 19
A canPublish() 0 18 4
C canDelete() 3 23 7
C canCreate() 3 32 11
C canEdit() 3 29 8
A getSiteConfig() 0 8 2
A prepopulate_permission_cache() 0 10 3
D batch_permission_check() 0 123 23
A can_edit_multiple() 0 3 1
B can_delete_multiple() 0 57 8
A collateDescendants() 0 13 4
C MetaTags() 0 50 10
A ContentSource() 0 3 1
B requireDefaultRecords() 0 38 5
C onBeforeWrite() 0 50 11
A syncLinkTracking() 0 3 1
A onBeforeDelete() 0 10 4
A onAfterDelete() 0 13 3
A flushCache() 0 4 1
C validate() 0 37 8
D validURLSegment() 0 34 15
A generateURLSegment() 0 12 4
A getStageURLSegment() 0 6 2
A getLiveURLSegment() 0 6 2
C DependentPages() 0 50 10
A VirtualPages() 0 10 2
F getCMSFields() 0 180 22
C getSettingsFields() 0 106 8
B fieldLabels() 0 38 3
D getCMSActions() 0 148 23
A onAfterPublish() 0 8 1
A onAfterRevertToLive() 0 12 2
A isParentArchived() 0 9 4
B doRestoreToStage() 0 36 6
A isNew() 0 11 3
D getClassDropdown() 0 44 10
C allowedChildren() 0 23 9
A defaultChild() 0 9 4
A defaultParent() 0 3 1
A getMenuTitle() 0 7 2
A setMenuTitle() 0 7 2
C getStatusFlags() 0 34 7
C getTreeTitle() 0 29 7
A Level() 0 9 3
A getPageLevel() 0 6 2
C CMSTreeClasses() 0 31 8
A getIsDeletedFromStage() 0 9 3
A getExistsOnLive() 0 3 1
A getIsModifiedOnStage() 0 12 3
A getIsAddedToStage() 0 9 3
A disableCMSFieldsExtensions() 0 3 1
A enableCMSFieldsExtensions() 0 3 1
B providePermissions() 0 34 1
A i18n_singular_name() 0 5 2
A provideI18nEntities() 0 16 3
A getParentType() 0 3 2
A reset() 0 3 1
A on_db_reset() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SiteTree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SiteTree, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use SilverStripe\ORM\DataObject;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DataObject.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
4
use SilverStripe\ORM\Hierarchy\Hierarchy;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Hierarchy.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
5
use SilverStripe\ORM\ManyManyList;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ManyManyList.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
6
use SilverStripe\ORM\Versioning\Versioned;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Versioned.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
use SilverStripe\ORM\ArrayList;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ArrayList.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
use SilverStripe\ORM\DB;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DB.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
9
use SilverStripe\ORM\DataList;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DataList.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use SilverStripe\ORM\HiddenClass;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, HiddenClass.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
11
use SilverStripe\Security\Member;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Member.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
12
use SilverStripe\Security\Permission;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Permission.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
use SilverStripe\Security\Group;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Group.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
14
use SilverStripe\Security\PermissionProvider;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PermissionProvider.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
16
17
/**
18
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
19
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
20
 * draft and published states.
21
 *
22
 * <h2>URLs</h2>
23
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
24
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
25
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
26
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
27
 * {@link URLSegmentFilter::$default_allow_multibyte}.
28
 *
29
 * @property string URLSegment
30
 * @property string Title
31
 * @property string MenuTitle
32
 * @property string Content HTML content of the page.
33
 * @property string MetaDescription
34
 * @property string ExtraMeta
35
 * @property string ShowInMenus
36
 * @property string ShowInSearch
37
 * @property string Sort Integer value denoting the sort order.
38
 * @property string ReportClass
39
 * @property string CanViewType Type of restriction for viewing this object.
40
 * @property string CanEditType Type of restriction for editing this object.
41
 *
42
 * @method ManyManyList ViewerGroups List of groups that can view this object.
43
 * @method ManyManyList EditorGroups List of groups that can edit this object.
44
 *
45
 * @mixin Hierarchy
46
 * @mixin Versioned
47
 * @mixin SiteTreeLinkTracking
48
 *
49
 * @package cms
50
 */
51
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {
52
53
	/**
54
	 * Indicates what kind of children this page type can have.
55
	 * This can be an array of allowed child classes, or the string "none" -
56
	 * indicating that this page type can't have children.
57
	 * If a classname is prefixed by "*", such as "*Page", then only that
58
	 * class is allowed - no subclasses. Otherwise, the class and all its
59
	 * subclasses are allowed.
60
	 * To control allowed children on root level (no parent), use {@link $can_be_root}.
61
	 *
62
	 * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
63
	 *
64
	 * @config
65
	 * @var array
66
	 */
67
	private static $allowed_children = array("SiteTree");
68
69
	/**
70
	 * The default child class for this page.
71
	 * Note: Value might be cached, see {@link $allowed_chilren}.
72
	 *
73
	 * @config
74
	 * @var string
75
	 */
76
	private static $default_child = "Page";
77
78
	/**
79
	 * Default value for SiteTree.ClassName enum
80
	 * {@see DBClassName::getDefault}
81
	 *
82
	 * @config
83
	 * @var string
84
	 */
85
	private static $default_classname = "Page";
86
87
	/**
88
	 * The default parent class for this page.
89
	 * Note: Value might be cached, see {@link $allowed_chilren}.
90
	 *
91
	 * @config
92
	 * @var string
93
	 */
94
	private static $default_parent = null;
95
96
	/**
97
	 * Controls whether a page can be in the root of the site tree.
98
	 * Note: Value might be cached, see {@link $allowed_chilren}.
99
	 *
100
	 * @config
101
	 * @var bool
102
	 */
103
	private static $can_be_root = true;
104
105
	/**
106
	 * List of permission codes a user can have to allow a user to create a page of this type.
107
	 * Note: Value might be cached, see {@link $allowed_chilren}.
108
	 *
109
	 * @config
110
	 * @var array
111
	 */
112
	private static $need_permission = null;
113
114
	/**
115
	 * If you extend a class, and don't want to be able to select the old class
116
	 * in the cms, set this to the old class name. Eg, if you extended Product
117
	 * to make ImprovedProduct, then you would set $hide_ancestor to Product.
118
	 *
119
	 * @config
120
	 * @var string
121
	 */
122
	private static $hide_ancestor = null;
123
124
	private static $db = array(
125
		"URLSegment" => "Varchar(255)",
126
		"Title" => "Varchar(255)",
127
		"MenuTitle" => "Varchar(100)",
128
		"Content" => "HTMLText",
129
		"MetaDescription" => "Text",
130
		"ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
131
		"ShowInMenus" => "Boolean",
132
		"ShowInSearch" => "Boolean",
133
		"Sort" => "Int",
134
		"HasBrokenFile" => "Boolean",
135
		"HasBrokenLink" => "Boolean",
136
		"ReportClass" => "Varchar",
137
		"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
138
		"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
139
	);
140
141
	private static $indexes = array(
142
		"URLSegment" => true,
143
	);
144
145
	private static $many_many = array(
146
		"ViewerGroups" => "SilverStripe\\Security\\Group",
147
		"EditorGroups" => "SilverStripe\\Security\\Group",
148
	);
149
150
	private static $has_many = array(
151
		"VirtualPages" => "VirtualPage.CopyContentFrom"
152
	);
153
154
	private static $owned_by = array(
155
		"VirtualPages"
156
	);
157
158
	private static $casting = array(
159
		"Breadcrumbs" => "HTMLFragment",
160
		"LastEdited" => "Datetime",
161
		"Created" => "Datetime",
162
		'Link' => 'Text',
163
		'RelativeLink' => 'Text',
164
		'AbsoluteLink' => 'Text',
165
		'CMSEditLink' => 'Text',
166
		'TreeTitle' => 'HTMLFragment',
167
		'MetaTags' => 'HTMLFragment',
168
	);
169
170
	private static $defaults = array(
171
		"ShowInMenus" => 1,
172
		"ShowInSearch" => 1,
173
		"CanViewType" => "Inherit",
174
		"CanEditType" => "Inherit"
175
	);
176
177
	private static $versioning = array(
178
		"Stage",  "Live"
179
	);
180
181
	private static $default_sort = "\"Sort\"";
182
183
	/**
184
	 * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
185
	 * @var boolean
186
	 * @config
187
	 */
188
	private static $can_create = true;
189
190
	/**
191
	 * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
192
	 * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
193
	 *
194
	 * @see CMSMain::generateTreeStylingCSS()
195
	 * @config
196
	 * @var string
197
	 */
198
	private static $icon = null;
199
200
	/**
201
	 * @config
202
	 * @var string Description of the class functionality, typically shown to a user
203
	 * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
204
	 */
205
	private static $description = 'Generic content page';
206
207
	private static $extensions = array(
208
		'SilverStripe\ORM\Hierarchy\Hierarchy',
209
		'SilverStripe\ORM\Versioning\Versioned',
210
		"SiteTreeLinkTracking"
211
	);
212
213
	private static $searchable_fields = array(
214
		'Title',
215
		'Content',
216
	);
217
218
	private static $field_labels = array(
219
		'URLSegment' => 'URL'
220
	);
221
222
	/**
223
	 * @config
224
	 */
225
	private static $nested_urls = true;
226
227
	/**
228
	 * @config
229
	*/
230
	private static $create_default_pages = true;
231
232
	/**
233
	 * This controls whether of not extendCMSFields() is called by getCMSFields.
234
	 */
235
	private static $runCMSFieldsExtensions = true;
236
237
	/**
238
	 * Cache for canView/Edit/Publish/Delete permissions.
239
	 * Keyed by permission type (e.g. 'edit'), with an array
240
	 * of IDs mapped to their boolean permission ability (true=allow, false=deny).
241
	 * See {@link batch_permission_check()} for details.
242
	 */
243
	private static $cache_permissions = array();
244
245
	/**
246
	 * @config
247
	 * @var boolean
248
	 */
249
	private static $enforce_strict_hierarchy = true;
250
251
	/**
252
	 * The value used for the meta generator tag. Leave blank to omit the tag.
253
	 *
254
	 * @config
255
	 * @var string
256
	 */
257
	private static $meta_generator = 'SilverStripe - http://silverstripe.org';
258
259
	protected $_cache_statusFlags = null;
260
261
	/**
262
	 * Fetches the {@link SiteTree} object that maps to a link.
263
	 *
264
	 * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
265
	 * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
266
	 *
267
	 * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
268
	 * by a extension attached to {@link SiteTree}
269
	 *
270
	 * @param string $link  The link of the page to search for
271
	 * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
272
	 * @return SiteTree
273
	 */
274
	static public function get_by_link($link, $cache = true) {
275
		if(trim($link, '/')) {
276
			$link = trim(Director::makeRelative($link), '/');
277
		} else {
278
			$link = RootURLController::get_homepage_link();
279
		}
280
281
		$parts = preg_split('|/+|', $link);
282
283
		// Grab the initial root level page to traverse down from.
284
		$URLSegment = array_shift($parts);
285
		$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
286
		if(self::config()->nested_urls) {
287
			$conditions[] = array('"SiteTree"."ParentID"' => 0);
288
		}
289
		$sitetree = DataObject::get_one('SiteTree', $conditions, $cache);
290
291
		/// Fall back on a unique URLSegment for b/c.
292
		if(	!$sitetree
293
			&& self::config()->nested_urls
294
			&& $page = DataObject::get_one('SiteTree', array(
295
				'"SiteTree"."URLSegment"' => $URLSegment
296
			), $cache)
297
		) {
298
			return $page;
299
		}
300
301
		// Attempt to grab an alternative page from extensions.
302
		if(!$sitetree) {
303
			$parentID = self::config()->nested_urls ? 0 : null;
304
305 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...
306
				foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
307
			}
308
309
			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...
310
		}
311
312
		// Check if we have any more URL parts to parse.
313
		if(!self::config()->nested_urls || !count($parts)) return $sitetree;
314
315
		// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
316
		foreach($parts as $segment) {
317
			$next = DataObject::get_one('SiteTree', array(
318
					'"SiteTree"."URLSegment"' => $segment,
319
					'"SiteTree"."ParentID"' => $sitetree->ID
320
				),
321
				$cache
322
			);
323
324
			if(!$next) {
325
				$parentID = (int) $sitetree->ID;
326
327 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...
328
					foreach($alternatives as $alternative) if($alternative) $next = $alternative;
329
				}
330
331
				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...
332
			}
333
334
			$sitetree->destroy();
335
			$sitetree = $next;
336
		}
337
338
		return $sitetree;
339
	}
340
341
	/**
342
	 * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
343
	 *
344
	 * @return array
345
	 */
346
	public static function page_type_classes() {
347
		$classes = ClassInfo::getValidSubClasses();
348
349
		$baseClassIndex = array_search('SiteTree', $classes);
350
		if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
351
352
		$kill_ancestors = array();
353
354
		// figure out if there are any classes we don't want to appear
355
		foreach($classes as $class) {
356
			$instance = singleton($class);
357
358
			// do any of the progeny want to hide an ancestor?
359
			if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
360
				// note for killing later
361
				$kill_ancestors[] = $ancestor_to_hide;
362
			}
363
		}
364
365
		// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
366
		// requirements
367
		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...
368
			$kill_ancestors = array_unique($kill_ancestors);
369
			foreach($kill_ancestors as $mark) {
370
				// unset from $classes
371
				$idx = array_search($mark, $classes, true);
372
				if ($idx !== false) {
373
					unset($classes[$idx]);
374
				}
375
			}
376
		}
377
378
		return $classes;
379
	}
380
381
	/**
382
	 * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
383
	 *
384
	 * @param array      $arguments
385
	 * @param string     $content
386
	 * @param TextParser $parser
387
	 * @return string
388
	 */
389
	static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
390
		if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
391
392
		if (
393
			   !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
394
			&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
395
		) {
396
			 return null; // There were no suitable matches at all.
397
		}
398
399
		$link = Convert::raw2att($page->Link());
400
401
		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...
402
			return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
0 ignored issues
show
Unused Code introduced by
The call to TextParser::parse() has too many arguments starting with $content.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Bug introduced by
It seems like $parser is not always an object, but can also be of type null. Maybe add an additional type check?

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

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
403
		} else {
404
			return $link;
405
		}
406
	}
407
408
	/**
409
	 * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
410
	 *
411
	 * @param string $action Optional controller action (method).
412
	 *                       Note: URI encoding of this parameter is applied automatically through template casting,
413
	 *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
414
	 *                       append GET parameters.
415
	 * @return string
416
	 */
417
	public function Link($action = null) {
418
		return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
419
	}
420
421
	/**
422
	 * Get the absolute URL for this page, including protocol and host.
423
	 *
424
	 * @param string $action See {@link Link()}
425
	 * @return string
426
	 */
427
	public function AbsoluteLink($action = null) {
428
		if($this->hasMethod('alternateAbsoluteLink')) {
429
			return $this->alternateAbsoluteLink($action);
430
		} else {
431
			return Director::absoluteURL($this->Link($action));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($this->Link($action)); of type string|false adds false to the return on line 431 which is incompatible with the return type documented by SiteTree::AbsoluteLink of type string. It seems like you forgot to handle an error condition.
Loading history...
432
		}
433
	}
434
435
	/**
436
	 * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
437
	 * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
438
	 *
439
	 * @param string $action See {@link Link()}
440
	 * @return string
441
	 */
442
	public function PreviewLink($action = null) {
443
		if($this->hasMethod('alternatePreviewLink')) {
444
			Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
445
			return $this->alternatePreviewLink($action);
446
		}
447
448
		$link = $this->AbsoluteLink($action);
449
		$this->extend('updatePreviewLink', $link, $action);
450
		return $link;
451
	}
452
453
	public function getMimeType() {
454
		return 'text/html';
455
	}
456
457
	/**
458
	 * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
459
	 *
460
	 * By default, if this page is the current home page, and there is no action specified then this will return a link
461
	 * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
462
	 * and returned in its full form.
463
	 *
464
	 * @uses RootURLController::get_homepage_link()
465
	 *
466
	 * @param string $action See {@link Link()}
467
	 * @return string
468
	 */
469
	public function RelativeLink($action = null) {
470
		if($this->ParentID && self::config()->nested_urls) {
471
			$parent = $this->Parent();
472
			// If page is removed select parent from version history (for archive page view)
473
			if((!$parent || !$parent->exists()) && $this->IsDeletedFromStage) {
474
				$parent = Versioned::get_latest_version('SiteTree', $this->ParentID);
475
			}
476
			$base = $parent->RelativeLink($this->URLSegment);
477
		} 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...
478
			// Unset base for root-level homepages.
479
			// Note: Homepages with action parameters (or $action === true)
480
			// need to retain their URLSegment.
481
			$base = null;
482
		} else {
483
			$base = $this->URLSegment;
484
		}
485
486
		$this->extend('updateRelativeLink', $base, $action);
487
488
		// Legacy support: If $action === true, retain URLSegment for homepages,
489
		// but don't append any action
490
		if($action === true) $action = null;
491
492
		return Controller::join_links($base, '/', $action);
493
	}
494
495
	/**
496
	 * Get the absolute URL for this page on the Live site.
497
	 *
498
	 * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
499
	 * @return string
500
	 */
501
	public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
502
		$oldReadingMode = Versioned::get_reading_mode();
503
		Versioned::set_stage(Versioned::LIVE);
504
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
505
			'"SiteTree"."ID"' => $this->ID
506
		));
507
		if($live) {
508
			$link = $live->AbsoluteLink();
509
			if($includeStageEqualsLive) {
510
				$link = Controller::join_links($link, '?stage=Live');
511
			}
512
		} else {
513
			$link = null;
514
		}
515
516
		Versioned::set_reading_mode($oldReadingMode);
517
		return $link;
518
	}
519
520
	/**
521
	 * Generates a link to edit this page in the CMS.
522
	 *
523
	 * @return string
524
	 */
525
	public function CMSEditLink() {
526
		$link = Controller::join_links(
527
			singleton('CMSPageEditController')->Link('show'),
528
			$this->ID
529
		);
530
		return Director::absoluteURL($link);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \Director::absoluteURL($link); of type string|false adds false to the return on line 530 which is incompatible with the return type declared by the interface CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
531
	}
532
533
534
	/**
535
	 * Return a CSS identifier generated from this page's link.
536
	 *
537
	 * @return string The URL segment
538
	 */
539
	public function ElementName() {
540
		return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
541
	}
542
543
	/**
544
	 * Returns true if this is the currently active page being used to handle this request.
545
	 *
546
	 * @return bool
547
	 */
548
	public function isCurrent() {
549
		return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
550
	}
551
552
	/**
553
	 * Check if this page is in the currently active section (e.g. it is either current or one of its children is
554
	 * currently being viewed).
555
	 *
556
	 * @return bool
557
	 */
558
	public function isSection() {
559
		return $this->isCurrent() || (
560
			Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
561
		);
562
	}
563
564
	/**
565
	 * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
566
	 * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
567
	 * to external users.
568
	 *
569
	 * @return bool
570
	 */
571
	public function isOrphaned() {
572
		// Always false for root pages
573
		if(empty($this->ParentID)) return false;
574
575
		// Parent must exist and not be an orphan itself
576
		$parent = $this->Parent();
577
		return !$parent || !$parent->exists() || $parent->isOrphaned();
578
	}
579
580
	/**
581
	 * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
582
	 *
583
	 * @return string
584
	 */
585
	public function LinkOrCurrent() {
586
		return $this->isCurrent() ? 'current' : 'link';
587
	}
588
589
	/**
590
	 * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
591
	 *
592
	 * @return string
593
	 */
594
	public function LinkOrSection() {
595
		return $this->isSection() ? 'section' : 'link';
596
	}
597
598
	/**
599
	 * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
600
	 * but in the current section.
601
	 *
602
	 * @return string
603
	 */
604
	public function LinkingMode() {
605
		if($this->isCurrent()) {
606
			return 'current';
607
		} elseif($this->isSection()) {
608
			return 'section';
609
		} else {
610
			return 'link';
611
		}
612
	}
613
614
	/**
615
	 * Check if this page is in the given current section.
616
	 *
617
	 * @param string $sectionName Name of the section to check
618
	 * @return bool True if we are in the given section
619
	 */
620
	public function InSection($sectionName) {
621
		$page = Director::get_current_page();
622
		while($page) {
623
			if($sectionName == $page->URLSegment)
624
				return true;
625
			$page = $page->Parent;
626
		}
627
		return false;
628
	}
629
630
	/**
631
	 * Reset Sort on duped page
632
	 *
633
	 * @param SiteTree $original
634
	 * @param bool $doWrite
635
	 */
636
	public function onBeforeDuplicate($original, $doWrite) {
637
		$this->Sort = 0;
638
	}
639
640
	/**
641
	 * Duplicates each child of this node recursively and returns the top-level duplicate node.
642
	 *
643
	 * @return self The duplicated object
644
	 */
645
	public function duplicateWithChildren() {
646
		$clone = $this->duplicate();
647
		$children = $this->AllChildren();
648
649
		if($children) {
650
			foreach($children as $child) {
651
				$childClone = method_exists($child, 'duplicateWithChildren')
652
					? $child->duplicateWithChildren()
653
					: $child->duplicate();
654
				$childClone->ParentID = $clone->ID;
655
				$childClone->write();
656
			}
657
		}
658
659
		return $clone;
660
	}
661
662
	/**
663
	 * Duplicate this node and its children as a child of the node with the given ID
664
	 *
665
	 * @param int $id ID of the new node's new parent
666
	 */
667
	public function duplicateAsChild($id) {
668
		$newSiteTree = $this->duplicate();
669
		$newSiteTree->ParentID = $id;
670
		$newSiteTree->Sort = 0;
671
		$newSiteTree->write();
672
	}
673
674
	/**
675
	 * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
676
	 *
677
	 * @param int $maxDepth The maximum depth to traverse.
678
	 * @param boolean $unlinked Whether to link page titles.
679
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
680
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
681
	 * @return string The breadcrumb trail.
682
	 */
683
	public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
684
		$pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
685
		$template = new SSViewer('BreadcrumbsTemplate');
686
		return $template->process($this->customise(new ArrayData(array(
687
			"Pages" => $pages,
688
			"Unlinked" => $unlinked
689
		))));
690
	}
691
692
693
	/**
694
	 * Returns a list of breadcrumbs for the current page.
695
	 *
696
	 * @param int $maxDepth The maximum depth to traverse.
697
	 * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
698
	 * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
699
	 *
700
	 * @return ArrayList
701
	*/
702
	public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false) {
703
		$page = $this;
704
		$pages = array();
705
706
		while(
707
			$page
708
 			&& (!$maxDepth || count($pages) < $maxDepth)
709
 			&& (!$stopAtPageType || $page->ClassName != $stopAtPageType)
710
 		) {
711
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
712
				$pages[] = $page;
713
			}
714
715
			$page = $page->Parent;
716
		}
717
718
		return new ArrayList(array_reverse($pages));
719
	}
720
721
722
	/**
723
	 * Make this page a child of another page.
724
	 *
725
	 * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
726
	 *
727
	 * @param SiteTree|int $item Either the parent object, or the parent ID
728
	 */
729
	public function setParent($item) {
730
		if(is_object($item)) {
731
			if (!$item->exists()) $item->write();
732
			$this->setField("ParentID", $item->ID);
733
		} else {
734
			$this->setField("ParentID", $item);
735
		}
736
	}
737
738
	/**
739
	 * Get the parent of this page.
740
	 *
741
	 * @return SiteTree Parent of this page
742
	 */
743
	public function getParent() {
744
		if ($parentID = $this->getField("ParentID")) {
745
			return DataObject::get_by_id("SiteTree", $parentID);
746
		}
747
	}
748
749
	/**
750
	 * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
751
	 *
752
	 * @param int $level The maximum amount of levels to traverse.
753
	 * @param string $separator Seperating string
754
	 * @return string The resulting string
755
	 */
756
	public function NestedTitle($level = 2, $separator = " - ") {
757
		$item = $this;
758
		$parts = [];
759
		while($item && $level > 0) {
760
			$parts[] = $item->Title;
761
			$item = $item->Parent;
762
			$level--;
763
		}
764
		return implode($separator, array_reverse($parts));
765
	}
766
767
	/**
768
	 * This function should return true if the current user can execute this action. It can be overloaded to customise
769
	 * the security model for an application.
770
	 *
771
	 * Slightly altered from parent behaviour in {@link DataObject->can()}:
772
	 * - Checks for existence of a method named "can<$perm>()" on the object
773
	 * - Calls decorators and only returns for FALSE "vetoes"
774
	 * - Falls back to {@link Permission::check()}
775
	 * - Does NOT check for many-many relations named "Can<$perm>"
776
	 *
777
	 * @uses DataObjectDecorator->can()
778
	 *
779
	 * @param string $perm The permission to be checked, such as 'View'
780
	 * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
781
	 * @param array $context Context argument for canCreate()
782
	 * @return bool True if the the member is allowed to do the given action
783
	 */
784
	public function can($perm, $member = null, $context = array()) {
785 View Code Duplication
		if(!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Security\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...
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...
786
			$member = Member::currentUserID();
787
		}
788
789
		if($member && Permission::checkMember($member, "ADMIN")) return true;
790
791
		if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
792
			$method = 'can' . ucfirst($perm);
793
			return $this->$method($member);
794
		}
795
796
		$results = $this->extend('can', $member);
797
		if($results && is_array($results)) if(!min($results)) return false;
798
799
		return ($member && Permission::checkMember($member, $perm));
800
	}
801
802
	/**
803
	 * This function should return true if the current user can add children to this page. It can be overloaded to
804
	 * customise the security model for an application.
805
	 *
806
	 * Denies permission if any of the following conditions is true:
807
	 * - alternateCanAddChildren() on a extension returns false
808
	 * - canEdit() is not granted
809
	 * - There are no classes defined in {@link $allowed_children}
810
	 *
811
	 * @uses SiteTreeExtension->canAddChildren()
812
	 * @uses canEdit()
813
	 * @uses $allowed_children
814
	 *
815
	 * @param Member|int $member
816
	 * @return bool True if the current user can add children
817
	 */
818
	public function canAddChildren($member = null) {
819
		// Disable adding children to archived pages
820
		if($this->getIsDeletedFromStage()) {
821
			return false;
822
		}
823
824 View Code Duplication
		if(!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Security\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...
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...
825
			$member = Member::currentUserID();
826
		}
827
828
		// Standard mechanism for accepting permission changes from extensions
829
		$extended = $this->extendedCan('canAddChildren', $member);
830
		if($extended !== null) {
831
			return $extended;
832
		}
833
834
		// Default permissions
835
		if($member && Permission::checkMember($member, "ADMIN")) {
836
			return true;
837
		}
838
839
		return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
840
	}
841
842
	/**
843
	 * This function should return true if the current user can view this page. It can be overloaded to customise the
844
	 * security model for an application.
845
	 *
846
	 * Denies permission if any of the following conditions is true:
847
	 * - canView() on any extension returns false
848
	 * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
849
	 * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
850
	 * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
851
	 *
852
	 * @uses DataExtension->canView()
853
	 * @uses ViewerGroups()
854
	 *
855
	 * @param Member|int $member
856
	 * @return bool True if the current user can view this page
857
	 */
858
	public function canView($member = null) {
859 View Code Duplication
		if(!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Security\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...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
860
			$member = Member::currentUserID();
861
		}
862
863
		// Standard mechanism for accepting permission changes from extensions
864
		$extended = $this->extendedCan('canView', $member);
865
		if($extended !== null) {
866
			return $extended;
867
		}
868
869
		// admin override
870
		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
871
			return true;
872
		}
873
874
		// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
875
		if($this->isOrphaned()) {
876
			return false;
877
		}
878
879
		// check for empty spec
880
		if(!$this->CanViewType || $this->CanViewType == 'Anyone') {
881
			return true;
882
		}
883
884
		// check for inherit
885
		if($this->CanViewType == 'Inherit') {
886
			if($this->ParentID) return $this->Parent()->canView($member);
887
			else return $this->getSiteConfig()->canViewPages($member);
888
		}
889
890
		// check for any logged-in users
891
		if($this->CanViewType == 'LoggedInUsers' && $member) {
892
			return true;
893
		}
894
895
		// check for specific groups
896
		if($member && is_numeric($member)) {
897
			$member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
898
		}
899
		if(
900
			$this->CanViewType == 'OnlyTheseUsers'
901
			&& $member
902
			&& $member->inGroups($this->ViewerGroups())
903
		) return true;
904
905
		return false;
906
	}
907
908
	/**
909
	 * Check if this page can be published
910
	 *
911
	 * @param Member $member
912
	 * @return bool
913
	 */
914
	public function canPublish($member = null) {
915
		if(!$member) {
916
			$member = Member::currentUser();
917
		}
918
919
		// Check extension
920
		$extended = $this->extendedCan('canPublish', $member);
921
		if($extended !== null) {
922
			return $extended;
923
		}
924
925
		if(Permission::checkMember($member, "ADMIN")) {
926
			return true;
927
		}
928
929
		// Default to relying on edit permission
930
		return $this->canEdit($member);
931
	}
932
933
	/**
934
	 * This function should return true if the current user can delete this page. It can be overloaded to customise the
935
	 * security model for an application.
936
	 *
937
	 * Denies permission if any of the following conditions is true:
938
	 * - canDelete() returns false on any extension
939
	 * - canEdit() returns false
940
	 * - any descendant page returns false for canDelete()
941
	 *
942
	 * @uses canDelete()
943
	 * @uses SiteTreeExtension->canDelete()
944
	 * @uses canEdit()
945
	 *
946
	 * @param Member $member
947
	 * @return bool True if the current user can delete this page
948
	 */
949
	public function canDelete($member = null) {
950 View Code Duplication
		if($member instanceof Member) $memberID = $member->ID;
0 ignored issues
show
Bug introduced by
The class SilverStripe\Security\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...
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...
951
		else if(is_numeric($member)) $memberID = $member;
952
		else $memberID = Member::currentUserID();
953
954
		// Standard mechanism for accepting permission changes from extensions
955
		$extended = $this->extendedCan('canDelete', $memberID);
956
		if($extended !== null) {
957
			return $extended;
958
		}
959
960
		// Default permission check
961
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
962
			return true;
963
		}
964
965
		// Regular canEdit logic is handled by can_edit_multiple
966
		$results = self::can_delete_multiple(array($this->ID), $memberID);
967
968
		// If this page no longer exists in stage/live results won't contain the page.
969
		// Fail-over to false
970
		return isset($results[$this->ID]) ? $results[$this->ID] : false;
971
	}
972
973
	/**
974
	 * This function should return true if the current user can create new pages of this class, regardless of class. It
975
	 * can be overloaded to customise the security model for an application.
976
	 *
977
	 * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
978
	 * create beneath a parent is based on the ability to edit that parent page.
979
	 *
980
	 * Use {@link canAddChildren()} to control behaviour of creating children under this page.
981
	 *
982
	 * @uses $can_create
983
	 * @uses DataExtension->canCreate()
984
	 *
985
	 * @param Member $member
986
	 * @param array $context Optional array which may contain array('Parent' => $parentObj)
987
	 *                       If a parent page is known, it will be checked for validity.
988
	 *                       If omitted, it will be assumed this is to be created as a top level page.
989
	 * @return bool True if the current user can create pages on this class.
990
	 */
991
	public function canCreate($member = null, $context = array()) {
992 View Code Duplication
		if(!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
993
			$member = Member::currentUserID();
994
		}
995
996
		// Check parent (custom canCreate option for SiteTree)
997
		// Block children not allowed for this parent type
998
		$parent = isset($context['Parent']) ? $context['Parent'] : null;
999
		if($parent && !in_array(get_class($this), $parent->allowedChildren())) {
1000
			return false;
1001
		}
1002
1003
		// Standard mechanism for accepting permission changes from extensions
1004
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
1005
		if($extended !== null) {
1006
			return $extended;
1007
		}
1008
1009
		// Check permission
1010
		if($member && Permission::checkMember($member, "ADMIN")) {
1011
			return true;
1012
		}
1013
1014
		// Fall over to inherited permissions
1015
		if($parent) {
1016
			return $parent->canAddChildren($member);
1017
		} else {
1018
			// This doesn't necessarily mean we are creating a root page, but that
1019
			// we don't know if there is a parent, so default to this permission
1020
			return SiteConfig::current_site_config()->canCreateTopLevel($member);
1021
		}
1022
	}
1023
1024
	/**
1025
	 * This function should return true if the current user can edit this page. It can be overloaded to customise the
1026
	 * security model for an application.
1027
	 *
1028
	 * Denies permission if any of the following conditions is true:
1029
	 * - canEdit() on any extension returns false
1030
	 * - canView() return false
1031
	 * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1032
	 * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1033
	 *   CMS_Access_CMSMAIN permission code
1034
	 * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1035
	 *
1036
	 * @uses canView()
1037
	 * @uses EditorGroups()
1038
	 * @uses DataExtension->canEdit()
1039
	 *
1040
	 * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1041
	 *                       unit tests)
1042
	 * @return bool True if the current user can edit this page
1043
	 */
1044
	public function canEdit($member = null) {
1045 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 SilverStripe\Security\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...
1046
		else if(is_numeric($member)) $memberID = $member;
1047
		else $memberID = Member::currentUserID();
1048
1049
		// Standard mechanism for accepting permission changes from extensions
1050
		$extended = $this->extendedCan('canEdit', $memberID);
1051
		if($extended !== null) {
1052
			return $extended;
1053
		}
1054
1055
		// Default permissions
1056
		if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1057
			return true;
1058
		}
1059
1060
		if($this->ID) {
1061
			// Regular canEdit logic is handled by can_edit_multiple
1062
			$results = self::can_edit_multiple(array($this->ID), $memberID);
1063
1064
			// If this page no longer exists in stage/live results won't contain the page.
1065
			// Fail-over to false
1066
			return isset($results[$this->ID]) ? $results[$this->ID] : false;
1067
1068
		// Default for unsaved pages
1069
		} else {
1070
			return $this->getSiteConfig()->canEditPages($member);
1071
		}
1072
	}
1073
1074
	/**
1075
	 * Stub method to get the site config, unless the current class can provide an alternate.
1076
	 *
1077
	 * @return SiteConfig
1078
	 */
1079
	public function getSiteConfig() {
1080
		$configs = $this->invokeWithExtensions('alternateSiteConfig');
1081
		foreach(array_filter($configs) as $config) {
1082
			return $config;
1083
		}
1084
1085
		return SiteConfig::current_site_config();
1086
	}
1087
1088
	/**
1089
	 * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1090
	 * can_(perm)_multiple method for efficiency.
1091
	 *
1092
	 * @param string          $permission    The permission: edit, view, publish, approve, etc.
1093
	 * @param array           $ids           An array of page IDs
1094
	 * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1095
	 *                                       to 'SiteTree::can_(permission)_multiple'
1096
	 */
1097
	static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
1098
		if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
1099
1100
		if(is_callable($batchCallback)) {
1101
			call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1102
		} else {
1103
			user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1104
				. "with callback '$batchCallback'", E_USER_WARNING);
1105
		}
1106
	}
1107
1108
	/**
1109
	 * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1110
	 * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1111
	 * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1112
	 * efficiently.
1113
	 *
1114
	 * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1115
	 * property to FALSE.
1116
	 *
1117
	 * @param array  $ids              Of {@link SiteTree} IDs
1118
	 * @param int    $memberID         Member ID
1119
	 * @param string $typeField        A property on the data record, e.g. "CanEditType".
1120
	 * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1121
	 * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1122
	 * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1123
	 * @param bool   $useCached
1124
	 * @return array An map of {@link SiteTree} ID keys to boolean values
1125
	 */
1126
	public static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod,
1127
												  $globalPermission = null, $useCached = true) {
1128
		if($globalPermission === NULL) $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1129
1130
		// Sanitise the IDs
1131
		$ids = array_filter($ids, 'is_numeric');
1132
1133
		// This is the name used on the permission cache
1134
		// converts something like 'CanEditType' to 'edit'.
1135
		$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1136
1137
		// Default result: nothing editable
1138
		$result = array_fill_keys($ids, false);
1139
		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...
1140
1141
			// Look in the cache for values
1142
			if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1143
				$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1144
1145
				// If we can't find everything in the cache, then look up the remainder separately
1146
				$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1147
				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...
1148
					$cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
1149
				}
1150
				return $cachedValues;
1151
			}
1152
1153
			// If a member doesn't have a certain permission then they can't edit anything
1154
			if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1155
				return $result;
1156
			}
1157
1158
			// Placeholder for parameterised ID list
1159
			$idPlaceholders = DB::placeholders($ids);
1160
1161
			// If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1162
			// be enabled
1163
			//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1164
1165
			// Get the groups that the given member belongs to
1166
			$groupIDs = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID)->Groups()->column("ID");
1167
			$SQL_groupList = implode(", ", $groupIDs);
1168
			if (!$SQL_groupList) $SQL_groupList = '0';
1169
1170
			$combinedStageResult = array();
1171
1172
			foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1173
				// Start by filling the array with the pages that actually exist
1174
				$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1175
1176
				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...
1177
					$idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1178
					$stageIds = DB::prepared_query($idQuery, $ids)->column();
1179
				} else {
1180
					$stageIds = array();
1181
				}
1182
				$result = array_fill_keys($stageIds, false);
1183
1184
				// Get the uninherited permissions
1185
				$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
1186
					->where(array(
1187
						"(\"$typeField\" = 'LoggedInUsers' OR
1188
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1189
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1190
						=> $ids
1191
					))
1192
					->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1193
1194
				if($uninheritedPermissions) {
1195
					// Set all the relevant items in $result to true
1196
					$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1197
				}
1198
1199
				// Get permissions that are inherited
1200
				$potentiallyInherited = Versioned::get_by_stage(
1201
					"SiteTree",
1202
					$stage,
1203
					array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
1204
				);
1205
1206
				if($potentiallyInherited) {
1207
					// Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1208
					// then see which ones the user has permission on
1209
					$groupedByParent = array();
1210
					foreach($potentiallyInherited as $item) {
1211
						/** @var SiteTree $item */
1212
						if($item->ParentID) {
1213
							if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
1214
							$groupedByParent[$item->ParentID][] = $item->ID;
1215
						} else {
1216
							// Might return different site config based on record context, e.g. when subsites module
1217
							// is used
1218
							$siteConfig = $item->getSiteConfig();
1219
							$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1220
						}
1221
					}
1222
1223
					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...
1224
						$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1225
						if($actuallyInherited) {
1226
							$parentIDs = array_keys(array_filter($actuallyInherited));
1227
							foreach($parentIDs as $parentID) {
1228
								// Set all the relevant items in $result to true
1229
								$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1230
							}
1231
						}
1232
					}
1233
				}
1234
1235
				$combinedStageResult = $combinedStageResult + $result;
1236
1237
			}
1238
		}
1239
1240
		if(isset($combinedStageResult)) {
1241
			// Cache the results
1242
 			if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
1243
 			self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1244
			return $combinedStageResult;
1245
		} else {
1246
			return array();
1247
		}
1248
	}
1249
1250
	/**
1251
	 * Get the 'can edit' information for a number of SiteTree pages.
1252
	 *
1253
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1254
	 * @param int   $memberID  ID of member
1255
	 * @param bool  $useCached Return values from the permission cache if they exist
1256
	 * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1257
	 *                         edited
1258
	 */
1259
	static public function can_edit_multiple($ids, $memberID, $useCached = true) {
1260
		return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1261
	}
1262
1263
	/**
1264
	 * Get the 'can edit' information for a number of SiteTree pages.
1265
	 *
1266
	 * @param array $ids       An array of IDs of the SiteTree pages to look up
1267
	 * @param int   $memberID  ID of member
1268
	 * @param bool  $useCached Return values from the permission cache if they exist
1269
	 * @return array
1270
	 */
1271
	static public function can_delete_multiple($ids, $memberID, $useCached = true) {
1272
		$deletable = array();
1273
		$result = array_fill_keys($ids, false);
1274
		$cacheKey = "delete-$memberID";
1275
1276
		// Look in the cache for values
1277
		if($useCached && isset(self::$cache_permissions[$cacheKey])) {
1278
			$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1279
1280
			// If we can't find everything in the cache, then look up the remainder separately
1281
			$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1282
			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...
1283
				$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1284
					+ $cachedValues;
1285
			}
1286
			return $cachedValues;
1287
		}
1288
1289
		// You can only delete pages that you can edit
1290
		$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1291
		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...
1292
1293
			// You can only delete pages whose children you can delete
1294
			$editablePlaceholders = DB::placeholders($editableIDs);
1295
			$childRecords = SiteTree::get()->where(array(
1296
				"\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1297
			));
1298
			if($childRecords) {
1299
				$children = $childRecords->map("ID", "ParentID");
1300
1301
				// Find out the children that can be deleted
1302
				$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1303
1304
				// Get a list of all the parents that have no undeletable children
1305
				$deletableParents = array_fill_keys($editableIDs, true);
1306
				foreach($deletableChildren as $id => $canDelete) {
1307
					if(!$canDelete) unset($deletableParents[$children[$id]]);
1308
				}
1309
1310
				// Use that to filter the list of deletable parents that have children
1311
				$deletableParents = array_keys($deletableParents);
1312
1313
				// Also get the $ids that don't have children
1314
				$parents = array_unique($children->values());
1315
				$deletableLeafNodes = array_diff($editableIDs, $parents);
1316
1317
				// Combine the two
1318
				$deletable = array_merge($deletableParents, $deletableLeafNodes);
1319
1320
			} else {
1321
				$deletable = $editableIDs;
1322
			}
1323
		}
1324
1325
		// Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1326
		return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1327
	}
1328
1329
	/**
1330
	 * Collate selected descendants of this page.
1331
	 *
1332
	 * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1333
	 * $collator array.
1334
	 *
1335
	 * @param string $condition The PHP condition to be evaluated. The page will be called $item
1336
	 * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1337
	 * @return bool
1338
	 */
1339
	public function collateDescendants($condition, &$collator) {
1340
		if($children = $this->Children()) {
1341
			foreach($children as $item) {
1342
1343
				if(eval("return $condition;")) {
1344
					$collator[] = $item;
1345
				}
1346
				/** @var SiteTree $item */
1347
				$item->collateDescendants($condition, $collator);
1348
			}
1349
			return true;
1350
		}
1351
	}
1352
1353
	/**
1354
	 * Return the title, description, keywords and language metatags.
1355
	 *
1356
	 * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1357
	 *
1358
	 * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1359
	 * @return string The XHTML metatags
1360
	 */
1361
	public function MetaTags($includeTitle = true) {
1362
		$tags = array();
1363
		if($includeTitle && strtolower($includeTitle) != 'false') {
1364
			$tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1365
		}
1366
1367
		$generator = trim(Config::inst()->get('SiteTree', 'meta_generator'));
1368
		if (!empty($generator)) {
1369
			$tags[] = FormField::create_tag('meta', array(
1370
				'name' => 'generator',
1371
				'content' => $generator,
1372
			));
1373
		}
1374
1375
		$charset = Config::inst()->get('ContentNegotiator', 'encoding');
1376
		$tags[] = FormField::create_tag('meta', array(
1377
			'http-equiv' => 'Content-Type',
1378
			'content' => 'text/html; charset=' . $charset,
1379
		));
1380
		if($this->MetaDescription) {
1381
			$tags[] = FormField::create_tag('meta', array(
1382
				'name' => 'description',
1383
				'content' => $this->MetaDescription,
1384
			));
1385
		}
1386
1387
		if(Permission::check('CMS_ACCESS_CMSMain')
1388
			&& in_array('CMSPreviewable', class_implements($this))
1389
			&& !$this instanceof ErrorPage
1390
			&& $this->ID > 0
1391
		) {
1392
			$tags[] = FormField::create_tag('meta', array(
1393
				'name' => 'x-page-id',
1394
				'content' => $this->obj('ID')->forTemplate(),
1395
			));
1396
			$tags[] = FormField::create_tag('meta', array(
1397
				'name' => 'x-cms-edit-link',
1398
				'content' => $this->obj('CMSEditLink')->forTemplate(),
1399
			));
1400
		}
1401
1402
		$tags = implode("\n", $tags);
1403
		if($this->ExtraMeta) {
1404
			$tags .= $this->obj('ExtraMeta')->forTemplate();
1405
		}
1406
1407
		$this->extend('MetaTags', $tags);
1408
1409
		return $tags;
1410
	}
1411
1412
	/**
1413
	 * Returns the object that contains the content that a user would associate with this page.
1414
	 *
1415
	 * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1416
	 * return the page that is linked to.
1417
	 *
1418
	 * @return $this
1419
	 */
1420
	public function ContentSource() {
1421
		return $this;
1422
	}
1423
1424
	/**
1425
	 * Add default records to database.
1426
	 *
1427
	 * This function is called whenever the database is built, after the database tables have all been created. Overload
1428
	 * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1429
	 */
1430
	public function requireDefaultRecords() {
1431
		parent::requireDefaultRecords();
1432
1433
		// default pages
1434
		if($this->class == 'SiteTree' && $this->config()->create_default_pages) {
1435
			if(!SiteTree::get_by_link(Config::inst()->get('RootURLController', 'default_homepage_link'))) {
1436
				$homepage = new Page();
1437
				$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1438
				$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>');
1439
				$homepage->URLSegment = Config::inst()->get('RootURLController', 'default_homepage_link');
1440
				$homepage->Sort = 1;
1441
				$homepage->write();
1442
				$homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1443
				$homepage->flushCache();
1444
				DB::alteration_message('Home page created', 'created');
1445
			}
1446
1447
			if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1448
				$aboutus = new Page();
1449
				$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1450
				$aboutus->Content = _t('SiteTree.DEFAULTABOUTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>');
1451
				$aboutus->Sort = 2;
1452
				$aboutus->write();
1453
				$aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1454
				$aboutus->flushCache();
1455
				DB::alteration_message('About Us page created', 'created');
1456
1457
				$contactus = new Page();
1458
				$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1459
				$contactus->Content = _t('SiteTree.DEFAULTCONTACTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>');
1460
				$contactus->Sort = 3;
1461
				$contactus->write();
1462
				$contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1463
				$contactus->flushCache();
1464
				DB::alteration_message('Contact Us page created', 'created');
1465
			}
1466
		}
1467
	}
1468
1469
	protected function onBeforeWrite() {
1470
		parent::onBeforeWrite();
1471
1472
		// If Sort hasn't been set, make this page come after it's siblings
1473
		if(!$this->Sort) {
1474
			$parentID = ($this->ParentID) ? $this->ParentID : 0;
1475
			$this->Sort = DB::prepared_query(
1476
				"SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1477
				array($parentID)
1478
			)->value();
1479
		}
1480
1481
		// If there is no URLSegment set, generate one from Title
1482
		$defaultSegment = $this->generateURLSegment(_t(
1483
			'CMSMain.NEWPAGE',
1484
			array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1485
		));
1486
		if((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1487
			$this->URLSegment = $this->generateURLSegment($this->Title);
1488
		} else if($this->isChanged('URLSegment', 2)) {
1489
			// Do a strict check on change level, to avoid double encoding caused by
1490
			// bogus changes through forceChange()
1491
			$filter = URLSegmentFilter::create();
1492
			$this->URLSegment = $filter->filter($this->URLSegment);
1493
			// If after sanitising there is no URLSegment, give it a reasonable default
1494
			if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
1495
		}
1496
1497
		// Ensure that this object has a non-conflicting URLSegment value.
1498
		$count = 2;
1499
		while(!$this->validURLSegment()) {
1500
			$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1501
			$count++;
1502
		}
1503
1504
		$this->syncLinkTracking();
1505
1506
		// Check to see if we've only altered fields that shouldn't affect versioning
1507
		$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1508
		$changedFields = array_keys($this->getChangedFields(true, 2));
1509
1510
		// This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1511
		// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1512
		$oneChangedFields = array_keys($this->getChangedFields(true, 1));
1513
1514
		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...
1515
			// This will have the affect of preserving the versioning
1516
			$this->migrateVersion($this->Version);
1517
		}
1518
	}
1519
1520
	/**
1521
	 * Trigger synchronisation of link tracking
1522
	 *
1523
	 * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1524
	 */
1525
	public function syncLinkTracking() {
1526
		$this->extend('augmentSyncLinkTracking');
1527
	}
1528
1529
	public function onBeforeDelete() {
1530
		parent::onBeforeDelete();
1531
1532
		// If deleting this page, delete all its children.
1533
		if(SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
1534
			foreach($children as $child) {
1535
				$child->delete();
1536
			}
1537
		}
1538
	}
1539
1540
	public function onAfterDelete() {
1541
		// Need to flush cache to avoid outdated versionnumber references
1542
		$this->flushCache();
1543
1544
		// Need to mark pages depending to this one as broken
1545
		$dependentPages = $this->DependentPages();
1546
		if($dependentPages) foreach($dependentPages as $page) {
1547
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
1548
			$page->write();
1549
		}
1550
1551
		parent::onAfterDelete();
1552
	}
1553
1554
	public function flushCache($persistent = true) {
1555
		parent::flushCache($persistent);
1556
		$this->_cache_statusFlags = null;
1557
	}
1558
1559
	public function validate() {
1560
		$result = parent::validate();
1561
1562
		// Allowed children validation
1563
		$parent = $this->getParent();
1564
		if($parent && $parent->exists()) {
1565
			// No need to check for subclasses or instanceof, as allowedChildren() already
1566
			// deconstructs any inheritance trees already.
1567
			$allowed = $parent->allowedChildren();
1568
			$subject = ($this instanceof VirtualPage && $this->CopyContentFromID) ? $this->CopyContentFrom() : $this;
1569
			if(!in_array($subject->ClassName, $allowed)) {
1570
1571
				$result->error(
1572
					_t(
1573
						'SiteTree.PageTypeNotAllowed',
1574
						'Page type "{type}" not allowed as child of this parent page',
1575
						array('type' => $subject->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $subject->i18n_singular_name()) is of type array<string,?,{"type":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1576
					),
1577
					'ALLOWED_CHILDREN'
1578
				);
1579
			}
1580
		}
1581
1582
		// "Can be root" validation
1583
		if(!$this->stat('can_be_root') && !$this->ParentID) {
1584
			$result->error(
1585
				_t(
1586
					'SiteTree.PageTypNotAllowedOnRoot',
1587
					'Page type "{type}" is not allowed on the root level',
1588
					array('type' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $this->i18n_singular_name()) is of type array<string,string,{"type":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1589
				),
1590
				'CAN_BE_ROOT'
1591
			);
1592
		}
1593
1594
		return $result;
1595
	}
1596
1597
	/**
1598
	 * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1599
	 * checks for:
1600
	 *  - A page with the same URLSegment that has a conflict
1601
	 *  - Conflicts with actions on the parent page
1602
	 *  - A conflict caused by a root page having the same URLSegment as a class name
1603
	 *
1604
	 * @return bool
1605
	 */
1606
	public function validURLSegment() {
1607
		if(self::config()->nested_urls && $parent = $this->Parent()) {
1608
			if($controller = ModelAsController::controller_for($parent)) {
1609
				if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1610
			}
1611
		}
1612
1613
		if(!self::config()->nested_urls || !$this->ParentID) {
1614
			if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1615
		}
1616
1617
		// Filters by url, id, and parent
1618
		$filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1619
		if($this->ID) {
1620
			$filter['"SiteTree"."ID" <> ?'] = $this->ID;
1621
		}
1622
		if(self::config()->nested_urls) {
1623
			$filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
1624
		}
1625
1626
		$votes = array_filter(
1627
			(array)$this->extend('augmentValidURLSegment'),
1628
			function($v) {return !is_null($v);}
1629
		);
1630
		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...
1631
			return min($votes);
1632
		}
1633
1634
		// Check existence
1635
		$existingPage = DataObject::get_one('SiteTree', $filter);
1636
		if ($existingPage) return false;
1637
1638
		return !($existingPage);
1639
		}
1640
1641
	/**
1642
	 * Generate a URL segment based on the title provided.
1643
	 *
1644
	 * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1645
	 * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1646
	 * the title that was originally used as the source of this generated URL. This lets extensions either start from
1647
	 * scratch, or incrementally modify the generated URL.
1648
	 *
1649
	 * @param string $title Page title
1650
	 * @return string Generated url segment
1651
	 */
1652
	public function generateURLSegment($title){
1653
		$filter = URLSegmentFilter::create();
1654
		$t = $filter->filter($title);
1655
1656
		// Fallback to generic page name if path is empty (= no valid, convertable characters)
1657
		if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";
1658
1659
		// Hook for extensions
1660
		$this->extend('updateURLSegment', $t, $title);
1661
1662
		return $t;
1663
	}
1664
1665
	/**
1666
	 * Gets the URL segment for the latest draft version of this page.
1667
	 *
1668
	 * @return string
1669
	 */
1670
	public function getStageURLSegment() {
1671
		$stageRecord = Versioned::get_one_by_stage('SiteTree', Versioned::DRAFT, array(
1672
			'"SiteTree"."ID"' => $this->ID
1673
		));
1674
		return ($stageRecord) ? $stageRecord->URLSegment : null;
1675
	}
1676
1677
	/**
1678
	 * Gets the URL segment for the currently published version of this page.
1679
	 *
1680
	 * @return string
1681
	 */
1682
	public function getLiveURLSegment() {
1683
		$liveRecord = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
1684
			'"SiteTree"."ID"' => $this->ID
1685
		));
1686
		return ($liveRecord) ? $liveRecord->URLSegment : null;
1687
	}
1688
1689
	/**
1690
	 * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1691
	 *
1692
	 * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1693
	 * @return ArrayList
1694
	 */
1695
	public function DependentPages($includeVirtuals = true) {
1696
		if(class_exists('Subsite')) {
1697
			$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1698
			Subsite::disable_subsite_filter(true);
1699
		}
1700
1701
		// Content links
1702
		$items = new ArrayList();
1703
1704
		// We merge all into a regular SS_List, because DataList doesn't support merge
1705
		if($contentLinks = $this->BackLinkTracking()) {
1706
			$linkList = new ArrayList();
1707
			foreach($contentLinks as $item) {
1708
				$item->DependentLinkType = 'Content link';
1709
				$linkList->push($item);
1710
			}
1711
			$items->merge($linkList);
1712
		}
1713
1714
		// Virtual pages
1715
		if($includeVirtuals) {
1716
			$virtuals = $this->VirtualPages();
1717
			if($virtuals) {
1718
				$virtualList = new ArrayList();
1719
				foreach($virtuals as $item) {
1720
					$item->DependentLinkType = 'Virtual page';
1721
					$virtualList->push($item);
1722
				}
1723
				$items->merge($virtualList);
1724
			}
1725
		}
1726
1727
		// Redirector pages
1728
		$redirectors = RedirectorPage::get()->where(array(
1729
			'"RedirectorPage"."RedirectionType"' => 'Internal',
1730
			'"RedirectorPage"."LinkToID"' => $this->ID
1731
		));
1732
		if($redirectors) {
1733
			$redirectorList = new ArrayList();
1734
			foreach($redirectors as $item) {
1735
				$item->DependentLinkType = 'Redirector page';
1736
				$redirectorList->push($item);
1737
			}
1738
			$items->merge($redirectorList);
1739
		}
1740
1741
		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...
1742
1743
		return $items;
1744
	}
1745
1746
	/**
1747
	 * Return all virtual pages that link to this page.
1748
	 *
1749
	 * @return DataList
1750
	 */
1751
	public function VirtualPages() {
1752
		$pages = parent::VirtualPages();
1753
1754
		// Disable subsite filter for these pages
1755
		if($pages instanceof DataList) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\ORM\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...
1756
			return $pages->setDataQueryParam('Subsite.filter', false);
1757
		} else {
1758
			return $pages;
1759
		}
1760
	}
1761
1762
	/**
1763
	 * Returns a FieldList with which to create the main editing form.
1764
	 *
1765
	 * You can override this in your child classes to add extra fields - first get the parent fields using
1766
	 * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1767
	 *
1768
	 * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1769
	 * e.g. access control.
1770
	 *
1771
	 * @return FieldList The fields to be displayed in the CMS
1772
	 */
1773
	public function getCMSFields() {
1774
		require_once("forms/Form.php");
1775
		// Status / message
1776
		// Create a status message for multiple parents
1777
		if($this->ID && is_numeric($this->ID)) {
1778
			$linkedPages = $this->VirtualPages();
1779
1780
			$parentPageLinks = array();
1781
1782
			if($linkedPages->Count() > 0) {
1783
				foreach($linkedPages as $linkedPage) {
1784
					$parentPage = $linkedPage->Parent;
1785
					if($parentPage) {
1786
						if($parentPage->ID) {
1787
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1788
						} else {
1789
							$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
1790
								_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1791
								"</a>";
1792
						}
1793
					}
1794
				}
1795
1796
				$lastParent = array_pop($parentPageLinks);
1797
				$parentList = "'$lastParent'";
1798
1799
				if(count($parentPageLinks) > 0) {
1800
					$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1801
						. $parentList;
1802
				}
1803
1804
				$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...
1805
					'SiteTree.APPEARSVIRTUALPAGES',
1806
					"This content also appears on the virtual pages in the {title} sections.",
1807
					array('title' => $parentList)
0 ignored issues
show
Documentation introduced by
array('title' => $parentList) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1808
				);
1809
			}
1810
		}
1811
1812
		if($this->HasBrokenLink || $this->HasBrokenFile) {
1813
			$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...
1814
		}
1815
1816
		$dependentNote = '';
1817
		$dependentTable = new LiteralField('DependentNote', '<p></p>');
1818
1819
		// Create a table for showing pages linked to this one
1820
		$dependentPages = $this->DependentPages();
1821
		$dependentPagesCount = $dependentPages->Count();
1822
		if($dependentPagesCount) {
1823
			$dependentColumns = array(
1824
				'Title' => $this->fieldLabel('Title'),
1825
				'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1826
				'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1827
			);
1828
			if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1829
1830
			$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>');
1831
			$dependentTable = GridField::create(
1832
				'DependentPages',
1833
				false,
1834
				$dependentPages
1835
			);
1836
			$dependentTable->getConfig()->getComponentByType('GridFieldDataColumns')
1837
				->setDisplayFields($dependentColumns)
1838
				->setFieldFormatting(array(
1839
					'Title' => function($value, &$item) {
1840
						return sprintf(
1841
							'<a href="admin/pages/edit/show/%d">%s</a>',
1842
							(int)$item->ID,
1843
							Convert::raw2xml($item->Title)
1844
						);
1845
					},
1846
					'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...
1847
						return sprintf(
1848
							'<a href="%s" target="_blank">%s</a>',
1849
							Convert::raw2xml($value),
1850
							Convert::raw2xml($value)
1851
						);
1852
					}
1853
				));
1854
		}
1855
1856
		$baseLink = Controller::join_links (
1857
			Director::absoluteBaseURL(),
1858
			(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
1859
		);
1860
1861
		$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1862
			->setURLPrefix($baseLink)
1863
			->setDefaultURL($this->generateURLSegment(_t(
1864
				'CMSMain.NEWPAGE',
1865
				array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1866
			)));
1867
		$helpText = (self::config()->nested_urls && count($this->Children())) ? $this->fieldLabel('LinkChangeNote') : '';
1868
		if(!Config::inst()->get('URLSegmentFilter', 'default_allow_multibyte')) {
1869
			$helpText .= $helpText ? '<br />' : '';
1870
			$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1871
		}
1872
		$urlsegment->setHelpText($helpText);
1873
1874
		$fields = new FieldList(
1875
			$rootTab = new TabSet("Root",
1876
				$tabMain = new Tab('Main',
1877
					new TextField("Title", $this->fieldLabel('Title')),
1878
					$urlsegment,
1879
					new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1880
					$htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1881
					ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
1882
						array(
1883
							$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1884
							$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1885
						)
1886
					)->setHeadingLevel(4)
1887
				),
1888
				$tabDependent = new Tab('Dependent',
1889
					$dependentNote,
1890
					$dependentTable
1891
				)
1892
			)
1893
		);
1894
		$htmlField->addExtraClass('stacked');
1895
1896
		// Help text for MetaData on page content editor
1897
		$metaFieldDesc
1898
			->setRightTitle(
1899
				_t(
1900
					'SiteTree.METADESCHELP',
1901
					"Search engines use this content for displaying search results (although it will not influence their ranking)."
1902
				)
1903
			)
1904
			->addExtraClass('help');
1905
		$metaFieldExtra
1906
			->setRightTitle(
1907
				_t(
1908
					'SiteTree.METAEXTRAHELP',
1909
					"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1910
				)
1911
			)
1912
			->addExtraClass('help');
1913
1914
		// Conditional dependent pages tab
1915
		if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1916
		else $fields->removeFieldFromTab('Root', 'Dependent');
1917
1918
		$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
1919
1920
		if($this->ObsoleteClassName) {
1921
			$obsoleteWarning = _t(
1922
				'SiteTree.OBSOLETECLASS',
1923
				"This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1924
				array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Documentation introduced by
array('type' => $this->ObsoleteClassName) is of type array<string,?,{"type":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1925
			);
1926
1927
			$fields->addFieldToTab(
1928
				"Root.Main",
1929
				new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1930
				"Title"
1931
			);
1932
		}
1933
1934
		if(file_exists(BASE_PATH . '/install.php')) {
1935
			$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
1936
				"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
1937
				"Warning: You should remove install.php from this SilverStripe install for security reasons.")
1938
				. "</p>"), "Title");
1939
		}
1940
1941
		// Backwards compat: Rewrite nested "Content" tabs to toplevel
1942
		$fields->setTabPathRewrites(array(
1943
			'/^Root\.Content\.Main$/' => 'Root.Main',
1944
			'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
1945
		));
1946
1947
		if(self::$runCMSFieldsExtensions) {
1948
			$this->extend('updateCMSFields', $fields);
1949
		}
1950
1951
		return $fields;
1952
	}
1953
1954
1955
	/**
1956
	 * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1957
	 * for content-related fields.
1958
	 *
1959
	 * @return FieldList
1960
	 */
1961
	public function getSettingsFields() {
1962
		$groupsMap = array();
1963
		foreach(Group::get() as $group) {
1964
			// Listboxfield values are escaped, use ASCII char instead of &raquo;
1965
			$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1966
		}
1967
		asort($groupsMap);
1968
1969
		$fields = new FieldList(
1970
			$rootTab = new TabSet("Root",
1971
				$tabBehaviour = new Tab('Settings',
1972
					new DropdownField(
1973
						"ClassName",
1974
						$this->fieldLabel('ClassName'),
1975
						$this->getClassDropdown()
1976
					),
1977
					$parentTypeSelector = new CompositeField(
1978
						new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
1979
							"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1980
							"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1981
						)),
1982
						$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
1983
					),
1984
					$visibility = new FieldGroup(
1985
						new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1986
						new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1987
					),
1988
					$viewersOptionsField = new OptionsetField(
1989
						"CanViewType",
1990
						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
1991
					),
1992
					$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
1993
						->setSource($groupsMap)
1994
						->setAttribute(
1995
							'data-placeholder',
1996
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
1997
						),
1998
					$editorsOptionsField = new OptionsetField(
1999
						"CanEditType",
2000
						_t('SiteTree.EDITHEADER', "Who can edit this page?")
2001
					),
2002
					$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2003
						->setSource($groupsMap)
2004
						->setAttribute(
2005
							'data-placeholder',
2006
							_t('SiteTree.GroupPlaceholder', 'Click to select group')
2007
						)
2008
				)
2009
			)
2010
		);
2011
2012
		$visibility->setTitle($this->fieldLabel('Visibility'));
2013
2014
2015
		// This filter ensures that the ParentID dropdown selection does not show this node,
2016
		// or its descendents, as this causes vanishing bugs
2017
		$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2018
		$parentTypeSelector->addExtraClass('parentTypeSelector');
2019
2020
		$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2021
2022
		// Make page location fields read-only if the user doesn't have the appropriate permission
2023
		if(!Permission::check("SITETREE_REORGANISE")) {
2024
			$fields->makeFieldReadonly('ParentType');
2025
			if($this->ParentType == 'root') {
2026
				$fields->removeByName('ParentID');
2027
			} else {
2028
				$fields->makeFieldReadonly('ParentID');
2029
			}
2030
		}
2031
2032
		$viewersOptionsSource = array();
2033
		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2034
		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2035
		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2036
		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2037
		$viewersOptionsField->setSource($viewersOptionsSource);
2038
2039
		$editorsOptionsSource = array();
2040
		$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2041
		$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2042
		$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2043
		$editorsOptionsField->setSource($editorsOptionsSource);
2044
2045
		if(!Permission::check('SITETREE_GRANT_ACCESS')) {
2046
			$fields->makeFieldReadonly($viewersOptionsField);
2047
			if($this->CanViewType == 'OnlyTheseUsers') {
2048
				$fields->makeFieldReadonly($viewerGroupsField);
2049
			} else {
2050
				$fields->removeByName('ViewerGroups');
2051
			}
2052
2053
			$fields->makeFieldReadonly($editorsOptionsField);
2054
			if($this->CanEditType == 'OnlyTheseUsers') {
2055
				$fields->makeFieldReadonly($editorGroupsField);
2056
			} else {
2057
				$fields->removeByName('EditorGroups');
2058
			}
2059
		}
2060
2061
		if(self::$runCMSFieldsExtensions) {
2062
			$this->extend('updateSettingsFields', $fields);
2063
		}
2064
2065
		return $fields;
2066
	}
2067
2068
	/**
2069
	 * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2070
	 * @return array
2071
	 */
2072
	public function fieldLabels($includerelations = true) {
2073
		$cacheKey = $this->class . '_' . $includerelations;
2074
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
2075
			$labels = parent::fieldLabels($includerelations);
2076
			$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2077
			$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2078
			$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2079
			$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2080
			$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2081
			$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2082
			$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2083
			$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2084
			$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2085
			$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2086
			$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2087
			$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2088
			$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2089
			$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2090
			$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2091
			$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2092
			$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2093
			$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2094
			$labels['LinkChangeNote'] = _t (
2095
				'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
2096
			);
2097
2098
			if($includerelations){
2099
				$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2100
				$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2101
				$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2102
				$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2103
			}
2104
2105
			self::$_cache_field_labels[$cacheKey] = $labels;
2106
		}
2107
2108
		return self::$_cache_field_labels[$cacheKey];
2109
	}
2110
2111
	/**
2112
	 * Get the actions available in the CMS for this page - eg Save, Publish.
2113
	 *
2114
	 * Frontend scripts and styles know how to handle the following FormFields:
2115
	 * - top-level FormActions appear as standalone buttons
2116
	 * - top-level CompositeField with FormActions within appear as grouped buttons
2117
	 * - TabSet & Tabs appear as a drop ups
2118
	 * - FormActions within the Tab are restyled as links
2119
	 * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2120
	 *
2121
	 * @return FieldList The available actions for this page.
2122
	 */
2123
	public function getCMSActions() {
2124
		$existsOnLive = $this->isPublished();
2125
2126
		// Major actions appear as buttons immediately visible as page actions.
2127
		$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('btn-group ss-ui-buttonset noborder');
2128
2129
		// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2130
		$rootTabSet = new TabSet('ActionMenus');
2131
		$moreOptions = new Tab(
2132
			'MoreOptions',
2133
			_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2134
		);
2135
		$rootTabSet->push($moreOptions);
2136
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2137
2138
		// Render page information into the "more-options" drop-up, on the top.
2139
		$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
2140
			'"SiteTree"."ID"' => $this->ID
2141
		));
2142
		$moreOptions->push(
2143
			new LiteralField('Information',
2144
				$this->customise(array(
2145
					'Live' => $live,
2146
					'ExistsOnLive' => $existsOnLive
2147
				))->renderWith('SiteTree_Information')
2148
			)
2149
		);
2150
2151
		$moreOptions->push(AddToCampaignHandler_FormAction::create());
2152
2153
		// "readonly"/viewing version that isn't the current version of the record
2154
		$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), array(
2155
			'"SiteTree"."ID"' => $this->ID
2156
		));
2157
		if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
2158
			$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2159
			$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2160
2161
			$actions = new FieldList(array($majorActions, $rootTabSet));
2162
2163
			// getCMSActions() can be extended with updateCMSActions() on a extension
2164
			$this->extend('updateCMSActions', $actions);
2165
2166
			return $actions;
2167
		}
2168
2169
		if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
2170
			// "unpublish"
2171
			$moreOptions->push(
2172
				FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2173
					->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2174
					->addExtraClass('ss-ui-action-destructive')
2175
			);
2176
		}
2177
2178
		if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE) && !$this->getIsDeletedFromStage()) {
2179
			if($this->isPublished() && $this->canEdit())	{
2180
				// "rollback"
2181
				$moreOptions->push(
2182
					FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
2183
						->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
2184
				);
2185
			}
2186
		}
2187
2188
		if($this->canEdit()) {
2189
			if($this->getIsDeletedFromStage()) {
2190
				// The usual major actions are not available, so we provide alternatives here.
2191
				if($existsOnLive) {
2192
					// "restore"
2193
					$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
2194
					if($this->canDelete() && $this->canUnpublish()) {
2195
						// "delete from live"
2196
						$majorActions->push(
2197
							FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
2198
								->addExtraClass('ss-ui-action-destructive')
2199
						);
2200
					}
2201
				} else {
2202
					// Determine if we should force a restore to root (where once it was a subpage)
2203
					$restoreToRoot = $this->isParentArchived();
2204
2205
					// "restore"
2206
					$title = $restoreToRoot
2207
						? _t('CMSMain.RESTORE_TO_ROOT','Restore draft at top level')
2208
						: _t('CMSMain.RESTORE','Restore draft');
2209
					$description = $restoreToRoot
2210
						? _t('CMSMain.RESTORE_TO_ROOT_DESC','Restore the archived version to draft as a top level page')
2211
						: _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2212
					$majorActions->push(
2213
						FormAction::create('restore', $title)
2214
							->setDescription($description)
2215
							->setAttribute('data-to-root', $restoreToRoot)
2216
							->setAttribute('data-icon', 'decline')
2217
					);
2218
				}
2219
			} else {
2220
					if($this->canDelete()) {
2221
						// delete
2222
						$moreOptions->push(
2223
							FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
2224
								->addExtraClass('delete ss-ui-action-destructive')
2225
						);
2226
					}
2227
				if($this->canArchive()) {
2228
					// "archive"
2229
					$moreOptions->push(
2230
						FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
2231
							->setDescription(_t(
2232
								'SiteTree.BUTTONARCHIVEDESC',
2233
								'Unpublish and send to archive'
2234
							))
2235
							->addExtraClass('delete ss-ui-action-destructive')
2236
					);
2237
				}
2238
2239
				// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2240
				$majorActions->push(
2241
					FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2242
						->setAttribute('data-icon', 'accept')
2243
						->setAttribute('data-icon-alternate', 'addpage')
2244
						->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
2245
				);
2246
			}
2247
		}
2248
2249
		if($this->canPublish() && !$this->getIsDeletedFromStage()) {
2250
			// "publish", as with "save", it supports an alternate state to show when action is needed.
2251
			$majorActions->push(
2252
				$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2253
					->setAttribute('data-icon', 'accept')
2254
					->setAttribute('data-icon-alternate', 'disk')
2255
					->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2256
			);
2257
2258
			// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2259
			if($this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE)) {
2260
				$publish->addExtraClass('ss-ui-alternate');
2261
			}
2262
		}
2263
2264
		$actions = new FieldList(array($majorActions, $rootTabSet));
2265
2266
		// Hook for extensions to add/remove actions.
2267
		$this->extend('updateCMSActions', $actions);
2268
2269
		return $actions;
2270
	}
2271
2272
	public function onAfterPublish() {
2273
		// Force live sort order to match stage sort order
2274
		DB::prepared_query('UPDATE "SiteTree_Live"
2275
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2276
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2277
			array($this->ParentID)
2278
		);
2279
		}
2280
2281
	/**
2282
	 * Update draft dependant pages
2283
	 */
2284
	public function onAfterRevertToLive() {
2285
		// Use an alias to get the updates made by $this->publish
2286
		/** @var SiteTree $stageSelf */
2287
		$stageSelf = Versioned::get_by_stage('SiteTree', Versioned::DRAFT)->byID($this->ID);
2288
		$stageSelf->writeWithoutVersion();
2289
2290
		// Need to update pages linking to this one as no longer broken
2291
		foreach($stageSelf->DependentPages() as $page) {
2292
			/** @var SiteTree $page */
2293
			$page->writeWithoutVersion();
2294
		}
2295
	}
2296
2297
	/**
2298
	 * Determine if this page references a parent which is archived, and not available in stage
2299
	 *
2300
	 * @return bool True if there is an archived parent
2301
	 */
2302
	protected function isParentArchived() {
2303
		if($parentID = $this->ParentID) {
2304
			$parentPage = Versioned::get_latest_version("SiteTree", $parentID);
2305
			if(!$parentPage || $parentPage->IsDeletedFromStage) {
2306
				return true;
2307
			}
2308
		}
2309
		return false;
2310
	}
2311
2312
	/**
2313
	 * Restore the content in the active copy of this SiteTree page to the stage site.
2314
	 *
2315
	 * @return self
2316
	 */
2317
	public function doRestoreToStage() {
2318
		$this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2319
2320
		// Ensure that the parent page is restored, otherwise restore to root
2321
		if($this->isParentArchived()) {
2322
			$this->ParentID = 0;
2323
		}
2324
2325
		// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2326
		// create an empty record
2327
		if(!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2328
			$conn = DB::get_conn();
2329
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
2330
			DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2331
			if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
2332
		}
2333
2334
		$oldReadingMode = Versioned::get_reading_mode();
2335
		Versioned::set_stage(Versioned::DRAFT);
2336
		$this->forceChange();
2337
		$this->write();
2338
2339
		$result = DataObject::get_by_id($this->class, $this->ID);
2340
2341
		// Need to update pages linking to this one as no longer broken
2342
		foreach($result->DependentPages(false) as $page) {
2343
			// $page->write() calls syncLinkTracking, which does all the hard work for us.
2344
			$page->write();
2345
		}
2346
2347
		Versioned::set_reading_mode($oldReadingMode);
2348
2349
		$this->invokeWithExtensions('onAfterRestoreToStage', $this);
2350
2351
		return $result;
2352
	}
2353
2354
	/**
2355
	 * Check if this page is new - that is, if it has yet to have been written to the database.
2356
	 *
2357
	 * @return bool
2358
	 */
2359
	public function isNew() {
2360
		/**
2361
		 * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2362
		 * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2363
		 */
2364
		if(empty($this->ID)) return true;
2365
2366
		if(is_numeric($this->ID)) return false;
2367
2368
		return stripos($this->ID, 'new') === 0;
2369
	}
2370
2371
	/**
2372
	 * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2373
	 * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2374
	 * {@link SiteTree::$needs_permission}.
2375
	 *
2376
	 * @return array
2377
	 */
2378
	protected function getClassDropdown() {
2379
		$classes = self::page_type_classes();
2380
		$currentClass = null;
2381
		$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...
2382
2383
		$result = array();
2384
		foreach($classes as $class) {
2385
			$instance = singleton($class);
2386
2387
			// if the current page type is this the same as the class type always show the page type in the list
2388
			if ($this->ClassName != $instance->ClassName) {
2389
				if($instance instanceof HiddenClass) continue;
0 ignored issues
show
Bug introduced by
The class SilverStripe\ORM\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...
2390
				if(!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) continue;
2391
			}
2392
2393
			if($perms = $instance->stat('need_permission')) {
2394
				if(!$this->can($perms)) continue;
2395
			}
2396
2397
			$pageTypeName = $instance->i18n_singular_name();
2398
2399
			$currentClass = $class;
2400
			$result[$class] = $pageTypeName;
2401
2402
			// If we're in translation mode, the link between the translated pagetype title and the actual classname
2403
			// might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2404
			// "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2405
			if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2406
				$result[$class] = $result[$class] .  " ({$class})";
2407
			}
2408
		}
2409
2410
		// sort alphabetically, and put current on top
2411
		asort($result);
2412
		if($currentClass) {
2413
			$currentPageTypeName = $result[$currentClass];
2414
			unset($result[$currentClass]);
2415
			$result = array_reverse($result);
2416
			$result[$currentClass] = $currentPageTypeName;
2417
			$result = array_reverse($result);
2418
		}
2419
2420
		return $result;
2421
	}
2422
2423
	/**
2424
	 * Returns an array of the class names of classes that are allowed to be children of this class.
2425
	 *
2426
	 * @return string[]
2427
	 */
2428
	public function allowedChildren() {
2429
		$allowedChildren = array();
2430
		$candidates = $this->stat('allowed_children');
2431
		if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2432
			foreach($candidates as $candidate) {
2433
				// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2434
				// Otherwise, the class and all its subclasses are allowed.
2435
				if(substr($candidate,0,1) == '*') {
2436
					$allowedChildren[] = substr($candidate,1);
2437
				} else {
2438
					$subclasses = ClassInfo::subclassesFor($candidate);
2439
					foreach($subclasses as $subclass) {
2440
						if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\ORM\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...
2441
							continue;
2442
						}
2443
						$allowedChildren[] = $subclass;
2444
					}
2445
				}
2446
			}
2447
		}
2448
2449
		return $allowedChildren;
2450
	}
2451
2452
	/**
2453
	 * Returns the class name of the default class for children of this page.
2454
	 *
2455
	 * @return string
2456
	 */
2457
	public function defaultChild() {
2458
		$default = $this->stat('default_child');
2459
		$allowed = $this->allowedChildren();
2460
		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...
2461
			if(!$default || !in_array($default, $allowed))
2462
				$default = reset($allowed);
2463
			return $default;
2464
		}
2465
	}
2466
2467
	/**
2468
	 * Returns the class name of the default class for the parent of this page.
2469
	 *
2470
	 * @return string
2471
	 */
2472
	public function defaultParent() {
2473
		return $this->stat('default_parent');
2474
	}
2475
2476
	/**
2477
	 * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2478
	 * Title field.
2479
	 *
2480
	 * @return string
2481
	 */
2482
	public function getMenuTitle(){
2483
		if($value = $this->getField("MenuTitle")) {
2484
			return $value;
2485
		} else {
2486
			return $this->getField("Title");
2487
		}
2488
	}
2489
2490
2491
	/**
2492
	 * Set the menu title for this page.
2493
	 *
2494
	 * @param string $value
2495
	 */
2496
	public function setMenuTitle($value) {
2497
		if($value == $this->getField("Title")) {
2498
			$this->setField("MenuTitle", null);
2499
		} else {
2500
			$this->setField("MenuTitle", $value);
2501
		}
2502
	}
2503
2504
	/**
2505
	 * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2506
	 * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2507
	 * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2508
	 * the flags.
2509
	 *
2510
	 * Example (simple):
2511
	 *   "deletedonlive" => "Deleted"
2512
	 *
2513
	 * Example (with optional title attribute):
2514
	 *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2515
	 *
2516
	 * @param bool $cached Whether to serve the fields from cache; false regenerate them
2517
	 * @return array
2518
	 */
2519
	public function getStatusFlags($cached = true) {
2520
		if(!$this->_cache_statusFlags || !$cached) {
2521
			$flags = array();
2522
			if($this->getIsDeletedFromStage()) {
2523
				if($this->isPublished()) {
2524
					$flags['removedfromdraft'] = array(
2525
						'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
2526
						'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
2527
					);
2528
				} else {
2529
					$flags['archived'] = array(
2530
						'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2531
						'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2532
					);
2533
				}
2534
			} else if($this->getIsAddedToStage()) {
2535
				$flags['addedtodraft'] = array(
2536
					'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2537
					'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2538
				);
2539
			} else if($this->getIsModifiedOnStage()) {
2540
				$flags['modified'] = array(
2541
					'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2542
					'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2543
				);
2544
			}
2545
2546
			$this->extend('updateStatusFlags', $flags);
2547
2548
			$this->_cache_statusFlags = $flags;
2549
		}
2550
2551
		return $this->_cache_statusFlags;
2552
	}
2553
2554
	/**
2555
	 * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2556
	 * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2557
	 * publication status.
2558
	 *
2559
	 * @return string An HTML string ready to be directly used in a template
2560
	 */
2561
	public function getTreeTitle() {
2562
		// Build the list of candidate children
2563
		$children = array();
2564
		$candidates = static::page_type_classes();
2565
		foreach($this->allowedChildren() as $childClass) {
2566
			if(!in_array($childClass, $candidates)) continue;
2567
			$child = singleton($childClass);
2568
			if($child->canCreate(null, array('Parent' => $this))) {
2569
				$children[$childClass] = $child->i18n_singular_name();
2570
			}
2571
		}
2572
		$flags = $this->getStatusFlags();
2573
		$treeTitle = sprintf(
2574
			"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2575
			Convert::raw2att(Convert::raw2json($children)),
2576
			Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
2577
		);
2578
		foreach($flags as $class => $data) {
2579
			if(is_string($data)) $data = array('text' => $data);
2580
			$treeTitle .= sprintf(
2581
				"<span class=\"badge %s\"%s>%s</span>",
2582
				'status-' . Convert::raw2xml($class),
2583
				(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2584
				Convert::raw2xml($data['text'])
2585
			);
2586
		}
2587
2588
		return $treeTitle;
2589
	}
2590
2591
	/**
2592
	 * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2593
	 * we're currently inside, etc.
2594
	 *
2595
	 * @param int $level
2596
	 * @return SiteTree
2597
	 */
2598
	public function Level($level) {
2599
		$parent = $this;
2600
		$stack = array($parent);
2601
		while($parent = $parent->Parent) {
2602
			array_unshift($stack, $parent);
2603
		}
2604
2605
		return isset($stack[$level-1]) ? $stack[$level-1] : null;
2606
	}
2607
2608
	/**
2609
	 * Gets the depth of this page in the sitetree, where 1 is the root level
2610
	 *
2611
	 * @return int
2612
	 */
2613
	public function getPageLevel() {
2614
		if($this->ParentID) {
2615
			return 1 + $this->Parent()->getPageLevel();
2616
		}
2617
		return 1;
2618
	}
2619
2620
	/**
2621
	 * Return the CSS classes to apply to this node in the CMS tree.
2622
	 *
2623
	 * @param string $numChildrenMethod
2624
	 * @return string
2625
	 */
2626
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
2627
		$classes = sprintf('class-%s', $this->class);
2628
		if($this->HasBrokenFile || $this->HasBrokenLink) {
2629
			$classes .= " BrokenLink";
2630
		}
2631
2632
		if(!$this->canAddChildren()) {
2633
			$classes .= " nochildren";
2634
		}
2635
2636
		if(!$this->canEdit() && !$this->canAddChildren()) {
2637
			if (!$this->canView()) {
2638
				$classes .= " disabled";
2639
			} else {
2640
				$classes .= " edit-disabled";
2641
			}
2642
		}
2643
2644
		if(!$this->ShowInMenus) {
2645
			$classes .= " notinmenu";
2646
		}
2647
2648
		//TODO: Add integration
2649
		/*
2650
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2651
			$classes .= " untranslated ";
2652
		*/
2653
		$classes .= $this->markingClasses($numChildrenMethod);
2654
2655
		return $classes;
2656
	}
2657
2658
	/**
2659
	 * Compares current draft with live version, and returns true if no draft version of this page exists  but the page
2660
	 * is still published (eg, after triggering "Delete from draft site" in the CMS).
2661
	 *
2662
	 * @return bool
2663
	 */
2664
	public function getIsDeletedFromStage() {
2665
		if(!$this->ID) return true;
2666
		if($this->isNew()) return false;
2667
2668
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', Versioned::DRAFT, $this->ID);
2669
2670
		// Return true for both completely deleted pages and for pages just deleted from stage
2671
		return !($stageVersion);
2672
	}
2673
2674
	/**
2675
	 * Return true if this page exists on the live site
2676
	 *
2677
	 * @return bool
2678
	 */
2679
	public function getExistsOnLive() {
2680
		return $this->isPublished();
2681
	}
2682
2683
	/**
2684
	 * Compares current draft with live version, and returns true if these versions differ, meaning there have been
2685
	 * unpublished changes to the draft site.
2686
	 *
2687
	 * @return bool
2688
	 */
2689
	public function getIsModifiedOnStage() {
2690
		// New unsaved pages could be never be published
2691
		if($this->isNew()) return false;
2692
2693
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2694
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2695
2696
		$isModified = ($stageVersion && $stageVersion != $liveVersion);
2697
		$this->extend('getIsModifiedOnStage', $isModified);
2698
2699
		return $isModified;
2700
	}
2701
2702
	/**
2703
	 * Compares current draft with live version, and returns true if no live version exists, meaning the page was never
2704
	 * published.
2705
	 *
2706
	 * @return bool
2707
	 */
2708
	public function getIsAddedToStage() {
2709
		// New unsaved pages could be never be published
2710
		if($this->isNew()) return false;
2711
2712
		$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2713
		$liveVersion =	Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2714
2715
		return ($stageVersion && !$liveVersion);
2716
	}
2717
2718
	/**
2719
	 * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2720
	 * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2721
	 */
2722
	static public function disableCMSFieldsExtensions() {
2723
		self::$runCMSFieldsExtensions = false;
2724
	}
2725
2726
	/**
2727
	 * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2728
	 * disableCMSFieldsExtensions().
2729
	 */
2730
	static public function enableCMSFieldsExtensions() {
2731
		self::$runCMSFieldsExtensions = true;
2732
	}
2733
2734
	public function providePermissions() {
2735
		return array(
2736
			'SITETREE_GRANT_ACCESS' => array(
2737
				'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2738
				'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2739
				'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2740
				'sort' => 100
2741
			),
2742
			'SITETREE_VIEW_ALL' => array(
2743
				'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2744
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2745
				'sort' => -100,
2746
				'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')
2747
			),
2748
			'SITETREE_EDIT_ALL' => array(
2749
				'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2750
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2751
				'sort' => -50,
2752
				'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')
2753
			),
2754
			'SITETREE_REORGANISE' => array(
2755
				'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2756
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2757
				'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2758
				'sort' => 100
2759
			),
2760
			'VIEW_DRAFT_CONTENT' => array(
2761
				'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2762
				'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2763
				'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.'),
2764
				'sort' => 100
2765
			)
2766
		);
2767
	}
2768
2769
	/**
2770
	 * Return the translated Singular name.
2771
	 *
2772
	 * @return string
2773
	 */
2774
	public function i18n_singular_name() {
2775
		// Convert 'Page' to 'SiteTree' for correct localization lookups
2776
		$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
2777
		return _t($class.'.SINGULARNAME', $this->singular_name());
2778
	}
2779
2780
	/**
2781
	 * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2782
	 * picks it up for the wrong folder.
2783
	 *
2784
	 * @return array
2785
	 */
2786
	public function provideI18nEntities() {
2787
		$entities = parent::provideI18nEntities();
2788
2789
		if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
2790
		if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;
2791
2792
		$entities[$this->class . '.DESCRIPTION'] = array(
2793
			$this->stat('description'),
2794
			'Description of the page type (shown in the "add page" dialog)'
2795
		);
2796
2797
		$entities['SiteTree.SINGULARNAME'][0] = 'Page';
2798
		$entities['SiteTree.PLURALNAME'][0] = 'Pages';
2799
2800
		return $entities;
2801
	}
2802
2803
	/**
2804
	 * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2805
	 *
2806
	 * @return string
2807
	 */
2808
	public function getParentType() {
2809
		return $this->ParentID == 0 ? 'root' : 'subpage';
2810
	}
2811
2812
	/**
2813
	 * Clear the permissions cache for SiteTree
2814
	 */
2815
	public static function reset() {
2816
		self::$cache_permissions = array();
2817
	}
2818
2819
	static public function on_db_reset() {
2820
		self::$cache_permissions = array();
2821
	}
2822
2823
}
2824